<?php

declare(strict_types=1);

namespace Core;

class MigrationManager
{
    protected Database $database;
    protected string $path;

    public function __construct(Database $database, string $path)
    {
        $this->database = $database;
        $this->path = rtrim($path, '/');
    }

    public function ensureTable(): void
    {
        $this->database->execute(
            'CREATE TABLE IF NOT EXISTS migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, migration VARCHAR(255) NOT NULL, ran_at DATETIME DEFAULT CURRENT_TIMESTAMP)'
        );

        $this->database->execute(
            'DELETE FROM migrations
             WHERE id NOT IN (
                 SELECT MIN(id)
                 FROM migrations
                 GROUP BY migration
             )'
        );

        $this->database->execute(
            'CREATE UNIQUE INDEX IF NOT EXISTS idx_migrations_migration_unique ON migrations (migration)'
        );

        $files = array_map('basename', $this->migrationFiles());

        if ($files === []) {
            $this->database->execute('DELETE FROM migrations');
            return;
        }

        $placeholders = [];
        $parameters = [];

        foreach ($files as $index => $file) {
            $placeholder = 'migration_' . $index;
            $placeholders[] = ':' . $placeholder;
            $parameters[$placeholder] = $file;
        }

        $this->database->execute(
            'DELETE FROM migrations WHERE migration NOT IN (' . implode(', ', $placeholders) . ')',
            $parameters
        );
    }

    public function status(): array
    {
        $this->ensureTable();

        $ran = $this->database->query('SELECT migration, ran_at FROM migrations ORDER BY id ASC');
        $ranByName = [];

        foreach ($ran as $row) {
            $ranByName[$row['migration']] = $row['ran_at'];
        }

        $files = $this->migrationFiles();

        $status = [];

        foreach ($files as $file) {
            $name = basename($file);
            $status[] = [
                'migration' => $name,
                'ran' => array_key_exists($name, $ranByName),
                'ran_at' => $ranByName[$name] ?? null,
            ];
        }

        return $status;
    }

    public function runPending(): array
    {
        $this->ensureTable();

        $ran = $this->database->query('SELECT migration FROM migrations');
        $ranNames = array_column($ran, 'migration');
        $files = $this->migrationFiles();
        $ranMigrations = [];

        foreach ($files as $file) {
            $name = basename($file);

            if (in_array($name, $ranNames, true)) {
                continue;
            }

            $migration = $this->loadMigration($file);

            $this->database->pdo()->beginTransaction();

            try {
                $migration->up($this->database);

                $this->database->execute(
                    'INSERT OR IGNORE INTO migrations (migration) VALUES (:migration)',
                    ['migration' => $name]
                );

                $this->database->pdo()->commit();
                $ranMigrations[] = $name;
            } catch (\Throwable $exception) {
                $this->database->pdo()->rollBack();
                throw $exception;
            }
        }

        return $ranMigrations;
    }

    public function rollback(int $steps = 1): array
    {
        $this->ensureTable();

        $steps = max(1, $steps);
        $applied = $this->database->query(
            "SELECT MAX(id) AS id, migration
             FROM migrations
             GROUP BY migration
             ORDER BY id DESC
             LIMIT {$steps}"
        );
        $rolledBack = [];

        foreach ($applied as $row) {
            $file = $this->path . '/' . $row['migration'];

            if (!file_exists($file)) {
                throw new \RuntimeException("Migration file not found for rollback: {$row['migration']}");
            }

            $migration = $this->loadMigration($file);
            $this->database->pdo()->beginTransaction();

            try {
                $migration->down($this->database);

                $this->database->execute(
                    'DELETE FROM migrations WHERE id = :id',
                    ['id' => $row['id']]
                );

                $this->database->pdo()->commit();
                $rolledBack[] = $row['migration'];
            } catch (\Throwable $exception) {
                $this->database->pdo()->rollBack();
                throw $exception;
            }
        }

        return $rolledBack;
    }

    public function fresh(): array
    {
        $this->ensureTable();

        $files = array_reverse($this->migrationFiles());
        $rolledBack = [];

        foreach ($files as $file) {
            $migration = $this->loadMigration($file);
            $name = basename($file);

            $this->database->pdo()->beginTransaction();

            try {
                $migration->down($this->database);
                $this->database->pdo()->commit();
                $rolledBack[] = $name;
            } catch (\Throwable $exception) {
                $this->database->pdo()->rollBack();
                throw $exception;
            }
        }

        $this->database->execute('DELETE FROM migrations');
        $ran = $this->runPending();

        return [
            'rolled_back' => $rolledBack,
            'migrated' => $ran,
        ];
    }

    public function make(string $name): string
    {
        $slug = trim(strtolower(preg_replace('/[^a-zA-Z0-9]+/', '_', $name) ?? ''), '_');

        if ($slug === '') {
            throw new \InvalidArgumentException('Migration name must contain letters or numbers.');
        }

        if (!is_dir($this->path)) {
            mkdir($this->path, 0777, true);
        }

        $timestamp = date('Ymd_His');
        $filename = $timestamp . '_' . $slug . '.php';
        $path = $this->path . '/' . $filename;

        if (file_exists($path)) {
            throw new \RuntimeException("Migration already exists: {$filename}");
        }

        $template = <<<PHP
<?php

declare(strict_types=1);

use Core\Database;
use Core\Migration;

return new class extends Migration
{
    public function up(Database \$database): void
    {
        // Write the forward migration here.
    }

    public function down(Database \$database): void
    {
        // Write the rollback migration here.
    }
};
PHP;

        file_put_contents($path, $template . PHP_EOL);

        return $path;
    }

    private function migrationFiles(): array
    {
        $files = glob($this->path . '/*.php') ?: [];
        sort($files);

        return $files;
    }

    private function loadMigration(string $file): Migration
    {
        $migration = require $file;

        if ($migration instanceof Migration) {
            return $migration;
        }

        if (is_callable($migration)) {
            return new class ($migration, basename($file)) extends Migration
            {
                private $callback;
                private string $name;

                public function __construct(callable $callback, string $name)
                {
                    $this->callback = $callback;
                    $this->name = $name;
                }

                public function up(Database $database): void
                {
                    ($this->callback)($database);
                }

                public function down(Database $database): void
                {
                    throw new \RuntimeException("Migration {$this->name} cannot be rolled back because it has no down() method.");
                }
            };
        }

        throw new \RuntimeException('Migration files must return a Migration instance.');
    }
}
