<?php

declare(strict_types=1);

namespace Acms\Services\Installer;

/**
 * インストーラーの初期テーマ選択肢のカタログ（W8＋外部拡張ポイント）。
 *
 * 現行の install はテーマ選択肢を install.html に静的直書きし、`select_theme` を
 * ホワイトリスト検証せず `bin/<name>/` パス組立に使うため、任意の bin 名を差し込める。
 * 本カタログは「宣言（key/label/image 等）の集合」から候補一覧とホワイトリストを組み立て、
 * ビュー描画と選択値検証を一元化する。
 *
 * 宣言の収集元（コア既定テーマ＋`setup/bin/*` の宣言ファイル走査）はファイル I/O のため
 * 呼び出し側（Phase D の収集器）に置き、本クラスは配列 → 値オブジェクトへの純粋変換・整列・
 * 検証のみを担う。プラグインは `setup/bin/<name>/` に宣言ファイルを置くだけで候補を追加でき、
 * DB もコア改修も不要（setup 実行時はプラグインの ServiceProvider/Hook が発火しないため、
 * 駆動源はファイルシステム宣言である必要がある）。
 */
final class InstallThemeCatalog
{
    /**
     * @param list<InstallTheme> $themes
     */
    public function __construct(private readonly array $themes)
    {
    }

    /**
     * 宣言配列（各要素は theme.yaml 由来の連想配列）からカタログを構築する。
     * `key` または `label` を欠く宣言は候補にしない。
     *
     * @param list<array<string, mixed>> $declarations
     */
    public static function fromDeclarations(array $declarations): self
    {
        $themes = [];
        foreach ($declarations as $declaration) {
            $theme = self::toTheme($declaration);
            if ($theme !== null) {
                $themes[] = $theme;
            }
        }

        return new self($themes);
    }

    /**
     * 表示対象（hidden 以外）を order 昇順・同 order は宣言順で返す。
     *
     * @return list<InstallTheme>
     */
    public function visibleThemes(): array
    {
        $visible = array_values(array_filter(
            $this->themes,
            static fn (InstallTheme $theme): bool => !$theme->hidden
        ));

        // usort は安定ではないため、元の宣言順を tiebreaker に添えて安定ソートにする。
        $indexed = [];
        foreach ($visible as $index => $theme) {
            $indexed[] = [$index, $theme];
        }
        usort($indexed, static function (array $a, array $b): int {
            return [$a[1]->order, $a[0]] <=> [$b[1]->order, $b[0]];
        });

        return array_map(static fn (array $pair): InstallTheme => $pair[1], $indexed);
    }

    /**
     * 選択可能な key（＝ホワイトリスト。hidden は含めない）。
     *
     * @return list<string>
     */
    public function allowedKeys(): array
    {
        return array_map(static fn (InstallTheme $theme): string => $theme->key, $this->visibleThemes());
    }

    /**
     * 送信された select_theme 値が選択肢に含まれるか（W8 検証）。
     */
    public function isAllowed(string $key): bool
    {
        return in_array($key, $this->allowedKeys(), true);
    }

    public function find(string $key): ?InstallTheme
    {
        foreach ($this->themes as $theme) {
            if ($theme->key === $key) {
                return $theme;
            }
        }

        return null;
    }

    /**
     * @param array<string, mixed> $declaration
     */
    private static function toTheme(array $declaration): ?InstallTheme
    {
        $key = isset($declaration['key']) && is_string($declaration['key'])
            ? trim($declaration['key'])
            : '';
        $label = isset($declaration['label']) && is_string($declaration['label'])
            ? $declaration['label']
            : '';
        if ($key === '' || $label === '') {
            return null;
        }

        return new InstallTheme(
            key: $key,
            label: $label,
            description: isset($declaration['description']) && is_string($declaration['description'])
                ? $declaration['description']
                : '',
            image: isset($declaration['image']) && is_string($declaration['image'])
                ? $declaration['image']
                : '',
            order: isset($declaration['order']) && is_int($declaration['order'])
                ? $declaration['order']
                : 100,
            hidden: (bool)($declaration['hidden'] ?? false),
            comparison: self::normalizeComparison($declaration['comparison'] ?? null),
        );
    }

    /**
     * 宣言の `comparison`（属性ラベル → 値）を array<string, string> に正規化する。
     *
     * @return array<string, string>
     */
    private static function normalizeComparison(mixed $raw): array
    {
        if (!is_array($raw)) {
            return [];
        }
        $comparison = [];
        foreach ($raw as $attribute => $value) {
            if (is_string($attribute) && is_scalar($value)) {
                $comparison[$attribute] = (string)$value;
            }
        }

        return $comparison;
    }

    /**
     * 比較表に載せるテーマ（可視かつ comparison データを持つもの）を返す。
     *
     * @return list<InstallTheme>
     */
    public function comparisonThemes(): array
    {
        return array_values(array_filter(
            $this->visibleThemes(),
            static fn (InstallTheme $theme): bool => $theme->comparison !== []
        ));
    }

    /**
     * 比較表の属性ラベル一覧（comparisonThemes 横断の和集合を初出順で）。
     *
     * @return list<string>
     */
    public function comparisonAttributes(): array
    {
        $attributes = [];
        foreach ($this->comparisonThemes() as $theme) {
            foreach (array_keys($theme->comparison) as $attribute) {
                if (!in_array($attribute, $attributes, true)) {
                    $attributes[] = $attribute;
                }
            }
        }

        return $attributes;
    }
}
