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

declare(strict_types=1);

/**
 * optimize-check — gate das pré-condições do `php artisan optimize` (ADR-008, plano 05 §4).
 *
 * Regras (falha != 0):
 *   1. Todo `.php` (fora de vendor/cache) tem `declare(strict_types=1)`.
 *   2. `env()` só é usado em `config/*.php` (nunca em app/src/routes/database/bootstrap).
 *   3. Arquivos de rota (web/api) não usam closure como AÇÃO de rota
 *      (`Route::get('/', function () {})`), que quebra o `route:cache`.
 *      Callbacks de agrupamento (`Route::group(function () {})`) são permitidos.
 *
 * Uso: php tools/optimize-check [RAIZ]   (default: raiz do repositório)
 */

require __DIR__.'/../vendor/autoload.php';

use PhpParser\Node;
use PhpParser\Node\Expr\ArrowFunction;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use PhpParser\ParserFactory;

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

/** Diretórios varridos (relativos à raiz). */
$scanDirs = ['app', 'config', 'database', 'routes', 'src', 'bootstrap', 'public'];

/** Trechos de caminho ignorados. */
$ignored = [
    '/vendor/',
    '/node_modules/',
    '/storage/',
    '/bootstrap/cache/',
    '/s3-local/',
    '/.import/',
    '/tools/tests/fixtures/',
];

/** Verbos de rota cujo 2º argumento não pode ser closure. */
$routeVerbs = ['get', 'post', 'put', 'patch', 'delete', 'options', 'any', 'match', 'fallback'];

/**
 * Coleta os arquivos `.php` (exceto `.blade.php`) sob os diretórios varridos.
 *
 * @param  list<string>  $dirs
 * @param  list<string>  $ignored
 * @return list<string>
 */
function phpFiles(string $root, array $dirs, array $ignored): array
{
    $files = [];

    foreach ($dirs as $dir) {
        $base = $root.'/'.$dir;
        if (! is_dir($base)) {
            continue;
        }

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

        foreach ($iterator as $file) {
            if (! $file->isFile() || $file->getExtension() !== 'php') {
                continue;
            }
            if (str_ends_with($file->getFilename(), '.blade.php')) {
                continue;
            }

            $path = $file->getPathname();
            foreach ($ignored as $needle) {
                if (str_contains($path, $needle)) {
                    continue 2;
                }
            }

            $files[] = $path;
        }
    }

    sort($files);

    return $files;
}

/** É arquivo de rota (web/api), e não um console.php? */
function isRouteFile(string $relative): bool
{
    if (str_ends_with($relative, 'console.php')) {
        return false;
    }

    return (bool) preg_match('#(^|/)routes/[^/]+\.php$#', $relative)
        || (bool) preg_match('#/Routes/[^/]+\.php$#', $relative);
}

$parser = (new ParserFactory)->createForNewestSupportedVersion();
$violations = [];

$files = phpFiles($root, $scanDirs, $ignored);

foreach ($files as $path) {
    $relative = ltrim(substr($path, strlen($root)), '/');
    $contents = (string) file_get_contents($path);

    // Regra 1: declare(strict_types=1)
    $head = substr($contents, 0, 800);
    if (preg_match('/declare\s*\(\s*strict_types\s*=\s*1\s*\)/', $head) !== 1) {
        $violations[] = "{$relative}: falta `declare(strict_types=1)`.";
    }

    // Regras 2 e 3 (AST)
    try {
        $ast = $parser->parse($contents);
    } catch (Throwable $e) {
        $violations[] = "{$relative}: não parseou ({$e->getMessage()}).";

        continue;
    }

    if ($ast === null) {
        continue;
    }

    $inConfig = str_starts_with($relative, 'config/');
    $isRoute = isRouteFile($relative);

    $traverser = new NodeTraverser;
    $traverser->addVisitor(new class($relative, $inConfig, $isRoute, $routeVerbs, $violations) extends NodeVisitorAbstract
    {
        /** @param list<string> $routeVerbs @param list<string> $violations */
        public function __construct(
            private readonly string $relative,
            private readonly bool $inConfig,
            private readonly bool $isRoute,
            private readonly array $routeVerbs,
            private array &$violations,
        ) {}

        public function enterNode(Node $node): null
        {
            // Regra 2: env() fora de config/
            if (! $this->inConfig && $node instanceof FuncCall
                && $node->name instanceof Name
                && strtolower($node->name->toString()) === 'env') {
                $this->violations[] = "{$this->relative}:{$node->getStartLine()}: `env()` fora de `config/`.";
            }

            // Regra 3: closure como ação de rota
            if ($this->isRoute && ($node instanceof StaticCall || $node instanceof MethodCall)) {
                $name = $node->name instanceof Identifier ? strtolower($node->name->toString()) : null;

                if ($name !== null && in_array($name, $this->routeVerbs, true)) {
                    foreach ($node->args as $arg) {
                        if ($arg instanceof Node\Arg && ($arg->value instanceof Closure || $arg->value instanceof ArrowFunction)) {
                            $this->violations[] = "{$this->relative}:{$node->getStartLine()}: closure como ação de rota (quebra `route:cache`) — use controller.";
                            break;
                        }
                    }
                }
            }

            return null;
        }
    });

    $traverser->traverse($ast);
}

if ($violations !== []) {
    fwrite(STDERR, "optimize-check: ".count($violations)." violação(ões):\n");
    foreach ($violations as $violation) {
        fwrite(STDERR, '  - '.$violation."\n");
    }
    exit(1);
}

echo 'optimize-check: OK ('.count($files)." arquivos; strict_types, env() em config/, zero closure em rota).\n";
exit(0);
