<?php

declare(strict_types=1);

namespace App\Framework\Database\Schema\Index\Analysis;

use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Exception\DatabaseException;

/**
 * Analyzes index usage in the database
 */
final class IndexUsageAnalyzer
{
    /**
     * @var ConnectionInterface The database connection
     */
    private ConnectionInterface $connection;

    /**
     * Create a new index usage analyzer
     */
    public function __construct(ConnectionInterface $connection)
    {
        $this->connection = $connection;
    }

    /**
     * Get statistics about index usage
     *
     * @param string|null $table Optional table name to filter results
     * @return array<array<string, mixed>> Index usage statistics
     * @throws DatabaseException If the database doesn't support index usage statistics
     */
    public function getIndexUsageStatistics(?string $table = null): array
    {
        $driver = $this->getDriverName();

        return match($driver) {
            'mysql' => $this->getMySqlIndexUsageStatistics($table),
            'pgsql' => $this->getPostgreSqlIndexUsageStatistics($table),
            default => throw new DatabaseException("Index usage statistics not supported for {$driver}")
        };
    }

    /**
     * Get unused indexes
     *
     * @param string|null $table Optional table name to filter results
     * @param int $minDaysSinceCreation Minimum days since index creation to consider it unused
     * @return array<array<string, mixed>> Unused indexes
     * @throws DatabaseException If the database doesn't support index usage statistics
     */
    public function getUnusedIndexes(?string $table = null, int $minDaysSinceCreation = 30): array
    {
        $driver = $this->getDriverName();

        return match($driver) {
            'mysql' => $this->getMySqlUnusedIndexes($table, $minDaysSinceCreation),
            'pgsql' => $this->getPostgreSqlUnusedIndexes($table, $minDaysSinceCreation),
            default => throw new DatabaseException("Unused index detection not supported for {$driver}")
        };
    }

    /**
     * Get duplicate indexes
     *
     * @param string|null $table Optional table name to filter results
     * @return array<array<string, mixed>> Duplicate indexes
     * @throws DatabaseException If the database doesn't support duplicate index detection
     */
    public function getDuplicateIndexes(?string $table = null): array
    {
        $driver = $this->getDriverName();

        return match($driver) {
            'mysql' => $this->getMySqlDuplicateIndexes($table),
            'pgsql' => $this->getPostgreSqlDuplicateIndexes($table),
            default => throw new DatabaseException("Duplicate index detection not supported for {$driver}")
        };
    }

    /**
     * Get oversized indexes
     *
     * @param string|null $table Optional table name to filter results
     * @param int $sizeThresholdMb Size threshold in MB to consider an index oversized
     * @return array<array<string, mixed>> Oversized indexes
     * @throws DatabaseException If the database doesn't support index size statistics
     */
    public function getOversizedIndexes(?string $table = null, int $sizeThresholdMb = 100): array
    {
        $driver = $this->getDriverName();

        return match($driver) {
            'mysql' => $this->getMySqlOversizedIndexes($table, $sizeThresholdMb),
            'pgsql' => $this->getPostgreSqlOversizedIndexes($table, $sizeThresholdMb),
            default => throw new DatabaseException("Index size statistics not supported for {$driver}")
        };
    }

    /**
     * Get fragmented indexes
     *
     * @param string|null $table Optional table name to filter results
     * @param float $fragmentationThreshold Fragmentation threshold (0.0-1.0) to consider an index fragmented
     * @return array<array<string, mixed>> Fragmented indexes
     * @throws DatabaseException If the database doesn't support index fragmentation statistics
     */
    public function getFragmentedIndexes(?string $table = null, float $fragmentationThreshold = 0.3): array
    {
        $driver = $this->getDriverName();

        return match($driver) {
            'mysql' => $this->getMySqlFragmentedIndexes($table, $fragmentationThreshold),
            'pgsql' => $this->getPostgreSqlFragmentedIndexes($table, $fragmentationThreshold),
            default => throw new DatabaseException("Index fragmentation statistics not supported for {$driver}")
        };
    }

    /**
     * Get MySQL index usage statistics
     * @return array<array<string, mixed>>
     */
    private function getMySqlIndexUsageStatistics(?string $table = null): array
    {
        $whereClause = '';
        $params = [];

        if ($table !== null) {
            $whereClause = 'WHERE s.TABLE_NAME = ?';
            $params[] = $table;
        }

        $sql = <<<SQL
            SELECT
                t.TABLE_SCHEMA AS table_schema,
                t.TABLE_NAME AS table_name,
                s.INDEX_NAME AS index_name,
                s.COLUMN_NAME AS column_name,
                t.TABLE_ROWS AS table_rows,
                s.CARDINALITY AS cardinality,
                s.SEQ_IN_INDEX AS sequence,
                IFNULL(stat.ROWS_READ, 0) AS rows_read,
                IFNULL(stat.ROWS_READ, 0) / GREATEST(t.TABLE_ROWS, 1) AS read_ratio
            FROM
                information_schema.STATISTICS s
            JOIN
                information_schema.TABLES t ON s.TABLE_SCHEMA = t.TABLE_SCHEMA AND s.TABLE_NAME = t.TABLE_NAME
            LEFT JOIN
                performance_schema.table_io_waits_summary_by_index_usage stat
                ON stat.OBJECT_SCHEMA = s.TABLE_SCHEMA
                AND stat.OBJECT_NAME = s.TABLE_NAME
                AND stat.INDEX_NAME = s.INDEX_NAME
            {$whereClause}
            ORDER BY
                t.TABLE_SCHEMA, t.TABLE_NAME, s.INDEX_NAME, s.SEQ_IN_INDEX
        SQL;

        try {
            return $this->connection->query($sql, $params)->fetchAll();
        } catch (\Throwable $e) {
            // Fallback if performance_schema is not available
            $sql = <<<SQL
                SELECT
                    TABLE_SCHEMA AS table_schema,
                    TABLE_NAME AS table_name,
                    INDEX_NAME AS index_name,
                    COLUMN_NAME AS column_name,
                    SEQ_IN_INDEX AS sequence,
                    CARDINALITY AS cardinality,
                    NULL AS rows_read,
                    NULL AS read_ratio
                FROM
                    information_schema.STATISTICS
                {$whereClause}
                ORDER BY
                    TABLE_SCHEMA, TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX
            SQL;

            return $this->connection->query($sql, $params)->fetchAll();
        }
    }

    /**
     * Get MySQL unused indexes
     * @return array<array<string, mixed>>
     */
    private function getMySqlUnusedIndexes(?string $table = null, int $minDaysSinceCreation = 30): array
    {
        $whereClause = 'WHERE stat.INDEX_NAME IS NOT NULL AND stat.INDEX_NAME != "PRIMARY"';
        $params = [];

        if ($table !== null) {
            $whereClause .= ' AND stat.OBJECT_NAME = ?';
            $params[] = $table;
        }

        $sql = <<<SQL
            SELECT
                stat.OBJECT_SCHEMA AS table_schema,
                stat.OBJECT_NAME AS table_name,
                stat.INDEX_NAME AS index_name,
                stat.COUNT_STAR AS total_operations,
                stat.COUNT_READ AS read_operations,
                stat.COUNT_WRITE AS write_operations,
                stat.COUNT_FETCH AS fetch_operations
            FROM
                performance_schema.table_io_waits_summary_by_index_usage stat
            {$whereClause}
            AND stat.COUNT_STAR = 0
            ORDER BY
                stat.OBJECT_SCHEMA, stat.OBJECT_NAME, stat.INDEX_NAME
        SQL;

        try {
            return $this->connection->query($sql, $params)->fetchAll();
        } catch (\Throwable $e) {
            // Fallback if performance_schema is not available
            return [
                [
                    'error' => 'Performance schema not available. Enable it to detect unused indexes.',
                    'message' => $e->getMessage(),
                ],
            ];
        }
    }

    /**
     * Get MySQL duplicate indexes
     * @return array<array<string, mixed>>
     */
    private function getMySqlDuplicateIndexes(?string $table = null): array
    {
        $whereClause = '';
        $params = [];

        if ($table !== null) {
            $whereClause = 'WHERE s1.TABLE_NAME = ?';
            $params[] = $table;
        }

        $sql = <<<SQL
            SELECT
                s1.TABLE_SCHEMA AS table_schema,
                s1.TABLE_NAME AS table_name,
                s1.INDEX_NAME AS index_name,
                s2.INDEX_NAME AS duplicate_index_name,
                GROUP_CONCAT(s1.COLUMN_NAME ORDER BY s1.SEQ_IN_INDEX) AS columns
            FROM
                information_schema.STATISTICS s1
            JOIN
                information_schema.STATISTICS s2
                ON s1.TABLE_SCHEMA = s2.TABLE_SCHEMA
                AND s1.TABLE_NAME = s2.TABLE_NAME
                AND s1.COLUMN_NAME = s2.COLUMN_NAME
                AND s1.SEQ_IN_INDEX = s2.SEQ_IN_INDEX
                AND s1.INDEX_NAME < s2.INDEX_NAME
            {$whereClause}
            GROUP BY
                s1.TABLE_SCHEMA, s1.TABLE_NAME, s1.INDEX_NAME, s2.INDEX_NAME
            HAVING
                COUNT(*) = (
                    SELECT COUNT(*)
                    FROM information_schema.STATISTICS
                    WHERE TABLE_SCHEMA = s1.TABLE_SCHEMA
                    AND TABLE_NAME = s1.TABLE_NAME
                    AND INDEX_NAME = s1.INDEX_NAME
                )
                AND COUNT(*) = (
                    SELECT COUNT(*)
                    FROM information_schema.STATISTICS
                    WHERE TABLE_SCHEMA = s2.TABLE_SCHEMA
                    AND TABLE_NAME = s2.TABLE_NAME
                    AND INDEX_NAME = s2.INDEX_NAME
                )
            ORDER BY
                s1.TABLE_SCHEMA, s1.TABLE_NAME, s1.INDEX_NAME
        SQL;

        return $this->connection->query($sql, $params)->fetchAll();
    }

    /**
     * Get MySQL oversized indexes
     * @return array<array<string, mixed>>
     */
    private function getMySqlOversizedIndexes(?string $table = null, int $sizeThresholdMb = 100): array
    {
        $whereClause = 'WHERE t.TABLE_SCHEMA NOT IN ("mysql", "information_schema", "performance_schema", "sys")';
        $params = [];

        if ($table !== null) {
            $whereClause .= ' AND t.TABLE_NAME = ?';
            $params[] = $table;
        }

        $sizeThresholdBytes = $sizeThresholdMb * 1024 * 1024;

        $sql = <<<SQL
            SELECT
                t.TABLE_SCHEMA AS table_schema,
                t.TABLE_NAME AS table_name,
                t.ENGINE AS engine,
                t.TABLE_ROWS AS table_rows,
                t.AVG_ROW_LENGTH AS avg_row_length,
                t.DATA_LENGTH AS data_length,
                t.INDEX_LENGTH AS index_length,
                ROUND(t.INDEX_LENGTH / (1024 * 1024), 2) AS index_size_mb,
                ROUND(t.DATA_LENGTH / (1024 * 1024), 2) AS data_size_mb,
                ROUND(t.INDEX_LENGTH / GREATEST(t.DATA_LENGTH, 1), 2) AS index_to_data_ratio
            FROM
                information_schema.TABLES t
            {$whereClause}
            AND t.INDEX_LENGTH > {$sizeThresholdBytes}
            ORDER BY
                t.INDEX_LENGTH DESC
        SQL;

        return $this->connection->query($sql, $params)->fetchAll();
    }

    /**
     * Get MySQL fragmented indexes
     * @return array<array<string, mixed>>
     */
    private function getMySqlFragmentedIndexes(?string $table = null, float $fragmentationThreshold = 0.3): array
    {
        $whereClause = 'WHERE t.TABLE_SCHEMA NOT IN ("mysql", "information_schema", "performance_schema", "sys")';
        $params = [];

        if ($table !== null) {
            $whereClause .= ' AND t.TABLE_NAME = ?';
            $params[] = $table;
        }

        // MySQL doesn't provide direct fragmentation metrics, so we use data_free as an approximation
        $sql = <<<SQL
            SELECT
                t.TABLE_SCHEMA AS table_schema,
                t.TABLE_NAME AS table_name,
                t.ENGINE AS engine,
                t.TABLE_ROWS AS table_rows,
                t.DATA_LENGTH AS data_length,
                t.INDEX_LENGTH AS index_length,
                t.DATA_FREE AS data_free,
                ROUND(t.DATA_FREE / GREATEST(t.DATA_LENGTH + t.INDEX_LENGTH, 1), 2) AS fragmentation_ratio
            FROM
                information_schema.TABLES t
            {$whereClause}
            AND t.DATA_FREE > 0
            AND t.DATA_FREE / GREATEST(t.DATA_LENGTH + t.INDEX_LENGTH, 1) > {$fragmentationThreshold}
            ORDER BY
                fragmentation_ratio DESC
        SQL;

        return $this->connection->query($sql, $params)->fetchAll();
    }

    /**
     * Get PostgreSQL index usage statistics
     * @return array<array<string, mixed>>
     */
    private function getPostgreSqlIndexUsageStatistics(?string $table = null): array
    {
        $whereClause = '';
        $params = [];

        if ($table !== null) {
            $whereClause = 'WHERE t.tablename = $1';
            $params[] = $table;
        }

        $sql = <<<SQL
            SELECT
                schemaname AS table_schema,
                t.tablename AS table_name,
                indexrelname AS index_name,
                idx_scan AS index_scans,
                idx_tup_read AS tuples_read,
                idx_tup_fetch AS tuples_fetched,
                pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,
                pg_relation_size(i.indexrelid) AS index_bytes
            FROM
                pg_stat_user_indexes i
            JOIN
                pg_stat_user_tables t ON i.relid = t.relid
            {$whereClause}
            ORDER BY
                schemaname, t.tablename, indexrelname
        SQL;

        return $this->connection->query($sql, $params)->fetchAll();
    }

    /**
     * Get PostgreSQL unused indexes
     * @return array<array<string, mixed>>
     */
    private function getPostgreSqlUnusedIndexes(?string $table = null, int $minDaysSinceCreation = 30): array
    {
        $whereClause = 'WHERE idx_scan = 0 AND NOT indisprimary';
        $params = [];

        if ($table !== null) {
            $whereClause .= ' AND t.tablename = $1';
            $params[] = $table;
        }

        $sql = <<<SQL
            SELECT
                schemaname AS table_schema,
                t.tablename AS table_name,
                indexrelname AS index_name,
                idx_scan AS index_scans,
                pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,
                pg_relation_size(i.indexrelid) AS index_bytes,
                indexdef AS index_definition
            FROM
                pg_stat_user_indexes i
            JOIN
                pg_stat_user_tables t ON i.relid = t.relid
            JOIN
                pg_indexes idx ON i.schemaname = idx.schemaname AND i.indexrelname = idx.indexname
            JOIN
                pg_index pi ON i.indexrelid = pi.indexrelid
            {$whereClause}
            ORDER BY
                index_bytes DESC, schemaname, t.tablename, indexrelname
        SQL;

        return $this->connection->query($sql, $params)->fetchAll();
    }

    /**
     * Get PostgreSQL duplicate indexes
     * @return array<array<string, mixed>>
     */
    private function getPostgreSqlDuplicateIndexes(?string $table = null): array
    {
        $whereClause = '';
        $params = [];

        if ($table !== null) {
            $whereClause = 'WHERE t.tablename = $1';
            $params[] = $table;
        }

        $sql = <<<SQL
            WITH index_cols AS (
                SELECT
                    i.schemaname,
                    i.tablename,
                    i.indexname,
                    array_agg(a.attname ORDER BY c.ordinality) AS columns,
                    pi.indisprimary,
                    pi.indisunique
                FROM
                    pg_indexes i
                JOIN
                    pg_class ic ON i.indexname = ic.relname
                JOIN
                    pg_index pi ON ic.oid = pi.indexrelid
                JOIN
                    pg_class tc ON pi.indrelid = tc.oid AND i.tablename = tc.relname
                JOIN
                    pg_namespace n ON tc.relnamespace = n.oid AND i.schemaname = n.nspname
                JOIN LATERAL
                    unnest(pi.indkey) WITH ORDINALITY AS c(attnum, ordinality) ON true
                JOIN
                    pg_attribute a ON a.attrelid = tc.oid AND a.attnum = c.attnum
                GROUP BY
                    i.schemaname, i.tablename, i.indexname, pi.indisprimary, pi.indisunique
            )
            SELECT
                i1.schemaname AS table_schema,
                i1.tablename AS table_name,
                i1.indexname AS index_name,
                i2.indexname AS duplicate_index_name,
                i1.columns::text AS columns,
                i1.indisprimary AS is_primary,
                i1.indisunique AS is_unique,
                i2.indisprimary AS dup_is_primary,
                i2.indisunique AS dup_is_unique
            FROM
                index_cols i1
            JOIN
                index_cols i2 ON i1.schemaname = i2.schemaname
                AND i1.tablename = i2.tablename
                AND i1.columns = i2.columns
                AND i1.indexname < i2.indexname
            {$whereClause}
            ORDER BY
                i1.schemaname, i1.tablename, i1.indexname
        SQL;

        return $this->connection->query($sql, $params)->fetchAll();
    }

    /**
     * Get PostgreSQL oversized indexes
     * @return array<array<string, mixed>>
     */
    private function getPostgreSqlOversizedIndexes(?string $table = null, int $sizeThresholdMb = 100): array
    {
        $whereClause = '';
        $params = [];

        if ($table !== null) {
            $whereClause = 'WHERE t.tablename = $1';
            $params[] = $table;
        }

        $sizeThresholdBytes = $sizeThresholdMb * 1024 * 1024;

        $sql = <<<SQL
            SELECT
                schemaname AS table_schema,
                t.tablename AS table_name,
                indexrelname AS index_name,
                idx_scan AS index_scans,
                pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,
                pg_relation_size(i.indexrelid) AS index_bytes,
                pg_size_pretty(pg_relation_size(i.relid)) AS table_size,
                pg_relation_size(i.relid) AS table_bytes,
                ROUND(pg_relation_size(i.indexrelid)::numeric / GREATEST(pg_relation_size(i.relid), 1), 2) AS index_to_table_ratio
            FROM
                pg_stat_user_indexes i
            JOIN
                pg_stat_user_tables t ON i.relid = t.relid
            {$whereClause}
            AND pg_relation_size(i.indexrelid) > {$sizeThresholdBytes}
            ORDER BY
                index_bytes DESC, schemaname, t.tablename, indexrelname
        SQL;

        return $this->connection->query($sql, $params)->fetchAll();
    }

    /**
     * Get PostgreSQL fragmented indexes
     * @return array<array<string, mixed>>
     */
    private function getPostgreSqlFragmentedIndexes(?string $table = null, float $fragmentationThreshold = 0.3): array
    {
        $whereClause = '';
        $params = [];

        if ($table !== null) {
            $whereClause = 'WHERE t.tablename = $1';
            $params[] = $table;
        }

        $sql = <<<SQL
            SELECT
                schemaname AS table_schema,
                t.tablename AS table_name,
                indexrelname AS index_name,
                pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,
                pg_relation_size(i.indexrelid) AS index_bytes,
                ROUND(n_dead_tup::numeric / GREATEST(n_live_tup + n_dead_tup, 1), 2) AS fragmentation_ratio
            FROM
                pg_stat_user_indexes i
            JOIN
                pg_stat_user_tables t ON i.relid = t.relid
            {$whereClause}
            AND n_dead_tup > 0
            AND n_dead_tup::numeric / GREATEST(n_live_tup + n_dead_tup, 1) > {$fragmentationThreshold}
            ORDER BY
                fragmentation_ratio DESC, schemaname, t.tablename, indexrelname
        SQL;

        return $this->connection->query($sql, $params)->fetchAll();
    }

    /**
     * Get the database driver name
     */
    private function getDriverName(): string
    {
        return $this->connection->getPdo()->getAttribute(\PDO::ATTR_DRIVER_NAME);
    }
}
