<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class GenerateTestingSchema extends Command
{
    protected $signature = 'schema:generate-testing {--connection=pgsql : The database connection to read from}';

    protected $description = 'Generate SQLite testing schema from the PostgreSQL database';

    private array $typeMap = [
        '/\bbigint\b/' => 'INTEGER',
        '/\binteger\b/' => 'INTEGER',
        '/\bsmallint\b/' => 'INTEGER',
        '/\bboolean\b/' => 'INTEGER',
        '/character varying\(\d+\)/' => 'TEXT',
        '/timestamp\(\d+\) without time zone/' => 'TEXT',
        '/timestamp\(\d+\) with time zone/' => 'TEXT',
        '/\bjsonb\b/' => 'TEXT',
        '/\bjson\b/' => 'TEXT',
        '/\buuid\b/' => 'TEXT',
        '/double precision/' => 'REAL',
        '/numeric\(\d+,\d+\)/' => 'REAL',
        '/\bdate\b/' => 'TEXT',
    ];

    private array $castRemovals = [
        '::character varying',
        '::text',
        '::integer',
        '::boolean',
        '::timestamp without time zone',
        '::timestamp with time zone',
        '::numeric',
    ];

    public function handle(): int
    {
        $connection = $this->option('connection');

        if (DB::connection($connection)->getDriverName() !== 'pgsql') {
            $this->error("Connection '{$connection}' is not PostgreSQL.");

            return self::FAILURE;
        }

        $this->info('Reading schema from PostgreSQL...');

        $tables = $this->getTables($connection);
        $lastMigration = DB::connection($connection)
            ->table('migrations')
            ->orderByDesc('id')
            ->value('migration');

        $output = [];
        $output[] = '-- Generated by: php artisan schema:generate-testing';
        $output[] = '-- Date: '.now()->format('Y-m-d H:i:s');
        $output[] = '-- Last migration: '.($lastMigration ?? 'none');
        $output[] = '';

        foreach ($tables as $table) {
            $columns = $this->getColumns($connection, $table);
            $output[] = $this->generateCreateTable($table, $columns);
        }

        $indexes = $this->getIndexes($connection, $tables);
        foreach ($indexes as $index) {
            $output[] = $index;
        }

        $output[] = '';
        $output[] = '-- Migration records';

        $migrations = DB::connection($connection)->table('migrations')->orderBy('id')->get();
        foreach ($migrations as $m) {
            $migration = str_replace("'", "''", $m->migration);
            $output[] = "INSERT INTO \"migrations\" (\"id\", \"migration\", \"batch\") VALUES ({$m->id}, '{$migration}', {$m->batch});";
        }

        $path = database_path('schema/testing-schema.sql');

        if (! is_dir(dirname($path))) {
            mkdir(dirname($path), 0755, true);
        }

        file_put_contents($path, implode("\n", $output)."\n");

        $this->info("Schema written to {$path}");
        $this->info(count($tables).' tables, '.count($migrations).' migration records.');

        return self::SUCCESS;
    }

    private function getTables(string $connection): array
    {
        return collect(DB::connection($connection)->select(
            "SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename"
        ))->pluck('tablename')->toArray();
    }

    private function getColumns(string $connection, string $table): array
    {
        return DB::connection($connection)->select(
            "SELECT column_name, data_type, character_maximum_length, column_default,
                    is_nullable, udt_name, numeric_precision, numeric_scale, datetime_precision
             FROM information_schema.columns
             WHERE table_schema = 'public' AND table_name = ?
             ORDER BY ordinal_position",
            [$table]
        );
    }

    private function generateCreateTable(string $table, array $columns): string
    {
        $lines = [];

        foreach ($columns as $col) {
            $lines[] = '    '.$this->generateColumnDef($table, $col);
        }

        return "CREATE TABLE IF NOT EXISTS \"{$table}\" (\n".implode(",\n", $lines)."\n);\n";
    }

    private function generateColumnDef(string $table, object $col): string
    {
        $name = $col->column_name;
        $sqliteType = $this->convertType($col);

        // Auto-increment primary key for id columns
        if ($name === 'id' && $sqliteType === 'INTEGER' && $col->is_nullable === 'NO' && str_contains((string) $col->column_default, 'nextval')) {
            return "\"{$name}\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL";
        }

        $parts = ["\"{$name}\"", $sqliteType];

        // Default value
        $default = $col->column_default;
        if ($default !== null && ! str_contains($default, 'nextval')) {
            $default = $this->cleanDefault($default);
            $parts[] = "DEFAULT {$default}";
        }

        // NOT NULL
        if ($col->is_nullable === 'NO') {
            $parts[] = 'NOT NULL';
        }

        return implode(' ', $parts);
    }

    private function convertType(object $col): string
    {
        $pgType = $col->data_type;

        return match (true) {
            in_array($pgType, ['bigint', 'integer', 'smallint']) => 'INTEGER',
            $pgType === 'boolean' => 'INTEGER',
            in_array($pgType, ['character varying', 'text', 'USER-DEFINED']) => 'TEXT',
            str_contains($pgType, 'timestamp') => 'TEXT',
            in_array($pgType, ['json', 'jsonb']) => 'TEXT',
            $pgType === 'uuid' => 'TEXT',
            $pgType === 'double precision' => 'REAL',
            $pgType === 'numeric' => 'REAL',
            $pgType === 'date' => 'TEXT',
            default => 'TEXT',
        };
    }

    private function cleanDefault(string $default): string
    {
        foreach ($this->castRemovals as $cast) {
            $default = str_replace($cast, '', $default);
        }

        // Remove array type casts like ::text[]
        $default = preg_replace('/::[\w\s]+(\[\])?/', '', $default);

        return $default;
    }

    private function getIndexes(string $connection, array $tables): array
    {
        $results = [];

        $indexes = DB::connection($connection)->select(
            "SELECT indexname, tablename, indexdef FROM pg_indexes
             WHERE schemaname = 'public'
             ORDER BY tablename, indexname"
        );

        foreach ($indexes as $idx) {
            $def = $idx->indexdef;

            // Skip primary key indexes
            if (str_contains($def, '_pkey')) {
                continue;
            }

            // Skip PG-specific indexes (GIN, GIST, expression indexes)
            if (preg_match('/USING (gin|gist)/i', $def)) {
                continue;
            }
            if (str_contains($def, '->>') || str_contains($def, '::')) {
                continue;
            }

            // Convert to SQLite-compatible CREATE INDEX
            $unique = str_contains($def, 'UNIQUE') ? 'UNIQUE ' : '';

            // Extract columns from the index definition
            if (preg_match('/\((.+)\)$/', $def, $m)) {
                $cols = $m[1];
                $results[] = "CREATE {$unique}INDEX IF NOT EXISTS \"{$idx->indexname}\" ON \"{$idx->tablename}\" ({$cols});";
            }
        }

        return $results;
    }
}
