<?php

declare(strict_types=1);

namespace App\Core\Database;

use PDO;
use RuntimeException;
use Throwable;

final class Migrator
{
    public function __construct(
        private readonly PDO $pdo,
        private readonly string $migrationsPath
    ) {
    }

    /**
     * @return array{total:int, applied:int, pending:int, pending_files:array<int, string>}
     */
    public function status(): array
    {
        $this->ensureMigrationsTable();

        $allFiles = $this->migrationFiles();
        $appliedFiles = $this->appliedFilenames();
        $pendingFiles = array_values(array_diff($allFiles, $appliedFiles));

        return [
            'total' => count($allFiles),
            'applied' => count($appliedFiles),
            'pending' => count($pendingFiles),
            'pending_files' => $pendingFiles,
        ];
    }

    /**
     * @return array{executed:int, skipped:int, logs:array<int, string>}
     */
    public function runPending(): array
    {
        $this->acquireLock();

        try {
            $this->ensureMigrationsTable();

            $allFiles = $this->migrationFiles();
            $appliedFiles = $this->appliedFilenames();
            $pendingFiles = array_values(array_diff($allFiles, $appliedFiles));

            $insert = $this->pdo->prepare(
                'INSERT INTO migrations (filename, executed_at) VALUES (:filename, :executed_at)'
            );

            $executed = 0;
            $skipped = 0;
            $logs = [];

            foreach ($pendingFiles as $filename) {
                $fullPath = rtrim($this->migrationsPath, '/\\') . DIRECTORY_SEPARATOR . $filename;
                $sql = file_get_contents($fullPath);
                if ($sql === false || trim($sql) === '') {
                    $skipped++;
                    $logs[] = '[skip-empty] ' . $filename;
                    continue;
                }

                try {
                    $this->pdo->beginTransaction();
                    $this->pdo->exec($sql);
                    $insert->execute([
                        'filename' => $filename,
                        'executed_at' => date('Y-m-d H:i:s'),
                    ]);
                    if ($this->pdo->inTransaction()) {
                        $this->pdo->commit();
                    }
                    $executed++;
                    $logs[] = '[ok] ' . $filename;
                } catch (Throwable $exception) {
                    if ($this->pdo->inTransaction()) {
                        $this->pdo->rollBack();
                    }
                    $logs[] = '[error] ' . $filename . ' - ' . $exception->getMessage();
                    throw $exception;
                }
            }

            return [
                'executed' => $executed,
                'skipped' => $skipped,
                'logs' => $logs,
            ];
        } finally {
            $this->releaseLock();
        }
    }

    private function ensureMigrationsTable(): void
    {
        $this->pdo->exec(
            'CREATE TABLE IF NOT EXISTS migrations (
                id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                filename VARCHAR(190) NOT NULL,
                executed_at DATETIME NOT NULL,
                UNIQUE KEY migrations_filename_unique (filename)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
        );
    }

    private function acquireLock(): void
    {
        $statement = $this->pdo->query("SELECT GET_LOCK('backpro_migrations_lock', 10)");
        $value = $statement !== false ? $statement->fetchColumn() : false;
        if ((string) $value !== '1') {
            throw new RuntimeException('Nie mozna uzyskac blokady migracji. Sprobuj ponownie za chwile.');
        }
    }

    private function releaseLock(): void
    {
        $this->pdo->query("DO RELEASE_LOCK('backpro_migrations_lock')");
    }

    /**
     * @return array<int, string>
     */
    private function migrationFiles(): array
    {
        $files = glob(rtrim($this->migrationsPath, '/\\') . DIRECTORY_SEPARATOR . '*.sql');
        if (!is_array($files)) {
            return [];
        }

        $filenames = array_map(static fn(string $path): string => basename($path), $files);
        sort($filenames);

        return $filenames;
    }

    /**
     * @return array<int, string>
     */
    private function appliedFilenames(): array
    {
        $statement = $this->pdo->query('SELECT filename FROM migrations ORDER BY filename');
        $rows = $statement->fetchAll(PDO::FETCH_COLUMN);
        if (!is_array($rows)) {
            return [];
        }

        return array_values(array_map(static fn(mixed $value): string => (string) $value, $rows));
    }
}
