<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\Action;

/**
 * What `describe_admin_action` tells the caller about reversibility BEFORE the action runs.
 * `cannotUndo` carries the weight: a cache flush, a consumed auto-increment id or a sent mail
 * survives any rollback, and naming them up front is the difference between an honest tool and one
 * that promises an undo it cannot deliver.
 */
final class UndoSpec
{
    /**
     * What `$cannotUndo` resolves to, and what a tool response carries — the same choice
     * {@see ActionDescriber} and {@see ActionInvocation} already make for `title`.
     */
    public const DEFAULT_LANGUAGE = 'en';

    /** The key an unlocalised (plain-list) `cannotUndo:` is stored under. */
    private const UNLOCALISED = '';

    /**
     * @param list<string>                $tables
     * @param list<string>                $cannotUndo          resolved for {@see self::DEFAULT_LANGUAGE}
     * @param array<string,list<string>>  $cannotUndoByLanguage language code => reasons, or the
     *                                                         single {@see self::UNLOCALISED} key
     */
    private function __construct(
        public readonly bool $supported,
        public readonly array $tables,
        public readonly array $cannotUndo,
        private readonly array $cannotUndoByLanguage,
    ) {
    }

    /**
     * @param array<mixed> $raw
     */
    public static function fromArray(array $raw): self
    {
        $byLanguage = self::localised($raw['cannotUndo'] ?? null);

        return new self(
            (bool)($raw['supported'] ?? false),
            self::strings($raw['tables'] ?? null),
            self::resolve($byLanguage, self::DEFAULT_LANGUAGE),
            $byLanguage,
        );
    }

    /**
     * The reasons in the caller's language, falling back rather than answering nothing: an author
     * who wrote only German still tells an English caller *something*, and "this cannot be undone,
     * reason unavailable" is the one answer this list must never give.
     *
     * @return list<string>
     */
    public function cannotUndoFor(string $language): array
    {
        return self::resolve($this->cannotUndoByLanguage, $language);
    }

    /**
     * Two shapes, because the catalog is customer-extensible and the plain list predates the map:
     *
     *     cannotUndo: ["auto-increment kRedirect"]          # unlocalised, served to everyone
     *     cannotUndo: {de: ["…"], en: ["…"]}                # localised, de + en both required
     *
     * @return array<string,list<string>>
     */
    private static function localised(mixed $value): array
    {
        if (!\is_array($value) || $value === []) {
            return [];
        }
        if (\array_is_list($value)) {
            return [self::UNLOCALISED => self::strings($value)];
        }
        $out = [];
        foreach ($value as $language => $reasons) {
            if (\is_string($language) && $language !== '') {
                $out[$language] = self::strings($reasons);
            }
        }

        return $out;
    }

    /**
     * @param array<string,list<string>> $byLanguage
     * @return list<string>
     */
    private static function resolve(array $byLanguage, string $language): array
    {
        $first = \reset($byLanguage);

        return $byLanguage[$language]
            ?? $byLanguage[self::UNLOCALISED]
            ?? $byLanguage[self::DEFAULT_LANGUAGE]
            ?? ($first === false ? [] : $first);
    }

    /** @return list<string> */
    private static function strings(mixed $value): array
    {
        $out = [];
        foreach (\is_array($value) ? $value : [] as $item) {
            if (\is_string($item)) {
                $out[] = $item;
            }
        }

        return $out;
    }
}
