<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\Action;

use RuntimeException;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Yaml\Exception\ParseException;

/**
 * The only reader of `catalog/actions/*.yaml`, and the counterpart of {@see
 * \Plugin\tnt_mcp\src\Mcp\Catalog\TableCatalog} on the write side.
 *
 * Pure by design: no DB, no shop container, no `Shop::` — the catalog has to be loadable in a unit
 * test and in a build-time evidence check, not only inside a booted shop.
 *
 * Loading is all-or-nothing. A partially valid catalog is never handed out, because the failure it
 * produces would surface as a mis-bound `$_POST` against a live shop rather than as a load error.
 * Ids are unique across the whole catalog, not per file — one flat namespace is what
 * `invoke_admin_action` addresses.
 */
final class ActionCatalog
{
    /** @var list<ActionSpec> in load order */
    private array $actions = [];

    /** @var array<string,ActionSpec> action id => spec */
    private array $byId = [];

    /** @var array<string,int> file path => declared schema version */
    private array $versions = [];

    /** @var array<string,string> file path => declared shop-version constraint */
    private array $compatible = [];

    private function __construct()
    {
    }

    /**
     * @throws CatalogValidationException
     */
    public static function fromFile(string $path): self
    {
        return self::fromFiles([$path]);
    }

    /**
     * Sorted so that load order — and therefore {@see self::ids()} — does not depend on the
     * filesystem.
     *
     * @throws CatalogValidationException
     */
    public static function fromDirectory(string $directory): self
    {
        if (!\is_dir($directory)) {
            throw new RuntimeException('action catalog directory not found: ' . $directory);
        }
        $paths = \glob(\rtrim($directory, '/') . '/*.yaml');
        if ($paths === false) {
            throw new RuntimeException('action catalog directory is unreadable: ' . $directory);
        }
        \sort($paths);

        return self::fromFiles($paths);
    }

    /** @return list<ActionSpec> */
    public function all(): array
    {
        return $this->actions;
    }

    public function get(string $id): ?ActionSpec
    {
        return $this->byId[$id] ?? null;
    }

    /** @return list<string> */
    public function ids(): array
    {
        return \array_keys($this->byId);
    }

    /** @return list<string> the files this catalog was loaded from, in load order */
    public function files(): array
    {
        return \array_keys($this->versions);
    }

    /** Null when $file is not part of this catalog. */
    public function version(string $file): ?int
    {
        return $this->versions[$file] ?? null;
    }

    /** Null when $file is not part of this catalog. */
    public function compatibleShopVersions(string $file): ?string
    {
        return $this->compatible[$file] ?? null;
    }

    /**
     * @param list<string> $paths
     *
     * @throws CatalogValidationException
     */
    private static function fromFiles(array $paths): self
    {
        $self      = new self();
        $validator = new ActionCatalogValidator();
        $errors    = [];
        $parsed    = [];

        foreach ($paths as $path) {
            if (!\is_file($path)) {
                throw new RuntimeException('action catalog file not found: ' . $path);
            }
            try {
                $content = Yaml::parseFile($path);
            } catch (ParseException $e) {
                $errors[] = CatalogValidationError::at($path, '', 'YAML parse error: ' . $e->getMessage());
                continue;
            }
            $parsed[$path] = $content;
            foreach ($validator->validate($path, $content) as $error) {
                $errors[] = $error;
            }
        }
        if ($errors !== []) {
            throw CatalogValidationException::fromErrors($errors);
        }
        foreach ($parsed as $path => $content) {
            $self->add((string)$path, \is_array($content) ? $content : []);
        }

        return $self;
    }

    /**
     * @param array<mixed> $content already validated
     */
    private function add(string $path, array $content): void
    {
        $this->versions[$path]   = (int)($content['version'] ?? 0);
        $this->compatible[$path] = \is_string($content['compatibleShopVersions'] ?? null)
            ? $content['compatibleShopVersions']
            : '';

        foreach (\is_array($content['actions'] ?? null) ? $content['actions'] : [] as $entry) {
            if (!\is_array($entry)) {
                continue;
            }
            $action                 = ActionSpec::fromArray($entry);
            $this->actions[]        = $action;
            $this->byId[$action->id] = $action;
        }
    }
}
