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

declare(strict_types=1);

/**
 * arch-check — gate de fronteiras arquiteturais (ADR-013, plano 05 §3).
 *
 * Matriz permitida:
 *   core     -> core
 *   services -> core, services (de OUTRO service, só `Contracts|DTOs|Enums`; testes isentos)
 *   layout   -> core, services, layout
 *   modules  -> core, layout, o PRÓPRIO módulo, e services SOMENTE via
 *               `Services\{Nome}\{Contracts,DTOs,Enums}\*` (nunca implementação)
 *
 * Storage (ADR-026/ADR-030): **só** `src/services/Media/**` (e o provisionamento de bucket em
 * `src/core/Internal/System/Storage/**`) pode tocar `Storage`/Flysystem/AWS. Qualquer outro
 * módulo/service/layout que importe essas APIs quebra o build.
 *
 * Uso: php tools/arch-check [RAIZ]   (default: raiz do repositório; lê RAIZ/src)
 * Sai != 0 em qualquer violação.
 */

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

use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\NodeVisitorAbstract;
use PhpParser\ParserFactory;

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

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

/** Camadas e o que cada uma pode importar (modules é caso especial). */
$allowed = [
    'core' => ['core'],
    'services' => ['core', 'services'],
    'layout' => ['core', 'services', 'layout'],
];

/** @return string|null Camada de um FQCN, ou null se externo/ignorado. */
function layerOf(string $fqcn): ?string
{
    foreach (['Core' => 'core', 'Services' => 'services', 'Layout' => 'layout', 'Modules' => 'modules'] as $prefix => $layer) {
        if (str_starts_with($fqcn, $prefix.'\\')) {
            return $layer;
        }
    }

    return null;
}

// Descobre as raízes de módulos (diretório que contém ModuleProvider.php ou ServiceProvider.php).
$moduleRoots = []; // dir => namespace
$iterator = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator($src, FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
    if (! $file->isFile() || ! in_array($file->getFilename(), ['ModuleProvider.php', 'ServiceProvider.php'], true)) {
        continue;
    }
    $dir = $file->getPath();
    $rel = substr($dir, strlen($src) + 1);
    if (! str_starts_with($rel, 'modules/')) {
        continue;
    }
    $moduleRoots[$dir] = 'Modules\\'.str_replace('/', '\\', substr($rel, strlen('modules/')));
}

/** @return array{0: string, 1: ?string} [camada, namespace-do-modulo-próprio] */
function contextOf(string $filePath, string $src, array $moduleRoots): array
{
    $rel = substr($filePath, strlen($src) + 1);
    $layer = explode('/', $rel)[0];

    $ownModule = null;
    if ($layer === 'modules') {
        $best = null;
        foreach (array_keys($moduleRoots) as $dir) {
            if (str_starts_with($filePath, $dir.'/') && ($best === null || strlen($dir) > strlen($best))) {
                $best = $dir;
            }
        }
        if ($best !== null) {
            $ownModule = $moduleRoots[$best];
        }
    }

    return [$layer, $ownModule];
}

/** @return bool Se o FQCN é uma API de storage cujo uso é restrito ao service Media. */
function isStorageApi(string $fqcn): bool
{
    if ($fqcn === 'Storage') {
        return true;
    }

    foreach (['Illuminate\\Support\\Facades\\Storage', 'League\\Flysystem\\', 'Aws\\'] as $prefix) {
        if (str_starts_with($fqcn, $prefix)) {
            return true;
        }
    }

    return false;
}

$parser = (new ParserFactory)->createForNewestSupportedVersion();
$violations = [];
$excepted = [];
$phpFiles = [];
foreach ($iterator as $file) {
    if ($file->isFile() && $file->getExtension() === 'php') {
        $phpFiles[] = $file->getPathname();
    }
}
sort($phpFiles);

foreach ($phpFiles as $path) {
    $rel = substr($path, strlen($root) + 1);
    [$layer, $ownModule] = contextOf($path, $src, $moduleRoots);

    if (! isset($allowed[$layer]) && $layer !== 'modules') {
        continue; // fora das 4 camadas (ex.: tests-e2e)
    }

    try {
        $contents = (string) file_get_contents($path);
        if (str_contains($contents, '@arch-check-exception')) {
            $excepted[] = $rel;
            continue;
        }
        $ast = $parser->parse($contents);
    } catch (Throwable $e) {
        fwrite(STDERR, "ERRO ao parsear {$rel}: {$e->getMessage()}\n");
        exit(2);
    }
    if ($ast === null) {
        continue;
    }

    $references = [];
    $traverser = new NodeTraverser;
    $traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false]));
    $traverser->addVisitor(new class($references) extends NodeVisitorAbstract
    {
        /** @param array<int, array{0: string, 1: int}> $references */
        public function __construct(private array &$references) {}

        public function enterNode(Node $node): null
        {
            $names = [];
            if ($node instanceof Node\Stmt\Use_) {
                if ($node->type !== Node\Stmt\Use_::TYPE_NORMAL) {
                    return null;
                }
                foreach ($node->uses as $use) {
                    $names[] = [$use->name, $node->getStartLine()];
                }
            } elseif ($node instanceof Node\Stmt\GroupUse) {
                foreach ($node->uses as $use) {
                    if ($use->type === Node\Stmt\Use_::TYPE_NORMAL) {
                        $names[] = [new Node\Name($node->prefix->toString().'\\'.$use->name->toString()), $node->getStartLine()];
                    }
                }
            } elseif ($node instanceof Node\Expr\New_
                || $node instanceof Node\Expr\StaticCall
                || $node instanceof Node\Expr\ClassConstFetch
                || $node instanceof Node\Expr\Instanceof_
                || $node instanceof Node\Expr\StaticPropertyFetch) {
                $class = $node->class ?? null;
                if ($class instanceof Node\Name) {
                    $names[] = [$class, $node->getStartLine()];
                }
            }

            foreach ($names as [$name, $line]) {
                $resolved = $name->getAttribute('resolvedName');
                $fq = $resolved instanceof Node\Name ? $resolved->toString() : ltrim($name->toString(), '\\');
                $this->references[] = [$fq, $line];
            }

            return null;
        }
    });
    $traverser->traverse($ast);

    foreach ($references as [$fq, $line]) {
        // Fronteira de storage (ADR-026/ADR-030): só o Media fala com o disco/S3.
        $isStorageOwner = str_starts_with($rel, 'src/services/Media/')
            || str_starts_with($rel, 'src/core/Internal/System/Storage/')
            || str_contains($rel, '/Test/');

        if (! $isStorageOwner && isStorageApi($fq)) {
            $violations[] = sprintf('%s:%d: [arch] storage fora do service Media: %s', $rel, $line, $fq);
        }

        $targetLayer = layerOf($fq);
        if ($targetLayer === null) {
            continue;
        }

        if ($layer === 'services' && $targetLayer === 'services') {
            // Serviço só consome a **superfície de contrato** de outro serviço (ADR-015); a
            // implementação (Models/Actions/Support/Rules) fica no dono. Testes podem montar o
            // estado do vizinho diretamente (a regra vale para código de produção).
            $segments = explode('/', $rel);
            $ownService = 'Services\\'.($segments[2] ?? '');

            if (! str_starts_with($fq, $ownService.'\\') && ! str_contains($rel, '/Test/')) {
                $surface = explode('\\', $fq)[2] ?? '';

                if (! in_array($surface, ['Contracts', 'DTOs', 'Enums'], true)) {
                    $violations[] = sprintf('%s:%d: [arch] service importa implementação de outro service: %s', $rel, $line, $fq);
                }
            }

            continue;
        }

        if ($layer === 'modules') {
            if ($targetLayer === 'services') {
                // Módulo só consome a superfície de contrato do service (ADR-015).
                $segments = explode('\\', $fq);
                $surface = $segments[2] ?? '';
                $isContractSurface = ($segments[0] ?? '') === 'Services'
                    && in_array($surface, ['Contracts', 'DTOs', 'Enums'], true);

                if (! $isContractSurface) {
                    $violations[] = sprintf('%s:%d: [arch] módulo importa implementação de service: %s', $rel, $line, $fq);
                }

                continue;
            }

            if ($targetLayer !== 'modules') {
                continue; // módulo pode usar core/layout
            }

            // Módulo só pode referenciar o PRÓPRIO módulo.
            if ($ownModule === null || ! str_starts_with($fq, $ownModule.'\\')) {
                $violations[] = sprintf('%s:%d: [arch] módulo importa outro módulo: %s', $rel, $line, $fq);
            }

            continue;
        }

        if (! in_array($targetLayer, $allowed[$layer], true)) {
            $violations[] = sprintf('%s:%d: [arch] %s -> %s (%s)', $rel, $line, $layer, $targetLayer, $fq);
        }
    }
}

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

foreach ($excepted as $item) {
    echo "EXCEÇÃO justificada: {$item}\n";
}

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