<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\Catalog;

use RuntimeException;
use Symfony\Component\Yaml\Yaml;

/**
 * The hand-written half of the schema: which domain a table belongs to, what an LLM needs to know
 * about it, and a couple of queries worth copying.
 *
 * The database supplies names, types and row counts; nothing in it says `tversandartzahlungsart`
 * is shipping. That is what `catalog/table-domains.yaml` is for, and this class is its only reader.
 *
 * Pure: no DB, no shop container. Unknown tables resolve to {@see self::UNMAPPED} rather than to a
 * guess — a JTL release that adds tables degrades, it does not break.
 */
final class TableCatalog
{
    public const UNMAPPED = 'unmapped';
    public const INTERNAL = 'internal';

    /** @var array<string,string> lower-cased table => domain */
    private array $byTable = [];

    /** @var list<array{0:string,1:string}> [domain, PCRE] in the file's domain order */
    private array $byPattern = [];

    /** @var array<string,string> domain => one-line summary */
    private array $summaries = [];

    /** @var array<string,string> lower-cased table => note */
    private array $notes = [];

    /** @var array<string,list<array{title:string,sql:string}>> lower-cased table => queries */
    private array $samples = [];

    private function __construct()
    {
    }

    public static function fromFile(string $path): self
    {
        if (!\is_file($path)) {
            throw new RuntimeException('table-domains.yaml not found: ' . $path);
        }
        $parsed = Yaml::parseFile($path);

        return self::fromArray(\is_array($parsed) ? $parsed : []);
    }

    /**
     * @param array<mixed> $parsed
     */
    public static function fromArray(array $parsed): self
    {
        $self    = new self();
        $domains = $parsed['domains'] ?? [];
        if (\is_array($domains)) {
            foreach ($domains as $domain => $spec) {
                $self->addDomain((string)$domain, \is_array($spec) ? $spec : []);
            }
        }
        foreach ((array)($parsed['notes'] ?? []) as $table => $note) {
            if (\is_string($note)) {
                $self->notes[self::key((string)$table)] = \trim($note);
            }
        }
        foreach ((array)($parsed['sampleQueries'] ?? []) as $table => $queries) {
            $self->addSamples((string)$table, \is_array($queries) ? $queries : []);
        }

        return $self;
    }

    /** @return list<string> domain names, in catalog order */
    public function domains(): array
    {
        return \array_keys($this->summaries);
    }

    public function summary(string $domain): ?string
    {
        return $this->summaries[$domain] ?? null;
    }

    /**
     * Exact names win over patterns, and patterns are tried in catalog order — see the ordering
     * note at the top of the YAML, which several assignments depend on.
     */
    public function domain(string $table): string
    {
        $key = self::key($table);
        if (isset($this->byTable[$key])) {
            return $this->byTable[$key];
        }
        foreach ($this->byPattern as [$domain, $regex]) {
            if (\preg_match($regex, $key) === 1) {
                return $domain;
            }
        }

        return self::UNMAPPED;
    }

    public function note(string $table): ?string
    {
        return $this->notes[self::key($table)] ?? null;
    }

    /** @return list<array{title:string,sql:string}> */
    public function sampleQueries(string $table): array
    {
        return $this->samples[self::key($table)] ?? [];
    }

    /**
     * @param array<mixed> $spec
     */
    private function addDomain(string $domain, array $spec): void
    {
        $this->summaries[$domain] = \is_string($spec['summary'] ?? null) ? \trim($spec['summary']) : '';
        foreach ((array)($spec['tables'] ?? []) as $table) {
            $this->byTable[self::key((string)$table)] ??= $domain;
        }
        foreach ((array)($spec['patterns'] ?? []) as $pattern) {
            $this->byPattern[] = [$domain, self::toRegex((string)$pattern)];
        }
    }

    /**
     * @param array<mixed> $queries
     */
    private function addSamples(string $table, array $queries): void
    {
        $list = [];
        foreach ($queries as $query) {
            if (!\is_array($query) || !\is_string($query['sql'] ?? null)) {
                continue;
            }
            $list[] = [
                'title' => \trim((string)($query['title'] ?? '')),
                'sql'   => \trim($query['sql']),
            ];
        }
        if ($list !== []) {
            $this->samples[self::key($table)] = $list;
        }
    }

    /** `*` matches any run of table-name characters; everything else is literal. */
    private static function toRegex(string $pattern): string
    {
        return '/^' . \str_replace('\*', '[a-z0-9_]*', \preg_quote(self::key($pattern), '/')) . '$/';
    }

    private static function key(string $table): string
    {
        return \mb_convert_case(\trim($table), \MB_CASE_LOWER);
    }
}
