<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\Action;

use JTL\Language\LanguageHelper;

/**
 * {@see LanguageCodes} answered from the shop's own language tables.
 *
 * ## Why two sources
 *
 * The controllers that consume a `cName_<code>` field do not agree on how they enumerate languages:
 *
 * - `ShippingMethodsController.php:775, :924-927` — `LanguageHelper::getInstance()->gibInstallierteSprachen()`,
 *   i.e. active AND mapped to an installed ISO.
 * - `PaymentMethodsController.php:438-443` — `LanguageHelper::getAllLanguages(0, true, true)`, i.e.
 *   every active language, mapped or not.
 *
 * The allowlist is the union. Under-reporting would reject a key the shop would happily have
 * written — a localised name silently lost for one language, which is the failure mode this plugin's
 * catalog work exists to prevent. Over-reporting by one shop-known language is harmless: the
 * receiving controller iterates its own list and ignores anything outside it. What neither source
 * can produce is a code the caller made up, and that is the whole point.
 *
 * ## Why every failure is `null`
 *
 * The value is a security boundary, so "the database was unreachable" must not read as "the empty
 * set" (which would reject everything, an availability bug) nor be papered over with a default list
 * (which would accept `ger` on a shop that does not have it). It reads as "unknown", and
 * {@see ArgumentBinder} refuses to bind. The `class_exists()` probe is the same statement for the
 * unit-test case, where the shop autoloader may simply not be present.
 *
 * The result is memoised per instance: one bind can walk several localised fields
 * (`cName_{iso}`, `cGebuehrname_{iso}`, …) and none of them can change the answer mid-call.
 */
final class ShopLanguageCodes implements LanguageCodes
{
    /** @var list<string>|null */
    private ?array $codes = null;

    private bool $resolved = false;

    /**
     * @return list<string>|null
     */
    public function codes(): ?array
    {
        if ($this->resolved) {
            return $this->codes;
        }
        $this->resolved = true;
        $this->codes    = self::resolve();

        return $this->codes;
    }

    /**
     * @return list<string>|null
     */
    private static function resolve(): ?array
    {
        if (!\class_exists(LanguageHelper::class)) {
            return null;
        }
        try {
            $models = \array_merge(
                \array_values(LanguageHelper::getInstance()->gibInstallierteSprachen()),
                \array_values(LanguageHelper::getAllLanguages(0, true, true))
            );
        } catch (\Throwable) {
            return null;
        }
        $codes = [];
        foreach ($models as $model) {
            $code = $model->getCode();
            if ($code !== '' && !\in_array($code, $codes, true)) {
                $codes[] = $code;
            }
        }

        // Empty is indistinguishable from "the query returned nothing because something is wrong",
        // and a shop always has at least one language, so it is reported as unknown, not as a set.
        return $codes === [] ? null : $codes;
    }
}
