<?php

declare(strict_types=1);

namespace App\Framework\Database\Indexing;

use App\Framework\Database\Indexing\ValueObjects\IndexName;
use App\Framework\Database\Indexing\ValueObjects\IndexType;
use App\Framework\Database\Indexing\ValueObjects\IndexUsageMetrics;
use App\Framework\Database\PdoConnection;
use DateTimeImmutable;
use PDO;

/**
 * Core service for analyzing database index usage and effectiveness
 *
 * Parses EXPLAIN output and tracks real index usage statistics
 */
final readonly class IndexAnalyzer
{
    public function __construct(
        private PdoConnection $connection
    ) {}

    /**
     * Analyze a query and detect which indexes are actually used
     *
     * @return array{
     *     query: string,
     *     indexes_used: array<IndexName>,
     *     key_type: string,
     *     rows_examined: int,
     *     using_filesort: bool,
     *     using_temporary: bool,
     *     possible_keys: array<string>
     * }
     */
    public function analyzeQuery(string $sql): array
    {
        $driver = $this->connection->getDriver();

        return match ($driver) {
            'mysql' => $this->analyzeMySQLQuery($sql),
            'pgsql' => $this->analyzePostgreSQLQuery($sql),
            'sqlite' => $this->analyzeSQLiteQuery($sql),
            default => throw new \RuntimeException("Unsupported database driver: {$driver}")
        };
    }

    /**
     * Get all indexes for a table
     *
     * @return array<array{
     *     name: string,
     *     columns: array<string>,
     *     type: IndexType,
     *     is_unique: bool,
     *     is_primary: bool
     * }>
     */
    public function getTableIndexes(string $tableName): array
    {
        $driver = $this->connection->getDriver();

        return match ($driver) {
            'mysql' => $this->getMySQLIndexes($tableName),
            'pgsql' => $this->getPostgreSQLIndexes($tableName),
            'sqlite' => $this->getSQLiteIndexes($tableName),
            default => throw new \RuntimeException("Unsupported database driver: {$driver}")
        };
    }

    /**
     * Analyze MySQL query using EXPLAIN
     */
    private function analyzeMySQLQuery(string $sql): array
    {
        $stmt = $this->connection->getPdo()->prepare("EXPLAIN {$sql}");
        $stmt->execute();
        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$result) {
            return [
                'query' => $sql,
                'indexes_used' => [],
                'key_type' => 'none',
                'rows_examined' => 0,
                'using_filesort' => false,
                'using_temporary' => false,
                'possible_keys' => []
            ];
        }

        $indexesUsed = [];
        if (!empty($result['key']) && $result['key'] !== 'NULL') {
            $indexesUsed[] = new IndexName($result['key']);
        }

        $possibleKeys = [];
        if (!empty($result['possible_keys']) && $result['possible_keys'] !== 'NULL') {
            $possibleKeys = explode(',', $result['possible_keys']);
        }

        return [
            'query' => $sql,
            'indexes_used' => $indexesUsed,
            'key_type' => $result['type'] ?? 'unknown',
            'rows_examined' => (int) ($result['rows'] ?? 0),
            'using_filesort' => str_contains($result['Extra'] ?? '', 'Using filesort'),
            'using_temporary' => str_contains($result['Extra'] ?? '', 'Using temporary'),
            'possible_keys' => $possibleKeys
        ];
    }

    /**
     * Analyze PostgreSQL query using EXPLAIN
     */
    private function analyzePostgreSQLQuery(string $sql): array
    {
        $stmt = $this->connection->getPdo()->prepare("EXPLAIN (FORMAT JSON) {$sql}");
        $stmt->execute();
        $result = $stmt->fetch(PDO::FETCH_COLUMN);

        if (!$result) {
            return [
                'query' => $sql,
                'indexes_used' => [],
                'key_type' => 'none',
                'rows_examined' => 0,
                'using_filesort' => false,
                'using_temporary' => false,
                'possible_keys' => []
            ];
        }

        $plan = json_decode($result, true)[0]['Plan'] ?? [];

        $indexesUsed = [];
        $this->extractPostgreSQLIndexes($plan, $indexesUsed);

        return [
            'query' => $sql,
            'indexes_used' => $indexesUsed,
            'key_type' => $plan['Node Type'] ?? 'unknown',
            'rows_examined' => (int) ($plan['Plan Rows'] ?? 0),
            'using_filesort' => str_contains($plan['Node Type'] ?? '', 'Sort'),
            'using_temporary' => false, // PostgreSQL doesn't expose this directly
            'possible_keys' => [] // PostgreSQL doesn't expose this in EXPLAIN
        ];
    }

    /**
     * Recursively extract indexes from PostgreSQL EXPLAIN plan
     */
    private function extractPostgreSQLIndexes(array $plan, array &$indexes): void
    {
        if (isset($plan['Index Name'])) {
            $indexes[] = new IndexName($plan['Index Name']);
        }

        if (isset($plan['Plans'])) {
            foreach ($plan['Plans'] as $subPlan) {
                $this->extractPostgreSQLIndexes($subPlan, $indexes);
            }
        }
    }

    /**
     * Analyze SQLite query using EXPLAIN QUERY PLAN
     */
    private function analyzeSQLiteQuery(string $sql): array
    {
        $stmt = $this->connection->getPdo()->prepare("EXPLAIN QUERY PLAN {$sql}");
        $stmt->execute();
        $results = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $indexesUsed = [];
        $usingIndex = false;

        foreach ($results as $row) {
            $detail = $row['detail'] ?? '';

            // Extract index name from detail like "USING INDEX idx_users_email"
            if (preg_match('/USING INDEX ([a-zA-Z0-9_]+)/', $detail, $matches)) {
                $indexesUsed[] = new IndexName($matches[1]);
                $usingIndex = true;
            }
        }

        return [
            'query' => $sql,
            'indexes_used' => $indexesUsed,
            'key_type' => $usingIndex ? 'index' : 'scan',
            'rows_examined' => 0, // SQLite doesn't expose this
            'using_filesort' => false,
            'using_temporary' => false,
            'possible_keys' => []
        ];
    }

    /**
     * Get MySQL table indexes
     */
    private function getMySQLIndexes(string $tableName): array
    {
        $stmt = $this->connection->getPdo()->prepare("SHOW INDEX FROM {$tableName}");
        $stmt->execute();
        $results = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $indexes = [];
        foreach ($results as $row) {
            $indexName = $row['Key_name'];

            if (!isset($indexes[$indexName])) {
                $indexes[$indexName] = [
                    'name' => $indexName,
                    'columns' => [],
                    'type' => IndexType::BTREE, // Default for MySQL
                    'is_unique' => (int) $row['Non_unique'] === 0,
                    'is_primary' => $indexName === 'PRIMARY'
                ];
            }

            $indexes[$indexName]['columns'][] = $row['Column_name'];
        }

        return array_values($indexes);
    }

    /**
     * Get PostgreSQL table indexes
     */
    private function getPostgreSQLIndexes(string $tableName): array
    {
        $sql = "
            SELECT
                i.relname AS index_name,
                a.attname AS column_name,
                am.amname AS index_type,
                ix.indisunique AS is_unique,
                ix.indisprimary AS is_primary
            FROM
                pg_class t,
                pg_class i,
                pg_index ix,
                pg_attribute a,
                pg_am am
            WHERE
                t.oid = ix.indrelid
                AND i.oid = ix.indexrelid
                AND a.attrelid = t.oid
                AND a.attnum = ANY(ix.indkey)
                AND t.relkind = 'r'
                AND t.relname = :table_name
                AND i.relam = am.oid
            ORDER BY
                i.relname,
                a.attnum
        ";

        $stmt = $this->connection->getPdo()->prepare($sql);
        $stmt->execute(['table_name' => $tableName]);
        $results = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $indexes = [];
        foreach ($results as $row) {
            $indexName = $row['index_name'];

            if (!isset($indexes[$indexName])) {
                $indexes[$indexName] = [
                    'name' => $indexName,
                    'columns' => [],
                    'type' => IndexType::tryFrom(strtoupper($row['index_type'])) ?? IndexType::BTREE,
                    'is_unique' => (bool) $row['is_unique'],
                    'is_primary' => (bool) $row['is_primary']
                ];
            }

            $indexes[$indexName]['columns'][] = $row['column_name'];
        }

        return array_values($indexes);
    }

    /**
     * Get SQLite table indexes
     */
    private function getSQLiteIndexes(string $tableName): array
    {
        $stmt = $this->connection->getPdo()->prepare("PRAGMA index_list({$tableName})");
        $stmt->execute();
        $indexList = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $indexes = [];
        foreach ($indexList as $index) {
            $indexName = $index['name'];

            $stmt = $this->connection->getPdo()->prepare("PRAGMA index_info({$indexName})");
            $stmt->execute();
            $columns = $stmt->fetchAll(PDO::FETCH_ASSOC);

            $indexes[] = [
                'name' => $indexName,
                'columns' => array_column($columns, 'name'),
                'type' => IndexType::BTREE, // SQLite uses B-tree
                'is_unique' => (bool) $index['unique'],
                'is_primary' => str_contains($indexName, 'autoindex')
            ];
        }

        return $indexes;
    }
}
