<?php

namespace Shm\ShmUtils\Terms;

use League\CommonMark\GithubFlavoredMarkdownConverter;
use Shm\ShmUtils\ShmInit;
use Shm\ShmUtils\ShmUtils;

class TermsCatalog
{
    /** @var array|null Cached scan result for current request */
    private static ?array $index = null;

    private static ?GithubFlavoredMarkdownConverter $converter = null;

    public static function termsDir(): string
    {
        return rtrim(ShmInit::$rootDir ?? '', DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'terms';
    }

    public static function exists(): bool
    {
        $dir = self::termsDir();
        if (!is_dir($dir)) {
            return false;
        }

        $index = self::scan();
        return !empty($index['rootDocs']) || !empty($index['sections']);
    }

    /**
     * @return array{rootDocs: list<array>, sections: list<array>}
     */
    public static function scan(): array
    {
        if (self::$index !== null) {
            return self::$index;
        }

        self::$index = [
            'rootDocs' => [],
            'sections' => [],
        ];

        $termsDir = self::termsDir();
        $termsReal = realpath($termsDir);
        if ($termsReal === false || !is_dir($termsReal)) {
            return self::$index;
        }

        $entries = scandir($termsReal);
        if ($entries === false) {
            return self::$index;
        }

        foreach ($entries as $entry) {
            if ($entry === '.' || $entry === '..' || str_starts_with($entry, '.')) {
                continue;
            }

            $fullPath = $termsReal . DIRECTORY_SEPARATOR . $entry;

            if (is_file($fullPath) && str_ends_with(strtolower($entry), '.md')) {
                $slug = self::slugFromFilename($entry);
                if ($slug === null) {
                    continue;
                }
                $safePath = self::safeFilePath($termsReal, $fullPath);
                if ($safePath === null) {
                    continue;
                }
                self::$index['rootDocs'][] = self::docMeta(null, $slug, $safePath, $entry);
                continue;
            }

            if (!is_dir($fullPath)) {
                continue;
            }

            $sectionSlug = $entry;
            if (!self::isValidName($sectionSlug)) {
                continue;
            }

            $sectionReal = realpath($fullPath);
            if ($sectionReal === false || !str_starts_with($sectionReal, $termsReal . DIRECTORY_SEPARATOR)) {
                continue;
            }

            $sectionUrl = self::buildCategoryUrl([$sectionSlug]);
            $contents = self::scanDirectoryLevel($sectionReal, $termsReal, [$sectionSlug], $sectionUrl);

            if ($contents['documents'] === [] && $contents['categories'] === []) {
                continue;
            }

            self::$index['sections'][] = [
                'dirName' => $sectionSlug,
                'title' => $sectionSlug,
                'url' => $sectionUrl,
                'documents' => $contents['documents'],
                'categories' => $contents['categories'],
            ];
        }

        usort(self::$index['rootDocs'], fn ($a, $b) => strcmp($a['title'], $b['title']));
        usort(self::$index['sections'], fn ($a, $b) => strcmp($a['dirName'], $b['dirName']));

        return self::$index;
    }

    public static function resolve(?string $section, ?string $slug): ?TermsDocument
    {
        if (!self::exists()) {
            return null;
        }

        $index = self::scan();

        if ($section !== null) {
            $section = self::decodePathSegment($section);
        }
        if ($slug !== null) {
            $slug = self::decodePathSegment($slug);
        }

        if ($section !== null && !self::isValidName($section)) {
            return null;
        }
        if ($slug !== null && !self::isValidName($slug)) {
            return null;
        }

        if ($section === null && $slug === null) {
            return null;
        }

        if ($section !== null && $slug !== null) {
            foreach ($index['sections'] as $sec) {
                if ($sec['dirName'] !== $section) {
                    continue;
                }
                foreach ($sec['documents'] as $doc) {
                    if ($doc['slug'] === $slug) {
                        return self::loadDocument($doc);
                    }
                }
            }
            return null;
        }

        // Single segment: root document or section-only is handled by TermsRoute
        $segment = $section ?? $slug;
        if ($segment === null) {
            return null;
        }

        foreach ($index['rootDocs'] as $doc) {
            if ($doc['slug'] === $segment) {
                return self::loadDocument($doc);
            }
        }

        return null;
    }

    /**
     * @param list<string> $pathSegments directory names from terms root
     * @return array{title: string, url: string, documents: list<array>, categories: list<array>}|null
     */
    public static function resolveCategoryByPath(array $pathSegments): ?array
    {
        if ($pathSegments === []) {
            return null;
        }

        $termsReal = realpath(self::termsDir());
        if ($termsReal === false) {
            return null;
        }

        $dir = $termsReal;
        foreach ($pathSegments as $segment) {
            if (!self::isValidName($segment)) {
                return null;
            }
            $dir = $dir . DIRECTORY_SEPARATOR . $segment;
            if (!is_dir($dir)) {
                return null;
            }
            $dir = realpath($dir);
            if ($dir === false || !str_starts_with($dir, $termsReal . DIRECTORY_SEPARATOR)) {
                return null;
            }
        }

        $url = self::buildCategoryUrl($pathSegments);
        $contents = self::scanDirectoryLevel($dir, $termsReal, $pathSegments, $url);

        if ($contents['documents'] === [] && $contents['categories'] === []) {
            return null;
        }

        return [
            'title' => $pathSegments[array_key_last($pathSegments)],
            'url' => $url,
            'documents' => $contents['documents'],
            'categories' => $contents['categories'],
        ];
    }

    /**
     * @param list<string> $pathSegments
     */
    public static function resolveDocumentByPath(array $pathSegments): ?TermsDocument
    {
        if (count($pathSegments) < 1) {
            return null;
        }

        $slug = array_pop($pathSegments);
        if (!self::isValidName($slug)) {
            return null;
        }

        if ($pathSegments === []) {
            return self::resolve(null, $slug);
        }

        $section = $pathSegments[0];
        if (count($pathSegments) === 1) {
            return self::resolve($section, $slug);
        }

        $termsReal = realpath(self::termsDir());
        if ($termsReal === false) {
            return null;
        }

        $dir = $termsReal;
        foreach ($pathSegments as $segment) {
            if (!self::isValidName($segment)) {
                return null;
            }
            $dir = $dir . DIRECTORY_SEPARATOR . $segment;
            if (!is_dir($dir)) {
                return null;
            }
            $dir = realpath($dir);
            if ($dir === false) {
                return null;
            }
        }

        $filePath = $dir . DIRECTORY_SEPARATOR . $slug . '.md';
        $safePath = self::safeFilePath($termsReal, $filePath);
        if ($safePath === null) {
            return null;
        }

        $sectionKey = implode('/', $pathSegments);

        return self::loadDocument(
            self::docMeta($sectionKey, $slug, $safePath, $slug . '.md')
        );
    }

    /**
     * @return array{documents: list<array>, categories: list<array>}
     */
    public static function hubContents(): array
    {
        $index = self::scan();

        return [
            'documents' => $index['rootDocs'],
            'categories' => array_map(static fn (array $s) => [
                'title' => $s['title'],
                'url' => $s['url'],
                'dirName' => $s['dirName'],
            ], $index['sections']),
        ];
    }

    /**
     * Full tree for API clients: categories with nested children; documents include contentHtml unless $treeOnly.
     *
     * @return array{type: string, title: string, url: string, children: list<array>}
     */
    public static function buildApiTree(bool $treeOnly = false): array
    {
        $termsReal = realpath(self::termsDir());
        if ($termsReal === false || !is_dir($termsReal)) {
            return self::absolutizeApiTreeUrls([
                'type' => 'root',
                'title' => '',
                'url' => '/terms',
                'children' => [],
            ]);
        }

        return self::absolutizeApiTreeUrls([
            'type' => 'root',
            'title' => '',
            'url' => '/terms',
            'children' => self::buildDirectoryTree($termsReal, $termsReal, [], $treeOnly),
        ]);
    }

    /**
     * @param array{type: string, title: string, url: string, children?: list<array>} $node
     * @return array{type: string, title: string, url: string, children?: list<array>}
     */
    private static function absolutizeApiTreeUrls(array $node): array
    {
        if (isset($node['url']) && is_string($node['url']) && $node['url'] !== '') {
            $node['url'] = ShmUtils::url($node['url']);
        }

        if (!empty($node['children'])) {
            $node['children'] = array_map(
                static fn (array $child): array => self::absolutizeApiTreeUrls($child),
                $node['children']
            );
        }

        return $node;
    }

    /**
     * Find category or document in the API tree where title or slug equals $q.
     * Returns the matched node (with its children) as a new tree root, or null.
     */
    public static function findApiTreeByQuery(string $q, bool $treeOnly = false): ?array
    {
        $q = trim($q);
        if ($q === '') {
            return null;
        }

        $tree = self::buildApiTree($treeOnly);

        return self::findApiTreeNode($tree, $q);
    }

    /**
     * @param array{type: string, title: string, url: string, children?: list<array>, slug?: string} $node
     */
    private static function findApiTreeNode(array $node, string $q): ?array
    {
        if (self::apiNodeMatchesQuery($node, $q)) {
            return $node;
        }

        foreach ($node['children'] ?? [] as $child) {
            $found = self::findApiTreeNode($child, $q);
            if ($found !== null) {
                return $found;
            }
        }

        return null;
    }

    /**
     * @param array{type: string, title: string, slug?: string} $node
     */
    private static function apiNodeMatchesQuery(array $node, string $q): bool
    {
        if ($node['type'] === 'root') {
            return false;
        }

        if (($node['title'] ?? '') === $q) {
            return true;
        }

        return $node['type'] === 'document' && ($node['slug'] ?? '') === $q;
    }

    /**
     * @param list<string> $pathSegments
     * @return list<array>
     */
    private static function buildDirectoryTree(string $dirReal, string $termsReal, array $pathSegments, bool $treeOnly = false): array
    {
        $nodes = [];
        $entries = scandir($dirReal);
        if ($entries === false) {
            return [];
        }

        $sectionKey = $pathSegments === [] ? null : implode('/', $pathSegments);

        foreach ($entries as $entry) {
            if ($entry === '.' || $entry === '..' || str_starts_with($entry, '.')) {
                continue;
            }

            $fullPath = $dirReal . DIRECTORY_SEPARATOR . $entry;

            if (is_file($fullPath) && str_ends_with(strtolower($entry), '.md')) {
                $docSlug = self::slugFromFilename($entry);
                if ($docSlug === null) {
                    continue;
                }
                $safePath = self::safeFilePath($termsReal, $fullPath);
                if ($safePath === null) {
                    continue;
                }
                $meta = self::docMeta($sectionKey, $docSlug, $safePath, $entry);
                $node = [
                    'type' => 'document',
                    'title' => $meta['title'],
                    'url' => $meta['url'],
                    'slug' => $meta['slug'],
                ];
                if (!$treeOnly) {
                    $markdown = file_get_contents($safePath);
                    $node['contentHtml'] = self::markdownToHtml($markdown !== false ? $markdown : '');
                }
                $nodes[] = $node;
                continue;
            }

            if (!is_dir($fullPath) || !self::isValidName($entry)) {
                continue;
            }

            $subReal = realpath($fullPath);
            if ($subReal === false || !str_starts_with($subReal, $termsReal . DIRECTORY_SEPARATOR)) {
                continue;
            }

            $subSegments = [...$pathSegments, $entry];
            $childNodes = self::buildDirectoryTree($subReal, $termsReal, $subSegments, $treeOnly);
            if ($childNodes === []) {
                continue;
            }

            $nodes[] = [
                'type' => 'category',
                'title' => $entry,
                'url' => self::buildCategoryUrl($subSegments),
                'children' => $childNodes,
            ];
        }

        usort($nodes, static function (array $a, array $b): int {
            $order = ['category' => 0, 'document' => 1];
            $ta = $order[$a['type']] ?? 2;
            $tb = $order[$b['type']] ?? 2;
            if ($ta !== $tb) {
                return $ta <=> $tb;
            }

            return strcmp($a['title'], $b['title']);
        });

        return $nodes;
    }

    /**
     * @param list<string> $pathSegments
     */
    public static function buildCategoryUrl(array $pathSegments): string
    {
        return '/terms/' . implode('/', array_map('rawurlencode', $pathSegments));
    }

    /**
     * @param list<string> $pathSegments path from terms root (for doc meta section key)
     * @return array{documents: list<array>, categories: list<array>}
     */
    private static function scanDirectoryLevel(
        string $dirReal,
        string $termsReal,
        array $pathSegments,
        string $categoryUrl
    ): array {
        $documents = [];
        $categories = [];
        $entries = scandir($dirReal);
        if ($entries === false) {
            return ['documents' => [], 'categories' => []];
        }

        $sectionKey = $pathSegments === [] ? null : implode('/', $pathSegments);

        foreach ($entries as $entry) {
            if ($entry === '.' || $entry === '..' || str_starts_with($entry, '.')) {
                continue;
            }

            $fullPath = $dirReal . DIRECTORY_SEPARATOR . $entry;

            if (is_file($fullPath) && str_ends_with(strtolower($entry), '.md')) {
                $docSlug = self::slugFromFilename($entry);
                if ($docSlug === null) {
                    continue;
                }
                $safePath = self::safeFilePath($termsReal, $fullPath);
                if ($safePath === null) {
                    continue;
                }
                $documents[] = self::docMeta($sectionKey, $docSlug, $safePath, $entry);
                continue;
            }

            if (!is_dir($fullPath)) {
                continue;
            }

            if (!self::isValidName($entry)) {
                continue;
            }

            $subReal = realpath($fullPath);
            if ($subReal === false || !str_starts_with($subReal, $termsReal . DIRECTORY_SEPARATOR)) {
                continue;
            }

            $subSegments = [...$pathSegments, $entry];
            $categories[] = [
                'dirName' => $entry,
                'title' => $entry,
                'url' => self::buildCategoryUrl($subSegments),
            ];
        }

        usort($documents, fn ($a, $b) => strcmp($a['title'], $b['title']));
        usort($categories, fn ($a, $b) => strcmp($a['title'], $b['title']));

        return ['documents' => $documents, 'categories' => $categories];
    }

    public static function loadDocument(array $meta): TermsDocument
    {
        return new TermsDocument(
            section: $meta['section'],
            slug: $meta['slug'],
            fileName: $meta['fileName'],
            title: $meta['title'],
            filePath: $meta['filePath'],
            url: $meta['url'],
        );
    }

    public static function markdownToHtml(string $markdown): string
    {
        if (self::$converter === null) {
            self::$converter = new GithubFlavoredMarkdownConverter([
                'html_input' => 'strip',
                'allow_unsafe_links' => false,
            ]);
        }

        return self::$converter->convert($markdown)->getContent();
    }

    public static function readDocumentHtml(TermsDocument $document): string
    {
        $content = file_get_contents($document->filePath);
        if ($content === false) {
            return '';
        }
        return self::markdownToHtml($content);
    }

    /**
     * @param string $currentUrl
     * @return array{rootDocs: list<array>, sections: list<array>}
     */
    public static function buildNav(string $currentUrl): array
    {
        $index = self::scan();
        $nav = [
            'rootDocs' => [],
            'sections' => [],
        ];

        foreach ($index['rootDocs'] as $doc) {
            $nav['rootDocs'][] = array_merge($doc, [
                'active' => $currentUrl === $doc['url'],
            ]);
        }

        foreach ($index['sections'] as $section) {
            $documents = [];
            foreach ($section['documents'] as $doc) {
                $documents[] = array_merge($doc, [
                    'active' => $currentUrl === $doc['url'],
                ]);
            }
            $sectionUrl = $section['url'] ?? self::buildCategoryUrl([$section['dirName']]);
            $sectionActive = $currentUrl === $sectionUrl
                || str_starts_with($currentUrl, $sectionUrl . '/');

            $nav['sections'][] = [
                'dirName' => $section['dirName'],
                'title' => $section['title'],
                'url' => $sectionUrl,
                'active' => $sectionActive && $currentUrl === $sectionUrl,
                'documents' => $documents,
            ];
        }

        return $nav;
    }

    private static function docMeta(?string $section, string $slug, string $filePath, string $fileName): array
    {
        return [
            'section' => $section,
            'slug' => $slug,
            'fileName' => $fileName,
            'title' => self::displayTitle($fileName),
            'filePath' => $filePath,
            'url' => self::buildUrl($section, $slug),
        ];
    }

    /** Visible name: filename without .md (case unchanged). */
    public static function displayTitle(string $fileName): string
    {
        if (str_ends_with(strtolower($fileName), '.md')) {
            return substr($fileName, 0, -3);
        }

        return $fileName;
    }

    /**
     * Whether the path has at least one document for PDF export (no markdown parsing).
     *
     * @param list<string>|null $pathSegments path after /terms/ (null = all documents)
     */
    public static function hasPdfEntries(?array $pathSegments = null): bool
    {
        $entries = self::collectPdfEntries($pathSegments, true);

        return $entries !== null && $entries !== [];
    }

    /**
     * Flat list of documents for PDF export (tree order, with category path).
     *
     * @param list<string>|null $pathSegments path after /terms/ (null = all documents)
     * @return list<array{category: ?string, title: string, contentHtml: string}>|null null if path not found
     */
    public static function collectPdfEntries(?array $pathSegments = null, bool $treeOnly = false): ?array
    {
        $termsReal = realpath(self::termsDir());
        if ($termsReal === false || !is_dir($termsReal)) {
            return $pathSegments === null || $pathSegments === [] ? [] : null;
        }

        if ($pathSegments === null || $pathSegments === []) {
            $entries = [];
            $nodes = self::buildDirectoryTree($termsReal, $termsReal, [], $treeOnly);
            self::flattenPdfEntries($nodes, null, $entries);

            return $entries;
        }

        $document = self::resolveDocumentByPath($pathSegments);
        if ($document !== null) {
            $categoryPath = null;
            if ($document->section !== null && $document->section !== '') {
                $categoryPath = str_replace('/', ' / ', $document->section);
            }

            return [[
                'category' => $categoryPath,
                'title' => $document->title,
                'contentHtml' => $treeOnly ? '' : self::readDocumentHtml($document),
            ]];
        }

        $dir = $termsReal;
        foreach ($pathSegments as $segment) {
            if (!self::isValidName($segment)) {
                return null;
            }
            $dir = $dir . DIRECTORY_SEPARATOR . $segment;
            if (!is_dir($dir)) {
                return null;
            }
            $dir = realpath($dir);
            if ($dir === false || !str_starts_with($dir, $termsReal . DIRECTORY_SEPARATOR)) {
                return null;
            }
        }

        $entries = [];
        $nodes = self::buildDirectoryTree($dir, $termsReal, $pathSegments, $treeOnly);
        $categoryLabel = implode(' / ', $pathSegments);
        self::flattenPdfEntries($nodes, $categoryLabel, $entries);

        return $entries;
    }

    /**
     * @param list<array> $nodes
     * @param list<array{category: ?string, title: string, contentHtml: string}> $entries
     */
    private static function flattenPdfEntries(array $nodes, ?string $categoryPath, array &$entries): void
    {
        foreach ($nodes as $node) {
            if (($node['type'] ?? '') === 'document') {
                $entries[] = [
                    'category' => $categoryPath,
                    'title' => $node['title'] ?? '',
                    'contentHtml' => $node['contentHtml'] ?? '',
                ];
                continue;
            }

            if (($node['type'] ?? '') !== 'category') {
                continue;
            }

            $title = $node['title'] ?? '';
            $path = $categoryPath === null || $categoryPath === ''
                ? $title
                : $categoryPath . ' / ' . $title;

            self::flattenPdfEntries($node['children'] ?? [], $path, $entries);
        }
    }

    /**
     * @return list<array>
     */
    public static function allDocuments(): array
    {
        $index = self::scan();
        $all = $index['rootDocs'];
        foreach ($index['sections'] as $section) {
            foreach ($section['documents'] as $doc) {
                $all[] = array_merge($doc, [
                    'sectionTitle' => $section['title'],
                ]);
            }
        }
        usort($all, fn ($a, $b) => strcmp($a['title'], $b['title']));

        return $all;
    }

    public static function buildUrl(?string $section, string $basename): string
    {
        if ($section === null || $section === '') {
            return '/terms/' . rawurlencode($basename);
        }

        $parts = explode('/', $section);

        return self::buildCategoryUrl($parts) . '/' . rawurlencode($basename);
    }

    public static function decodePathSegment(string $segment): string
    {
        return rawurldecode($segment);
    }

    private static function slugFromFilename(string $filename): ?string
    {
        if (!str_ends_with(strtolower($filename), '.md')) {
            return null;
        }
        $slug = substr($filename, 0, -3);
        return self::isValidName($slug) ? $slug : null;
    }

    private static function isValidName(string $name): bool
    {
        if ($name === '' || $name === '.' || $name === '..') {
            return false;
        }
        if (str_contains($name, '/') || str_contains($name, '\\') || str_contains($name, "\0")) {
            return false;
        }
        return true;
    }

    private static function safeFilePath(string $termsReal, string $candidatePath): ?string
    {
        $real = realpath($candidatePath);
        if ($real === false || !is_file($real)) {
            return null;
        }
        if (!str_starts_with($real, $termsReal . DIRECTORY_SEPARATOR)) {
            return null;
        }
        return $real;
    }
}
