<?php
declare(strict_types=1);

/**
 * Read-only repository for YachtMemory Architecture Decision Records.
 *
 * The filesystem and the metadata stored in the Markdown sources are authoritative.
 * This class maintains no manifest and writes no ADR data.
 */
final class AdrRepository
{
    private string $baseDir;
    private string $sourceDir;
    private string $pdfDir;
    private string $archiveDir;

    private const ALLOWED_STATUS = [
        'Draft',
        'Accepted',
        'Superseded',
        'Deprecated',
    ];

    public function __construct(?string $baseDir = null)
    {
        $this->baseDir = rtrim($baseDir ?? dirname(__DIR__, 2) . '/docs/adr', '/');
        $this->sourceDir = $this->baseDir . '/source';
        $this->pdfDir = $this->baseDir . '/pdf';
        $this->archiveDir = $this->baseDir . '/archive';
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    public function all(): array
    {
        $documents = [];

        foreach ($this->markdownFiles() as $file) {
            $doc = $this->readSource($file);
            $documents[] = $doc;
        }

        usort(
            $documents,
            static fn(array $a, array $b): int => strcmp((string)$a['adr'], (string)$b['adr'])
        );

        return $documents;
    }

    /**
     * @return array<string,mixed>|null
     */
    public function find(string $adr): ?array
    {
        $adr = strtoupper(trim($adr));

        foreach ($this->all() as $doc) {
            if ($doc['adr'] === $adr) {
                return $doc;
            }
        }

        return null;
    }

    /**
     * @return array{ok:bool,errors:array<int,string>,warnings:array<int,string>,documents:array<int,array<string,mixed>>}
     */
    public function checkConsistency(): array
    {
        $errors = [];
        $warnings = [];
        $documents = [];
        $ids = [];
        $expectedCurrentPdfs = [];
        $knownIds = [];

        foreach ($this->markdownFiles() as $file) {
            try {
                $doc = $this->readSource($file);
                $documents[] = $doc;
            } catch (Throwable $e) {
                $errors[] = basename($file) . ': ' . $e->getMessage();
                continue;
            }

            $adr = $doc['adr'];
            $knownIds[$adr] = true;

            if (isset($ids[$adr])) {
                $errors[] = sprintf(
                    '%s: ADR-ID %s ist bereits in %s definiert.',
                    basename($file),
                    $adr,
                    $ids[$adr]
                );
            } else {
                $ids[$adr] = basename($file);
            }

            foreach (['adr', 'title', 'category', 'status', 'version', 'date'] as $required) {
                if (!isset($doc[$required]) || trim((string)$doc[$required]) === '') {
                    $errors[] = basename($file) . ": Pflichtfeld '{$required}' fehlt.";
                }
            }

            if (!preg_match('/^ADR-\d{4}$/', $adr)) {
                $errors[] = basename($file) . ": ungültige ADR-ID '{$adr}'.";
            }

            if (!in_array($doc['status'], self::ALLOWED_STATUS, true)) {
                $errors[] = basename($file) . ": ungültiger Status '{$doc['status']}'.";
            }

            if (!preg_match('/^\d+\.\d+$/', $doc['version'])) {
                $errors[] = basename($file) . ": ungültige Version '{$doc['version']}'. Erwartet wird z. B. 1.0.";
            }

            $date = DateTimeImmutable::createFromFormat('!Y-m-d', $doc['date']);
            $dateErrors = DateTimeImmutable::getLastErrors();
            $dateInvalid = $date === false
                || ($dateErrors !== false && ($dateErrors['warning_count'] > 0 || $dateErrors['error_count'] > 0))
                || ($date !== false && $date->format('Y-m-d') !== $doc['date']);

            if ($dateInvalid) {
                $errors[] = basename($file) . ": ungültiges Datum '{$doc['date']}'.";
            }

            if ($doc['status'] === 'Accepted') {
                if (empty($doc['pdf'])) {
                    $errors[] = basename($file) . ': Accepted ADR benötigt das Metadatenfeld pdf.';
                } else {
                    $pdfName = basename((string)$doc['pdf']);
                    if ($pdfName !== $doc['pdf']) {
                        $errors[] = basename($file) . ': pdf darf nur einen Dateinamen enthalten.';
                    } else {
                        $expectedCurrentPdfs[$pdfName] = $adr;
                        if (!is_file($this->pdfDir . '/' . $pdfName)) {
                            $errors[] = "{$adr}: aktuelle PDF fehlt: {$pdfName}";
                        }

                        if (!str_contains($pdfName, $adr)) {
                            $errors[] = "{$adr}: PDF-Dateiname enthält die ADR-ID nicht: {$pdfName}";
                        }

                        if (!str_contains($pdfName, '-v' . $doc['version'] . '.pdf')) {
                            $errors[] = "{$adr}: PDF-Dateiname passt nicht zur Version {$doc['version']}: {$pdfName}";
                        }

                        if (is_file($this->archiveDir . '/' . $pdfName)) {
                            $errors[] = "{$adr}: aktuelle PDF liegt gleichzeitig im Archiv: {$pdfName}";
                        }
                    }
                }
            } elseif (!empty($doc['pdf'])) {
                $warnings[] = "{$adr}: Status {$doc['status']} enthält ein pdf-Feld; geprüft wird es nur bei Accepted.";
            }
        }

        foreach ($this->pdfFiles($this->pdfDir) as $pdf) {
            $name = basename($pdf);
            if (!isset($expectedCurrentPdfs[$name])) {
                $errors[] = "Nicht referenzierte aktuelle PDF in pdf/: {$name}";
            }
        }

        foreach ($this->pdfFiles($this->archiveDir) as $pdf) {
            $name = basename($pdf);

            if (!preg_match('/(ADR-\d{4})/i', $name, $m)) {
                $errors[] = "Archiv-PDF ohne erkennbare ADR-ID: {$name}";
                continue;
            }

            $adr = strtoupper($m[1]);
            if (!isset($knownIds[$adr])) {
                $errors[] = "Archiv-PDF gehört zu keiner vorhandenen ADR-Quelle: {$name}";
                continue;
            }

            $doc = null;
            foreach ($documents as $candidate) {
                if ($candidate['adr'] === $adr) {
                    $doc = $candidate;
                    break;
                }
            }

            if ($doc !== null && preg_match('/-v(\d+\.\d+)\.pdf$/', $name, $vm)) {
                if ($vm[1] === $doc['version']) {
                    $errors[] = "{$adr}: aktuelle Version {$doc['version']} liegt im Archiv: {$name}";
                }
            }
        }

        usort(
            $documents,
            static fn(array $a, array $b): int => strcmp((string)$a['adr'], (string)$b['adr'])
        );

        return [
            'ok' => count($errors) === 0,
            'errors' => $errors,
            'warnings' => $warnings,
            'documents' => $documents,
        ];
    }

    /**
     * Returns the verified absolute path of the current PDF for an Accepted ADR.
     * No user supplied path is ever used.
     */
    public function currentPdfPath(string $adr): ?string
    {
        $doc = $this->find($adr);

        if ($doc === null || $doc['status'] !== 'Accepted' || empty($doc['pdf'])) {
            return null;
        }

        $name = basename((string)$doc['pdf']);
        if ($name !== $doc['pdf']) {
            return null;
        }

        $path = $this->pdfDir . '/' . $name;
        return is_file($path) ? $path : null;
    }

    /**
     * @return array<string,string>
     */
    private function parseMetadata(string $content, string $file): array
    {
        if (!preg_match('/\A---\R(.*?)\R---\R/s', $content, $match)) {
            throw new RuntimeException('standardisierter Metadatenkopf fehlt.');
        }

        $metadata = [];

        foreach (preg_split('/\R/', trim($match[1])) as $line) {
            if (trim($line) === '') {
                continue;
            }

            if (!preg_match('/^([a-z_]+):\s*(.*?)\s*$/', $line, $parts)) {
                throw new RuntimeException("ungültige Metadatenzeile: {$line}");
            }

            $key = $parts[1];
            if (array_key_exists($key, $metadata)) {
                throw new RuntimeException("Metadatenfeld '{$key}' ist doppelt vorhanden.");
            }

            $metadata[$key] = trim($parts[2], " \t\n\r\0\x0B\"'");
        }

        return $metadata;
    }

    /**
     * @return array<string,mixed>
     */
    private function readSource(string $file): array
    {
        $content = file_get_contents($file);
        if ($content === false) {
            throw new RuntimeException('Quelle kann nicht gelesen werden.');
        }

        $metadata = $this->parseMetadata($content, $file);

        return [
            'adr' => strtoupper((string)($metadata['adr'] ?? '')),
            'title' => (string)($metadata['title'] ?? ''),
            'category' => (string)($metadata['category'] ?? ''),
            'status' => (string)($metadata['status'] ?? ''),
            'version' => (string)($metadata['version'] ?? ''),
            'date' => (string)($metadata['date'] ?? ''),
            'pdf' => isset($metadata['pdf']) && $metadata['pdf'] !== '' ? $metadata['pdf'] : null,
            'source_file' => basename($file),
            'source_path' => $file,
        ];
    }

    /**
     * @return array<int,string>
     */
    private function markdownFiles(): array
    {
        if (!is_dir($this->sourceDir)) {
            return [];
        }

        $files = glob($this->sourceDir . '/ADR-*.md') ?: [];
        sort($files, SORT_NATURAL | SORT_FLAG_CASE);
        return $files;
    }

    /**
     * @return array<int,string>
     */
    private function pdfFiles(string $dir): array
    {
        if (!is_dir($dir)) {
            return [];
        }

        $files = glob($dir . '/*.pdf') ?: [];
        sort($files, SORT_NATURAL | SORT_FLAG_CASE);
        return $files;
    }
}
