<?php

declare(strict_types=1);

final class MyronExtensionMigrationRunner
{
    /**
     * @param array<string, callable(PDO): void|list<string>> $migrations
     */
    public function run(string $vendor, PDO $pdo, array $migrations): void
    {
        if (!preg_match('/^[a-z][a-z0-9_]*$/', $vendor)) {
            throw new InvalidArgumentException('Extension migration vendor must be snake_case.');
        }

        $trackingTable = $vendor . '_schema_migrations';
        $this->runStatement(
            $pdo,
            'CREATE TABLE IF NOT EXISTS ' . $trackingTable . ' (
                version VARCHAR(64) PRIMARY KEY,
                applied_at TEXT NOT NULL
            )'
        );

        $statement = $pdo->query('SELECT version FROM ' . $trackingTable);
        $applied = $statement ? $statement->fetchAll(PDO::FETCH_COLUMN) : [];
        $appliedVersions = array_fill_keys(array_map('strval', $applied), true);

        foreach ($migrations as $version => $migration) {
            $version = (string) $version;
            if (isset($appliedVersions[$version])) {
                continue;
            }

            $pdo->beginTransaction();
            try {
                if (is_callable($migration)) {
                    $migration($pdo);
                } else {
                    foreach ($migration as $sql) {
                        $sql = (string) $sql;
                        $this->assertStatementIsIsolated($vendor, $sql);
                        $this->runStatement($pdo, $sql);
                    }
                }

                $record = $pdo->prepare('INSERT INTO ' . $trackingTable . ' (version, applied_at) VALUES (:version, :applied_at)');
                $record->execute([
                    ':version' => $version,
                    ':applied_at' => myronNow(),
                ]);
                if ($pdo->inTransaction()) {
                    $pdo->commit();
                } elseif ($pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql') {
                    $pdo->exec('COMMIT');
                }
            } catch (Throwable $exception) {
                if ($pdo->inTransaction()) {
                    $pdo->rollBack();
                }
                throw $exception;
            }
        }
    }

    private function assertStatementIsIsolated(string $vendor, string $sql): void
    {
        $normalized = preg_replace('/\s+/', ' ', trim($sql)) ?? '';
        if ($normalized === '') {
            return;
        }

        $patterns = [
            '/^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?([A-Za-z0-9_]+)`?/i',
            '/^ALTER\s+TABLE\s+`?([A-Za-z0-9_]+)`?/i',
            '/^DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?`?([A-Za-z0-9_]+)`?/i',
            '/^CREATE\s+(?:UNIQUE\s+)?INDEX\s+`?[A-Za-z0-9_]+`?\s+ON\s+`?([A-Za-z0-9_]+)`?/i',
        ];

        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $normalized, $matches) !== 1) {
                continue;
            }
            $table = (string) ($matches[1] ?? '');
            if (!str_starts_with($table, $vendor . '_')) {
                throw new InvalidArgumentException('Extension migrations may only modify tables prefixed with ' . $vendor . '_.');
            }
            return;
        }
    }

    private function runStatement(PDO $pdo, string $sql): void
    {
        $statement = $pdo->prepare($sql);
        if ($statement === false) {
            throw new RuntimeException('Failed to prepare extension migration SQL statement.');
        }
        $statement->execute();
    }
}
