<?php

declare(strict_types=1);

namespace App\Framework\Database\Schema\Comparison;

use App\Framework\Database\Migration\MigrationVersion;

/**
 * Generates migration code from schema differences
 */
final readonly class MigrationGenerator
{
    /**
     * Generate migration code from schema differences
     *
     * @param SchemaDifference $difference The schema differences
     * @param string $className The name of the migration class
     * @param string|null $description The description of the migration
     * @param MigrationVersion|null $version The version of the migration (generated if null)
     * @return string The generated migration code
     */
    public function generateMigration(
        SchemaDifference $difference,
        string $className,
        ?string $description = null,
        ?MigrationVersion $version = null
    ): string {
        if (! $difference->hasDifferences()) {
            throw new \InvalidArgumentException('Cannot generate migration from schema with no differences');
        }

        $version = $version ?? MigrationVersion::fromDateTime(new \DateTimeImmutable());
        $description = $description ?? "Schema update from {$difference->sourceSchema} to {$difference->targetSchema}";

        $upCode = $this->generateUpCode($difference);
        $downCode = $this->generateDownCode($difference);

        return $this->generateMigrationClass($className, $description, $version, $upCode, $downCode);
    }

    /**
     * Generate the up method code
     */
    private function generateUpCode(SchemaDifference $difference): string
    {
        $code = [];

        // Handle missing tables (create them)
        foreach ($difference->missingTables as $tableName => $tableDef) {
            $code[] = $this->generateCreateTableCode($tableName, $tableDef);
        }

        // Handle table differences
        foreach ($difference->tableDifferences as $tableDifference) {
            $code[] = $this->generateTableModificationCode($tableDifference);
        }

        // Handle extra tables (drop them in down method)

        return implode("\n\n", $code);
    }

    /**
     * Generate the down method code
     */
    private function generateDownCode(SchemaDifference $difference): string
    {
        $code = [];

        // Handle extra tables (drop them)
        foreach ($difference->extraTables as $tableName => $tableDef) {
            $code[] = "        \$schema->dropIfExists('{$tableName}');";
        }

        // Handle table differences (reverse the changes)
        foreach ($difference->tableDifferences as $tableDifference) {
            $code[] = $this->generateReverseTableModificationCode($tableDifference);
        }

        // Handle missing tables (create them in down method)
        foreach ($difference->missingTables as $tableName => $tableDef) {
            $code[] = $this->generateDropTableCode($tableName);
        }

        return implode("\n\n", $code);
    }

    /**
     * Generate code to create a table
     */
    private function generateCreateTableCode(string $tableName, array $tableDef): string
    {
        $code = [];
        $code[] = "        \$schema->create('{$tableName}', function (\$table) {";

        // Add columns
        foreach ($tableDef['columns'] ?? [] as $columnName => $columnDef) {
            $type = $columnDef['type'] ?? 'string';
            $columnCode = "            \$table->{$this->mapColumnType($type)}('{$columnName}')";

            // Add modifiers
            if (isset($columnDef['length']) && $columnDef['length'] > 0) {
                $columnCode .= "->length({$columnDef['length']})";
            }

            if (isset($columnDef['nullable']) && $columnDef['nullable']) {
                $columnCode .= '->nullable()';
            }

            if (isset($columnDef['default'])) {
                $default = $this->formatDefaultValue($columnDef['default'], $type);
                $columnCode .= "->default({$default})";
            }

            if (isset($columnDef['unsigned']) && $columnDef['unsigned']) {
                $columnCode .= '->unsigned()';
            }

            if (isset($columnDef['autoincrement']) && $columnDef['autoincrement']) {
                $columnCode .= '->autoIncrement()';
            }

            $columnCode .= ';';
            $code[] = $columnCode;
        }

        // Add indexes
        foreach ($tableDef['indexes'] ?? [] as $indexName => $indexDef) {
            $columns = implode("', '", $indexDef['columns'] ?? []);

            if (isset($indexDef['type']) && $indexDef['type'] === 'PRIMARY') {
                $code[] = "            \$table->primary(['{$columns}']);";
            } elseif (isset($indexDef['unique']) && $indexDef['unique']) {
                $code[] = "            \$table->unique(['{$columns}'], '{$indexName}');";
            } else {
                $code[] = "            \$table->index(['{$columns}'], '{$indexName}');";
            }
        }

        // Add foreign keys
        foreach ($tableDef['foreign_keys'] ?? [] as $fkName => $fkDef) {
            $columns = implode("', '", $fkDef['columns'] ?? []);
            $refTable = $fkDef['referenced_table'] ?? '';
            $refColumns = implode("', '", $fkDef['referenced_columns'] ?? []);

            $fkCode = "            \$table->foreignKey(['{$columns}'], '{$refTable}', ['{$refColumns}'], '{$fkName}')";

            if (isset($fkDef['update_rule'])) {
                $fkCode .= "->onUpdate('{$fkDef['update_rule']}')";
            }

            if (isset($fkDef['delete_rule'])) {
                $fkCode .= "->onDelete('{$fkDef['delete_rule']}')";
            }

            $fkCode .= ';';
            $code[] = $fkCode;
        }

        $code[] = '        });';

        return implode("\n", $code);
    }

    /**
     * Generate code to drop a table
     */
    private function generateDropTableCode(string $tableName): string
    {
        return "        \$schema->dropIfExists('{$tableName}');";
    }

    /**
     * Generate code to modify a table
     */
    private function generateTableModificationCode(TableDifference $difference): string
    {
        if (! $difference->hasDifferences()) {
            return '';
        }

        $code = [];
        $code[] = "        \$schema->table('{$difference->tableName}', function (\$table) {";

        // Add missing columns
        foreach ($difference->missingColumns as $columnName => $columnDef) {
            $type = $columnDef['type'] ?? 'string';
            $columnCode = "            \$table->add{$this->mapColumnType($type)}('{$columnName}')";

            // Add modifiers
            if (isset($columnDef['length']) && $columnDef['length'] > 0) {
                $columnCode .= "->length({$columnDef['length']})";
            }

            if (isset($columnDef['nullable']) && $columnDef['nullable']) {
                $columnCode .= '->nullable()';
            }

            if (isset($columnDef['default'])) {
                $default = $this->formatDefaultValue($columnDef['default'], $type);
                $columnCode .= "->default({$default})";
            }

            if (isset($columnDef['unsigned']) && $columnDef['unsigned']) {
                $columnCode .= '->unsigned()';
            }

            $columnCode .= ';';
            $code[] = $columnCode;
        }

        // Modify columns
        foreach ($difference->modifiedColumns as $columnName => $columnDiff) {
            $targetDef = $columnDiff['target'];
            $type = $targetDef['type'] ?? 'string';
            $columnCode = "            \$table->modify{$this->mapColumnType($type)}('{$columnName}')";

            // Add modifiers
            if (isset($targetDef['length']) && $targetDef['length'] > 0) {
                $columnCode .= "->length({$targetDef['length']})";
            }

            if (isset($targetDef['nullable']) && $targetDef['nullable']) {
                $columnCode .= '->nullable()';
            } elseif (isset($targetDef['nullable']) && ! $targetDef['nullable']) {
                $columnCode .= '->notNullable()';
            }

            if (isset($targetDef['default'])) {
                $default = $this->formatDefaultValue($targetDef['default'], $type);
                $columnCode .= "->default({$default})";
            }

            if (isset($targetDef['unsigned']) && $targetDef['unsigned']) {
                $columnCode .= '->unsigned()';
            }

            $columnCode .= ';';
            $code[] = $columnCode;
        }

        // Drop extra columns
        foreach ($difference->extraColumns as $columnName => $columnDef) {
            $code[] = "            \$table->dropColumn('{$columnName}');";
        }

        // Add missing indexes
        foreach ($difference->missingIndexes as $indexName => $indexDef) {
            $columns = implode("', '", $indexDef['columns'] ?? []);

            if (isset($indexDef['type']) && $indexDef['type'] === 'PRIMARY') {
                $code[] = "            \$table->primary(['{$columns}']);";
            } elseif (isset($indexDef['unique']) && $indexDef['unique']) {
                $code[] = "            \$table->unique(['{$columns}'], '{$indexName}');";
            } else {
                $code[] = "            \$table->index(['{$columns}'], '{$indexName}');";
            }
        }

        // Drop extra indexes
        foreach ($difference->extraIndexes as $indexName => $indexDef) {
            $code[] = "            \$table->dropIndex('{$indexName}');";
        }

        // Add missing foreign keys
        foreach ($difference->missingForeignKeys as $fkName => $fkDef) {
            $columns = implode("', '", $fkDef['columns'] ?? []);
            $refTable = $fkDef['referenced_table'] ?? '';
            $refColumns = implode("', '", $fkDef['referenced_columns'] ?? []);

            $fkCode = "            \$table->foreignKey(['{$columns}'], '{$refTable}', ['{$refColumns}'], '{$fkName}')";

            if (isset($fkDef['update_rule'])) {
                $fkCode .= "->onUpdate('{$fkDef['update_rule']}')";
            }

            if (isset($fkDef['delete_rule'])) {
                $fkCode .= "->onDelete('{$fkDef['delete_rule']}')";
            }

            $fkCode .= ';';
            $code[] = $fkCode;
        }

        // Drop extra foreign keys
        foreach ($difference->extraForeignKeys as $fkName => $fkDef) {
            $code[] = "            \$table->dropForeignKey('{$fkName}');";
        }

        $code[] = '        });';

        return implode("\n", $code);
    }

    /**
     * Generate code to reverse table modifications
     */
    private function generateReverseTableModificationCode(TableDifference $difference): string
    {
        if (! $difference->hasDifferences()) {
            return '';
        }

        $code = [];
        $code[] = "        \$schema->table('{$difference->tableName}', function (\$table) {";

        // Drop missing columns (they were added in up)
        foreach ($difference->missingColumns as $columnName => $columnDef) {
            $code[] = "            \$table->dropColumn('{$columnName}');";
        }

        // Restore modified columns to original state
        foreach ($difference->modifiedColumns as $columnName => $columnDiff) {
            $sourceDef = $columnDiff['source'];
            $type = $sourceDef['type'] ?? 'string';
            $columnCode = "            \$table->modify{$this->mapColumnType($type)}('{$columnName}')";

            // Add modifiers
            if (isset($sourceDef['length']) && $sourceDef['length'] > 0) {
                $columnCode .= "->length({$sourceDef['length']})";
            }

            if (isset($sourceDef['nullable']) && $sourceDef['nullable']) {
                $columnCode .= '->nullable()';
            } elseif (isset($sourceDef['nullable']) && ! $sourceDef['nullable']) {
                $columnCode .= '->notNullable()';
            }

            if (isset($sourceDef['default'])) {
                $default = $this->formatDefaultValue($sourceDef['default'], $type);
                $columnCode .= "->default({$default})";
            }

            if (isset($sourceDef['unsigned']) && $sourceDef['unsigned']) {
                $columnCode .= '->unsigned()';
            }

            $columnCode .= ';';
            $code[] = $columnCode;
        }

        // Add extra columns (they were dropped in up)
        foreach ($difference->extraColumns as $columnName => $columnDef) {
            $type = $columnDef['type'] ?? 'string';
            $columnCode = "            \$table->add{$this->mapColumnType($type)}('{$columnName}')";

            // Add modifiers
            if (isset($columnDef['length']) && $columnDef['length'] > 0) {
                $columnCode .= "->length({$columnDef['length']})";
            }

            if (isset($columnDef['nullable']) && $columnDef['nullable']) {
                $columnCode .= '->nullable()';
            }

            if (isset($columnDef['default'])) {
                $default = $this->formatDefaultValue($columnDef['default'], $type);
                $columnCode .= "->default({$default})";
            }

            if (isset($columnDef['unsigned']) && $columnDef['unsigned']) {
                $columnCode .= '->unsigned()';
            }

            $columnCode .= ';';
            $code[] = $columnCode;
        }

        // Drop missing indexes (they were added in up)
        foreach ($difference->missingIndexes as $indexName => $indexDef) {
            $code[] = "            \$table->dropIndex('{$indexName}');";
        }

        // Add extra indexes (they were dropped in up)
        foreach ($difference->extraIndexes as $indexName => $indexDef) {
            $columns = implode("', '", $indexDef['columns'] ?? []);

            if (isset($indexDef['type']) && $indexDef['type'] === 'PRIMARY') {
                $code[] = "            \$table->primary(['{$columns}']);";
            } elseif (isset($indexDef['unique']) && $indexDef['unique']) {
                $code[] = "            \$table->unique(['{$columns}'], '{$indexName}');";
            } else {
                $code[] = "            \$table->index(['{$columns}'], '{$indexName}');";
            }
        }

        // Drop missing foreign keys (they were added in up)
        foreach ($difference->missingForeignKeys as $fkName => $fkDef) {
            $code[] = "            \$table->dropForeignKey('{$fkName}');";
        }

        // Add extra foreign keys (they were dropped in up)
        foreach ($difference->extraForeignKeys as $fkName => $fkDef) {
            $columns = implode("', '", $fkDef['columns'] ?? []);
            $refTable = $fkDef['referenced_table'] ?? '';
            $refColumns = implode("', '", $fkDef['referenced_columns'] ?? []);

            $fkCode = "            \$table->foreignKey(['{$columns}'], '{$refTable}', ['{$refColumns}'], '{$fkName}')";

            if (isset($fkDef['update_rule'])) {
                $fkCode .= "->onUpdate('{$fkDef['update_rule']}')";
            }

            if (isset($fkDef['delete_rule'])) {
                $fkCode .= "->onDelete('{$fkDef['delete_rule']}')";
            }

            $fkCode .= ';';
            $code[] = $fkCode;
        }

        $code[] = '        });';

        return implode("\n", $code);
    }

    /**
     * Generate the migration class
     */
    private function generateMigrationClass(
        string $className,
        string $description,
        MigrationVersion $version,
        string $upCode,
        string $downCode
    ): string {
        $template = <<<PHP
<?php

declare(strict_types=1);

namespace App\Database\Migration;

use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Migration\AbstractMigration;
use App\Framework\Database\Migration\MigrationVersion;
use App\Framework\Database\Schema\Schema;

final class {$className} extends AbstractMigration
{
    public function getVersion(): MigrationVersion
    {
        return MigrationVersion::fromTimestamp('{$version->toString()}');
    }

    public function getDescription(): string
    {
        return '{$description}';
    }

    public function up(ConnectionInterface \$connection): void
    {
        \$schema = new Schema(\$connection);

{$upCode}

        \$schema->execute();
    }

    public function down(ConnectionInterface \$connection): void
    {
        \$schema = new Schema(\$connection);

{$downCode}

        \$schema->execute();
    }
}
PHP;

        return $template;
    }

    /**
     * Map database column type to schema builder method
     */
    private function mapColumnType(string $type): string
    {
        return match (strtolower($type)) {
            'int', 'integer' => 'integer',
            'bigint' => 'bigInteger',
            'smallint' => 'smallInteger',
            'tinyint' => 'tinyInteger',
            'float', 'double', 'real' => 'float',
            'decimal', 'numeric' => 'decimal',
            'boolean', 'bool' => 'boolean',
            'date' => 'date',
            'datetime' => 'dateTime',
            'timestamp' => 'timestamp',
            'time' => 'time',
            'char' => 'char',
            'varchar', 'string' => 'string',
            'text' => 'text',
            'mediumtext' => 'mediumText',
            'longtext' => 'longText',
            'binary' => 'binary',
            'blob' => 'binary',
            'json' => 'json',
            'jsonb' => 'jsonb',
            'uuid' => 'uuid',
            'enum' => 'enum',
            default => 'string',
        };
    }

    /**
     * Format default value for use in migration code
     */
    private function formatDefaultValue(mixed $value, string $type): string
    {
        if ($value === null) {
            return 'null';
        }

        return match (strtolower($type)) {
            'int', 'integer', 'bigint', 'smallint', 'tinyint', 'float', 'double', 'real', 'decimal', 'numeric' => (string) $value,
            'boolean', 'bool' => $value ? 'true' : 'false',
            default => "'{$value}'",
        };
    }
}
