<?php

declare(strict_types=1);

namespace App\Framework\Database\Schema\Comparison;

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

/**
 * Compares database schemas and identifies differences
 */
final readonly class SchemaComparator
{
    /**
     * Create a new schema comparator
     */
    public function __construct(
        private ConnectionInterface $sourceConnection,
        private ConnectionInterface $targetConnection
    ) {
    }

    /**
     * Compare the schemas of the source and target databases
     *
     * @param string|null $sourceSchema The schema name in the source database (null for default)
     * @param string|null $targetSchema The schema name in the target database (null for default)
     * @return SchemaDifference The differences between the schemas
     */
    public function compare(?string $sourceSchema = null, ?string $targetSchema = null): SchemaDifference
    {
        $sourceDriver = $this->getDriverName($this->sourceConnection);
        $targetDriver = $this->getDriverName($this->targetConnection);

        if ($sourceDriver !== $targetDriver) {
            throw new DatabaseException(
                "Cannot compare schemas between different database drivers: {$sourceDriver} and {$targetDriver}"
            );
        }

        return match($sourceDriver) {
            'mysql' => $this->compareMySql($sourceSchema, $targetSchema),
            'pgsql' => $this->comparePostgreSql($sourceSchema, $targetSchema),
            'sqlite' => $this->compareSqlite(),
            default => throw new DatabaseException("Schema comparison not supported for {$sourceDriver}")
        };
    }

    /**
     * Compare MySQL schemas
     */
    private function compareMySql(?string $sourceSchema = null, ?string $targetSchema = null): SchemaDifference
    {
        // Use the connection's database if schema is not specified
        $sourceSchema = $sourceSchema ?? $this->getDefaultSchema($this->sourceConnection);
        $targetSchema = $targetSchema ?? $this->getDefaultSchema($this->targetConnection);

        // Compare tables
        $sourceTables = $this->getMySqlTables($this->sourceConnection, $sourceSchema);
        $targetTables = $this->getMySqlTables($this->targetConnection, $targetSchema);

        $missingTables = array_diff($sourceTables, $targetTables);
        $extraTables = array_diff($targetTables, $sourceTables);
        $commonTables = array_intersect($sourceTables, $targetTables);

        // Compare table structures for common tables
        $tableDifferences = [];
        foreach ($commonTables as $table) {
            $sourceColumns = $this->getMySqlColumns($this->sourceConnection, $sourceSchema, $table);
            $targetColumns = $this->getMySqlColumns($this->targetConnection, $targetSchema, $table);

            $missingColumns = array_diff_key($sourceColumns, $targetColumns);
            $extraColumns = array_diff_key($targetColumns, $sourceColumns);

            // Check for column differences
            $modifiedColumns = [];
            foreach (array_intersect_key($sourceColumns, $targetColumns) as $columnName => $sourceColumn) {
                $targetColumn = $targetColumns[$columnName];

                if ($sourceColumn !== $targetColumn) {
                    $modifiedColumns[$columnName] = [
                        'source' => $sourceColumn,
                        'target' => $targetColumn,
                    ];
                }
            }

            // Compare indexes
            $sourceIndexes = $this->getMySqlIndexes($this->sourceConnection, $sourceSchema, $table);
            $targetIndexes = $this->getMySqlIndexes($this->targetConnection, $targetSchema, $table);

            $missingIndexes = array_diff_key($sourceIndexes, $targetIndexes);
            $extraIndexes = array_diff_key($targetIndexes, $sourceIndexes);

            // Check for index differences
            $modifiedIndexes = [];
            foreach (array_intersect_key($sourceIndexes, $targetIndexes) as $indexName => $sourceIndex) {
                $targetIndex = $targetIndexes[$indexName];

                if ($sourceIndex !== $targetIndex) {
                    $modifiedIndexes[$indexName] = [
                        'source' => $sourceIndex,
                        'target' => $targetIndex,
                    ];
                }
            }

            // Compare foreign keys
            $sourceForeignKeys = $this->getMySqlForeignKeys($this->sourceConnection, $sourceSchema, $table);
            $targetForeignKeys = $this->getMySqlForeignKeys($this->targetConnection, $targetSchema, $table);

            $missingForeignKeys = array_diff_key($sourceForeignKeys, $targetForeignKeys);
            $extraForeignKeys = array_diff_key($targetForeignKeys, $sourceForeignKeys);

            // Check for foreign key differences
            $modifiedForeignKeys = [];
            foreach (array_intersect_key($sourceForeignKeys, $targetForeignKeys) as $fkName => $sourceFk) {
                $targetFk = $targetForeignKeys[$fkName];

                if ($sourceFk !== $targetFk) {
                    $modifiedForeignKeys[$fkName] = [
                        'source' => $sourceFk,
                        'target' => $targetFk,
                    ];
                }
            }

            // Add table differences if any
            if (! empty($missingColumns) || ! empty($extraColumns) || ! empty($modifiedColumns) ||
                ! empty($missingIndexes) || ! empty($extraIndexes) || ! empty($modifiedIndexes) ||
                ! empty($missingForeignKeys) || ! empty($extraForeignKeys) || ! empty($modifiedForeignKeys)) {

                $tableDifferences[$table] = new TableDifference(
                    $table,
                    $missingColumns,
                    $extraColumns,
                    $modifiedColumns,
                    $missingIndexes,
                    $extraIndexes,
                    $modifiedIndexes,
                    $missingForeignKeys,
                    $extraForeignKeys,
                    $modifiedForeignKeys
                );
            }
        }

        return new SchemaDifference(
            $sourceSchema,
            $targetSchema,
            $missingTables,
            $extraTables,
            $tableDifferences
        );
    }

    /**
     * Compare PostgreSQL schemas
     */
    private function comparePostgreSql(?string $sourceSchema = null, ?string $targetSchema = null): SchemaDifference
    {
        // Use 'public' as the default schema if not specified
        $sourceSchema = $sourceSchema ?? 'public';
        $targetSchema = $targetSchema ?? 'public';

        // Compare tables
        $sourceTables = $this->getPostgreSqlTables($this->sourceConnection, $sourceSchema);
        $targetTables = $this->getPostgreSqlTables($this->targetConnection, $targetSchema);

        $missingTables = array_diff($sourceTables, $targetTables);
        $extraTables = array_diff($targetTables, $sourceTables);
        $commonTables = array_intersect($sourceTables, $targetTables);

        // Compare table structures for common tables
        $tableDifferences = [];
        foreach ($commonTables as $table) {
            $sourceColumns = $this->getPostgreSqlColumns($this->sourceConnection, $sourceSchema, $table);
            $targetColumns = $this->getPostgreSqlColumns($this->targetConnection, $targetSchema, $table);

            $missingColumns = array_diff_key($sourceColumns, $targetColumns);
            $extraColumns = array_diff_key($targetColumns, $sourceColumns);

            // Check for column differences
            $modifiedColumns = [];
            foreach (array_intersect_key($sourceColumns, $targetColumns) as $columnName => $sourceColumn) {
                $targetColumn = $targetColumns[$columnName];

                if ($sourceColumn !== $targetColumn) {
                    $modifiedColumns[$columnName] = [
                        'source' => $sourceColumn,
                        'target' => $targetColumn,
                    ];
                }
            }

            // Compare indexes
            $sourceIndexes = $this->getPostgreSqlIndexes($this->sourceConnection, $sourceSchema, $table);
            $targetIndexes = $this->getPostgreSqlIndexes($this->targetConnection, $targetSchema, $table);

            $missingIndexes = array_diff_key($sourceIndexes, $targetIndexes);
            $extraIndexes = array_diff_key($targetIndexes, $sourceIndexes);

            // Check for index differences
            $modifiedIndexes = [];
            foreach (array_intersect_key($sourceIndexes, $targetIndexes) as $indexName => $sourceIndex) {
                $targetIndex = $targetIndexes[$indexName];

                if ($sourceIndex !== $targetIndex) {
                    $modifiedIndexes[$indexName] = [
                        'source' => $sourceIndex,
                        'target' => $targetIndex,
                    ];
                }
            }

            // Compare foreign keys
            $sourceForeignKeys = $this->getPostgreSqlForeignKeys($this->sourceConnection, $sourceSchema, $table);
            $targetForeignKeys = $this->getPostgreSqlForeignKeys($this->targetConnection, $targetSchema, $table);

            $missingForeignKeys = array_diff_key($sourceForeignKeys, $targetForeignKeys);
            $extraForeignKeys = array_diff_key($targetForeignKeys, $sourceForeignKeys);

            // Check for foreign key differences
            $modifiedForeignKeys = [];
            foreach (array_intersect_key($sourceForeignKeys, $targetForeignKeys) as $fkName => $sourceFk) {
                $targetFk = $targetForeignKeys[$fkName];

                if ($sourceFk !== $targetFk) {
                    $modifiedForeignKeys[$fkName] = [
                        'source' => $sourceFk,
                        'target' => $targetFk,
                    ];
                }
            }

            // Add table differences if any
            if (! empty($missingColumns) || ! empty($extraColumns) || ! empty($modifiedColumns) ||
                ! empty($missingIndexes) || ! empty($extraIndexes) || ! empty($modifiedIndexes) ||
                ! empty($missingForeignKeys) || ! empty($extraForeignKeys) || ! empty($modifiedForeignKeys)) {

                $tableDifferences[$table] = new TableDifference(
                    $table,
                    $missingColumns,
                    $extraColumns,
                    $modifiedColumns,
                    $missingIndexes,
                    $extraIndexes,
                    $modifiedIndexes,
                    $missingForeignKeys,
                    $extraForeignKeys,
                    $modifiedForeignKeys
                );
            }
        }

        return new SchemaDifference(
            $sourceSchema,
            $targetSchema,
            $missingTables,
            $extraTables,
            $tableDifferences
        );
    }

    /**
     * Compare SQLite schemas
     */
    private function compareSqlite(): SchemaDifference
    {
        // SQLite doesn't have schemas, so we compare the entire database

        // Compare tables
        $sourceTables = $this->getSqliteTables($this->sourceConnection);
        $targetTables = $this->getSqliteTables($this->targetConnection);

        $missingTables = array_diff($sourceTables, $targetTables);
        $extraTables = array_diff($targetTables, $sourceTables);
        $commonTables = array_intersect($sourceTables, $targetTables);

        // Compare table structures for common tables
        $tableDifferences = [];
        foreach ($commonTables as $table) {
            $sourceColumns = $this->getSqliteColumns($this->sourceConnection, $table);
            $targetColumns = $this->getSqliteColumns($this->targetConnection, $table);

            $missingColumns = array_diff_key($sourceColumns, $targetColumns);
            $extraColumns = array_diff_key($targetColumns, $sourceColumns);

            // Check for column differences
            $modifiedColumns = [];
            foreach (array_intersect_key($sourceColumns, $targetColumns) as $columnName => $sourceColumn) {
                $targetColumn = $targetColumns[$columnName];

                if ($sourceColumn !== $targetColumn) {
                    $modifiedColumns[$columnName] = [
                        'source' => $sourceColumn,
                        'target' => $targetColumn,
                    ];
                }
            }

            // Compare indexes
            $sourceIndexes = $this->getSqliteIndexes($this->sourceConnection, $table);
            $targetIndexes = $this->getSqliteIndexes($this->targetConnection, $table);

            $missingIndexes = array_diff_key($sourceIndexes, $targetIndexes);
            $extraIndexes = array_diff_key($targetIndexes, $sourceIndexes);

            // Check for index differences
            $modifiedIndexes = [];
            foreach (array_intersect_key($sourceIndexes, $targetIndexes) as $indexName => $sourceIndex) {
                $targetIndex = $targetIndexes[$indexName];

                if ($sourceIndex !== $targetIndex) {
                    $modifiedIndexes[$indexName] = [
                        'source' => $sourceIndex,
                        'target' => $targetIndex,
                    ];
                }
            }

            // Compare foreign keys
            $sourceForeignKeys = $this->getSqliteForeignKeys($this->sourceConnection, $table);
            $targetForeignKeys = $this->getSqliteForeignKeys($this->targetConnection, $table);

            $missingForeignKeys = array_diff_key($sourceForeignKeys, $targetForeignKeys);
            $extraForeignKeys = array_diff_key($targetForeignKeys, $sourceForeignKeys);

            // Check for foreign key differences
            $modifiedForeignKeys = [];
            foreach (array_intersect_key($sourceForeignKeys, $targetForeignKeys) as $fkName => $sourceFk) {
                $targetFk = $targetForeignKeys[$fkName];

                if ($sourceFk !== $targetFk) {
                    $modifiedForeignKeys[$fkName] = [
                        'source' => $sourceFk,
                        'target' => $targetFk,
                    ];
                }
            }

            // Add table differences if any
            if (! empty($missingColumns) || ! empty($extraColumns) || ! empty($modifiedColumns) ||
                ! empty($missingIndexes) || ! empty($extraIndexes) || ! empty($modifiedIndexes) ||
                ! empty($missingForeignKeys) || ! empty($extraForeignKeys) || ! empty($modifiedForeignKeys)) {

                $tableDifferences[$table] = new TableDifference(
                    $table,
                    $missingColumns,
                    $extraColumns,
                    $modifiedColumns,
                    $missingIndexes,
                    $extraIndexes,
                    $modifiedIndexes,
                    $missingForeignKeys,
                    $extraForeignKeys,
                    $modifiedForeignKeys
                );
            }
        }

        return new SchemaDifference(
            'main',
            'main',
            $missingTables,
            $extraTables,
            $tableDifferences
        );
    }

    /**
     * Get the default schema for a connection
     */
    private function getDefaultSchema(ConnectionInterface $connection): string
    {
        $driver = $this->getDriverName($connection);

        return match($driver) {
            'mysql' => $connection->queryScalar('SELECT DATABASE()'),
            'pgsql' => 'public',
            'sqlite' => 'main',
            default => throw new DatabaseException("Unsupported driver: {$driver}")
        };
    }

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

    /**
     * Get tables in a MySQL schema
     *
     * @return array<string> List of table names
     */
    private function getMySqlTables(ConnectionInterface $connection, string $schema): array
    {
        $sql = "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE'";

        return $connection->queryColumn($sql, [$schema]);
    }

    /**
     * Get columns in a MySQL table
     *
     * @return array<string, array<string, mixed>> Map of column names to column definitions
     */
    private function getMySqlColumns(ConnectionInterface $connection, string $schema, string $table): array
    {
        $sql = "SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA, CHARACTER_SET_NAME, COLLATION_NAME
                FROM information_schema.COLUMNS
                WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
                ORDER BY ORDINAL_POSITION";

        $columns = [];
        $result = $connection->query($sql, [$schema, $table]);

        foreach ($result->fetchAll() as $row) {
            $columns[$row['COLUMN_NAME']] = [
                'type' => $row['COLUMN_TYPE'],
                'nullable' => $row['IS_NULLABLE'] === 'YES',
                'default' => $row['COLUMN_DEFAULT'],
                'extra' => $row['EXTRA'],
                'character_set' => $row['CHARACTER_SET_NAME'],
                'collation' => $row['COLLATION_NAME'],
            ];
        }

        return $columns;
    }

    /**
     * Get indexes in a MySQL table
     *
     * @return array<string, array<string, mixed>> Map of index names to index definitions
     */
    private function getMySqlIndexes(ConnectionInterface $connection, string $schema, string $table): array
    {
        $sql = "SELECT INDEX_NAME, NON_UNIQUE, COLUMN_NAME, SEQ_IN_INDEX, INDEX_TYPE
                FROM information_schema.STATISTICS
                WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
                ORDER BY INDEX_NAME, SEQ_IN_INDEX";

        $result = $connection->query($sql, [$schema, $table]);
        $indexes = [];

        foreach ($result->fetchAll() as $row) {
            $indexName = $row['INDEX_NAME'];

            if (! isset($indexes[$indexName])) {
                $indexes[$indexName] = [
                    'unique' => ! $row['NON_UNIQUE'],
                    'type' => $row['INDEX_TYPE'],
                    'columns' => [],
                ];
            }

            $indexes[$indexName]['columns'][$row['SEQ_IN_INDEX']] = $row['COLUMN_NAME'];
        }

        // Sort columns by sequence
        foreach ($indexes as &$index) {
            ksort($index['columns']);
            $index['columns'] = array_values($index['columns']);
        }

        return $indexes;
    }

    /**
     * Get foreign keys in a MySQL table
     *
     * @return array<string, array<string, mixed>> Map of foreign key names to foreign key definitions
     */
    private function getMySqlForeignKeys(ConnectionInterface $connection, string $schema, string $table): array
    {
        $sql = "SELECT CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME,
                       UPDATE_RULE, DELETE_RULE
                FROM information_schema.KEY_COLUMN_USAGE k
                JOIN information_schema.REFERENTIAL_CONSTRAINTS r
                    ON k.CONSTRAINT_NAME = r.CONSTRAINT_NAME
                    AND k.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA
                WHERE k.TABLE_SCHEMA = ? AND k.TABLE_NAME = ?
                    AND k.REFERENCED_TABLE_NAME IS NOT NULL
                ORDER BY CONSTRAINT_NAME, ORDINAL_POSITION";

        $result = $connection->query($sql, [$schema, $table]);
        $foreignKeys = [];

        foreach ($result->fetchAll() as $row) {
            $constraintName = $row['CONSTRAINT_NAME'];

            if (! isset($foreignKeys[$constraintName])) {
                $foreignKeys[$constraintName] = [
                    'referenced_table' => $row['REFERENCED_TABLE_NAME'],
                    'update_rule' => $row['UPDATE_RULE'],
                    'delete_rule' => $row['DELETE_RULE'],
                    'columns' => [],
                    'referenced_columns' => [],
                ];
            }

            $foreignKeys[$constraintName]['columns'][] = $row['COLUMN_NAME'];
            $foreignKeys[$constraintName]['referenced_columns'][] = $row['REFERENCED_COLUMN_NAME'];
        }

        return $foreignKeys;
    }

    /**
     * Get tables in a PostgreSQL schema
     *
     * @return array<string> List of table names
     */
    private function getPostgreSqlTables(ConnectionInterface $connection, string $schema): array
    {
        $sql = "SELECT tablename FROM pg_tables WHERE schemaname = ?";

        return $connection->queryColumn($sql, [$schema]);
    }

    /**
     * Get columns in a PostgreSQL table
     *
     * @return array<string, array<string, mixed>> Map of column names to column definitions
     */
    private function getPostgreSqlColumns(ConnectionInterface $connection, string $schema, string $table): array
    {
        $sql = "SELECT a.attname as column_name,
                       pg_catalog.format_type(a.atttypid, a.atttypmod) as data_type,
                       a.attnotnull as not_null,
                       pg_get_expr(d.adbin, d.adrelid) as default_value,
                       col_description(a.attrelid, a.attnum) as comment
                FROM pg_catalog.pg_attribute a
                LEFT JOIN pg_catalog.pg_attrdef d ON (a.attrelid, a.attnum) = (d.adrelid, d.adnum)
                JOIN pg_catalog.pg_class c ON a.attrelid = c.oid
                JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid
                WHERE c.relname = ?
                  AND n.nspname = ?
                  AND a.attnum > 0
                  AND NOT a.attisdropped
                ORDER BY a.attnum";

        $columns = [];
        $result = $connection->query($sql, [$table, $schema]);

        foreach ($result->fetchAll() as $row) {
            $columns[$row['column_name']] = [
                'type' => $row['data_type'],
                'nullable' => ! $row['not_null'],
                'default' => $row['default_value'],
                'comment' => $row['comment'],
            ];
        }

        return $columns;
    }

    /**
     * Get indexes in a PostgreSQL table
     *
     * @return array<string, array<string, mixed>> Map of index names to index definitions
     */
    private function getPostgreSqlIndexes(ConnectionInterface $connection, string $schema, string $table): array
    {
        $sql = "SELECT i.relname as index_name,
                       a.attname as column_name,
                       ix.indisunique as is_unique,
                       ix.indisprimary as is_primary,
                       am.amname as index_type,
                       a.attnum as column_position
                FROM pg_index ix
                JOIN pg_class i ON i.oid = ix.indexrelid
                JOIN pg_class t ON t.oid = ix.indrelid
                JOIN pg_namespace n ON n.oid = t.relnamespace
                JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
                JOIN pg_am am ON i.relam = am.oid
                WHERE t.relname = ?
                  AND n.nspname = ?
                ORDER BY i.relname, a.attnum";

        $result = $connection->query($sql, [$table, $schema]);
        $indexes = [];

        foreach ($result->fetchAll() as $row) {
            $indexName = $row['index_name'];

            if (! isset($indexes[$indexName])) {
                $indexes[$indexName] = [
                    'unique' => (bool)$row['is_unique'],
                    'primary' => (bool)$row['is_primary'],
                    'type' => $row['index_type'],
                    'columns' => [],
                ];
            }

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

        // Sort columns by position
        foreach ($indexes as &$index) {
            ksort($index['columns']);
            $index['columns'] = array_values($index['columns']);
        }

        return $indexes;
    }

    /**
     * Get foreign keys in a PostgreSQL table
     *
     * @return array<string, array<string, mixed>> Map of foreign key names to foreign key definitions
     */
    private function getPostgreSqlForeignKeys(ConnectionInterface $connection, string $schema, string $table): array
    {
        $sql = "SELECT con.conname as constraint_name,
                       con.confupdtype as update_rule,
                       con.confdeltype as delete_rule,
                       f.relname as referenced_table,
                       a.attname as column_name,
                       af.attname as referenced_column,
                       a.attnum as column_position
                FROM pg_constraint con
                JOIN pg_class t ON t.oid = con.conrelid
                JOIN pg_class f ON f.oid = con.confrelid
                JOIN pg_namespace n ON n.oid = t.relnamespace
                JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(con.conkey)
                JOIN pg_attribute af ON af.attrelid = f.oid AND af.attnum = ANY(con.confkey)
                WHERE con.contype = 'f'
                  AND t.relname = ?
                  AND n.nspname = ?
                ORDER BY con.conname, a.attnum";

        $result = $connection->query($sql, [$table, $schema]);
        $foreignKeys = [];

        foreach ($result->fetchAll() as $row) {
            $constraintName = $row['constraint_name'];

            if (! isset($foreignKeys[$constraintName])) {
                $foreignKeys[$constraintName] = [
                    'referenced_table' => $row['referenced_table'],
                    'update_rule' => $this->convertPostgreSqlRule($row['update_rule']),
                    'delete_rule' => $this->convertPostgreSqlRule($row['delete_rule']),
                    'columns' => [],
                    'referenced_columns' => [],
                ];
            }

            $foreignKeys[$constraintName]['columns'][$row['column_position']] = $row['column_name'];
            $foreignKeys[$constraintName]['referenced_columns'][$row['column_position']] = $row['referenced_column'];
        }

        // Sort columns by position
        foreach ($foreignKeys as &$fk) {
            ksort($fk['columns']);
            ksort($fk['referenced_columns']);
            $fk['columns'] = array_values($fk['columns']);
            $fk['referenced_columns'] = array_values($fk['referenced_columns']);
        }

        return $foreignKeys;
    }

    /**
     * Convert PostgreSQL rule character to rule name
     */
    private function convertPostgreSqlRule(string $rule): string
    {
        return match($rule) {
            'a' => 'NO ACTION',
            'r' => 'RESTRICT',
            'c' => 'CASCADE',
            'n' => 'SET NULL',
            'd' => 'SET DEFAULT',
            default => $rule
        };
    }

    /**
     * Get tables in a SQLite database
     *
     * @return array<string> List of table names
     */
    private function getSqliteTables(ConnectionInterface $connection): array
    {
        $sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'";

        return $connection->queryColumn($sql);
    }

    /**
     * Get columns in a SQLite table
     *
     * @return array<string, array<string, mixed>> Map of column names to column definitions
     */
    private function getSqliteColumns(ConnectionInterface $connection, string $table): array
    {
        $sql = "PRAGMA table_info({$table})";
        $result = $connection->query($sql);
        $columns = [];

        foreach ($result->fetchAll() as $row) {
            $columns[$row['name']] = [
                'type' => $row['type'],
                'nullable' => ! $row['notnull'],
                'default' => $row['dflt_value'],
                'primary' => (bool)$row['pk'],
            ];
        }

        return $columns;
    }

    /**
     * Get indexes in a SQLite table
     *
     * @return array<string, array<string, mixed>> Map of index names to index definitions
     */
    private function getSqliteIndexes(ConnectionInterface $connection, string $table): array
    {
        $sql = "PRAGMA index_list({$table})";
        $result = $connection->query($sql);
        $indexes = [];

        foreach ($result->fetchAll() as $row) {
            $indexName = $row['name'];
            $indexInfo = $connection->query("PRAGMA index_info({$indexName})")->fetchAll();

            $columns = [];
            foreach ($indexInfo as $column) {
                $columns[$column['seqno']] = $column['name'];
            }

            ksort($columns);

            $indexes[$indexName] = [
                'unique' => (bool)$row['unique'],
                'columns' => array_values($columns),
            ];
        }

        return $indexes;
    }

    /**
     * Get foreign keys in a SQLite table
     *
     * @return array<string, array<string, mixed>> Map of foreign key names to foreign key definitions
     */
    private function getSqliteForeignKeys(ConnectionInterface $connection, string $table): array
    {
        $sql = "PRAGMA foreign_key_list({$table})";
        $result = $connection->query($sql);
        $foreignKeys = [];

        foreach ($result->fetchAll() as $row) {
            $id = $row['id'];

            if (! isset($foreignKeys[$id])) {
                $foreignKeys[$id] = [
                    'referenced_table' => $row['table'],
                    'update_rule' => $row['on_update'],
                    'delete_rule' => $row['on_delete'],
                    'columns' => [],
                    'referenced_columns' => [],
                ];
            }

            $foreignKeys[$id]['columns'][$row['seq']] = $row['from'];
            $foreignKeys[$id]['referenced_columns'][$row['seq']] = $row['to'];
        }

        // Sort columns by sequence
        foreach ($foreignKeys as &$fk) {
            ksort($fk['columns']);
            ksort($fk['referenced_columns']);
            $fk['columns'] = array_values($fk['columns']);
            $fk['referenced_columns'] = array_values($fk['referenced_columns']);
        }

        return $foreignKeys;
    }
}
