#!/usr/bin/env php
<?php

declare(strict_types=1);

/**
 * migration-check — gate de migrations seguras (ADR-017/ADR-028, plano 05 §3.2).
 *
 * Falha quando:
 *   no-drop            o down() dropa tabela/coluna (deve renomear para backup_*)
 *   no-varchar         coluna declarada como `varchar(n)`/`char(n)` (use `text` — ADR-007)
 *   ui-column          coluna de apresentação (*_html/_css/_style/_class/style)
 *   rich-jsonb         conteúdo rico declarado como string/text (deve ser jsonb)
 *   presentation-flag  flags is_highlighted/is_super_highlighted como coluna
 *
 * Alerta quando:
 *   CREATE INDEX CONCURRENTLY aparece dentro de helper transacional (safely/beginTransaction)
 *
 * Exceções justificadas: comentar `@migration-check-exception: <regra>` (ou sem regra = arquivo).
 *
 * Uso: php tools/migration-check [RAIZ]
 */

$root = $argv[1] ?? dirname(__DIR__);
$migrationsRoot = rtrim($root, '/').'/src';

if (! is_dir($migrationsRoot)) {
    fwrite(STDERR, "ERRO: '{$migrationsRoot}' não existe.\n");
    exit(2);
}

$iterator = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator($migrationsRoot, FilesystemIterator::SKIP_DOTS)
);

$files = [];
foreach ($iterator as $file) {
    if ($file->isFile() && str_contains($file->getPathname(), '/Database/Migrations/')) {
        $files[] = $file->getPathname();
    }
}
sort($files);

/** Extrai o corpo de down() por contagem de chaves. */
function downBody(string $code): string
{
    $pos = strpos($code, 'function down');
    if ($pos === false) {
        return '';
    }
    $brace = strpos($code, '{', $pos);
    if ($brace === false) {
        return '';
    }
    $depth = 0;
    $len = strlen($code);
    for ($i = $brace; $i < $len; $i++) {
        if ($code[$i] === '{') {
            $depth++;
        } elseif ($code[$i] === '}') {
            $depth--;
            if ($depth === 0) {
                return substr($code, $brace, $i - $brace + 1);
            }
        }
    }

    return substr($code, $brace);
}

/** Regras que o arquivo declara estar dispensado. */
function exceptions(string $code): array
{
    if (! preg_match_all('/@migration-check-exception(?::\s*([a-z0-9-]+))?/', $code, $m, PREG_SET_ORDER)) {
        return [];
    }
    if (in_array(null, array_map(static fn ($x) => $x[1] ?? null, $m), true)) {
        return ['*'];
    }

    return array_values(array_unique(array_map(static fn ($x) => $x[1], $m)));
}

$failures = [];
$warnings = [];
$excepted = [];

foreach ($files as $path) {
    $rel = substr($path, strlen($root) + 1);
    $code = (string) file_get_contents($path);
    $skip = exceptions($code);
    $down = downBody($code);

    $check = static function (string $rule, bool $condition, string $detail) use (&$failures, $rel, $skip): void {
        if (! $condition) {
            return;
        }
        if (in_array('*', $skip, true) || in_array($rule, $skip, true)) {
            return;
        }
        $failures[] = "{$rel}: [migration] {$rule}: {$detail}";
    };

    // no-drop: down() não pode dropar tabela/coluna (DROP TRIGGER é permitido).
    $drop = (bool) preg_match('/\bSchema::drop(IfExists)?\s*\(/', $down)
        || (bool) preg_match('/->dropColumn\s*\(|->drop\s*\(/', $down)
        || (bool) preg_match('/DB::statement\s*\(\s*[\'"][^\'"]*DROP\s+(TABLE|COLUMN)\b/i', $down);
    $check('no-drop', $drop, 'o down() dropa tabela/coluna; renomeie para backup_* (ADR-017)');

    // no-varchar: `text` é a convenção (ADR-007/spec 10 §21) — `varchar(n)` trunca e não é
    // interoperável; o limite vive na aplicação ou em `CHECK`. `->rememberToken()` entra na lista
    // porque emite `varchar(100)` (helper do framework, não da convenção).
    $varchar = (bool) preg_match('/->string\s*\(|->rememberToken\s*\(/', $code)
        || (bool) preg_match('/->char\s*\(\s*[\'"][^\'"]*[\'"]\s*,\s*\d+/', $code);
    $check('no-varchar', $varchar, 'coluna `varchar(n)`; use `text` com limite na app/`CHECK` (ADR-007/spec 10)');

    // ui-column
    $ui = (bool) preg_match('/->(?:string|text|json|jsonb|char|uuid)\s*\(\s*[\'"][^\'"]*(?:_html|_css|_style|_class)[\'"]/', $code)
        || (bool) preg_match('/->(?:string|text|json|jsonb)\s*\(\s*[\'"]style[\'"]/', $code);
    $check('ui-column', $ui, 'coluna de apresentação no banco; use apresentacao JSONB/SlotConfig (ADR-009/017)');

    // rich-jsonb
    $rich = (bool) preg_match('/->(?:string|text)\s*\(\s*[\'"][^\'"]*(?:corpo|content|body|conteudo|conteúdo|blocks|schema)[^\'"]*[\'"]/', $code);
    $check('rich-jsonb', $rich, 'conteúdo rico deve ser jsonb (AST), não string/text (ADR-028)');

    // presentation-flag
    $flag = (bool) preg_match('/[\'"]is_(?:highlighted|super_highlighted)[\'"]/', $code);
    $check('presentation-flag', $flag, 'flag de destaque deve viver em apresentacao JSONB (ADR-017)');

    // Alerta: CREATE INDEX CONCURRENTLY dentro de transação.
    if (preg_match('/CREATE INDEX CONCURRENTLY/i', $code)
        && preg_match('/safely\s*\(|beginTransaction/', $code)) {
        $warnings[] = "{$rel}: [migration] concurrent-index: CREATE INDEX CONCURRENTLY dentro de transação";
    }

    if ($skip !== []) {
        $excepted[] = $rel.' ('.($skip === ['*'] ? 'arquivo' : implode(', ', $skip)).')';
    }
}

foreach ($warnings as $warning) {
    fwrite(STDERR, "AVISO: {$warning}\n");
}
foreach ($excepted as $item) {
    echo "EXCEÇÃO justificada: {$item}\n";
}

if ($failures !== []) {
    foreach ($failures as $failure) {
        fwrite(STDERR, $failure."\n");
    }
    fwrite(STDERR, sprintf("migration-check: %d problema(s).\n", count($failures)));
    exit(1);
}

echo "migration-check: OK\n";
exit(0);
