<?php

namespace Acms\Services\Validator;

use Acms\Services\Common\ValidatorFactory;
use Acms\Services\Validator\Attribute\AsValidationOption;
use ReflectionClass;
use ReflectionMethod;

/**
 * 管理画面（フォーム / フォーム2）のバリデーション選択肢を組み立てるサービス。
 *
 * 登録済みバリデータ（system / user / custom / プラグイン）のメソッドに付与された
 * #[AsValidationOption] 属性を収集し、バリデータメソッドに対応しない特別オプション
 * （filesize / converter / duplication）をマージして、グループ単位で返す。
 */
class OptionProvider
{
    /**
     * バリデータメソッドに対応しない、フォーム処理側の特別オプション。
     * フォーム・フォーム2 のどちらでも選択肢に出す。
     *
     * @var array<int, array{key: string, label: string, group: string, hasValue: bool}>
     */
    private const SPECIAL_OPTIONS = [
        ['key' => 'filesize', 'label' => 'ファイルサイズ上限値（KB）', 'group' => '入力値の制限', 'hasValue' => true],
        ['key' => 'converter', 'label' => '入力の変換', 'group' => 'その他', 'hasValue' => true],
        ['key' => 'duplication', 'label' => 'メールアドレスの重複禁止', 'group' => 'その他', 'hasValue' => false],
    ];

    /**
     * 既知グループの表示順。グループ名は日本語ラベルそのものを使う。
     * ここに無いグループ（拡張バリデータ独自など）は選択肢一覧の末尾に出現順で並ぶ。
     *
     * @var array<int, string>
     */
    private const GROUP_ORDER = [
        '入力値の制限',
        '形式チェック',
        'その他',
    ];

    /**
     * バリデータの処理順（小さいほど先に表示。後勝ちでメタデータを上書きするため、
     * 実行時の解決順 custom > user > system に合わせて system を先頭で処理する）。
     *
     * @var array<string, int>
     */
    private const SOURCE_ORDER = [
        'AcmsSystem' => 0,
        'AcmsUser' => 1,
        'AcmsCustom' => 2,
    ];

    /**
     * @var array<string, object> ns => バリデータインスタンス
     */
    private array $validators;

    /**
     * @param array<string, object>|null $validators ns => バリデータインスタンス。未指定なら ValidatorFactory から取得する。
     */
    public function __construct(?array $validators = null)
    {
        $this->validators = $validators ?? ValidatorFactory::singleton()->getCollection();
    }

    /**
     * バリデーション選択肢を、グループ単位で返す。
     *
     * @return array<int, array{group: string, label: string, options: array<int, array{key: string, label: string, hasValue: bool}>}>
     */
    public function getGroupedOptions(): array
    {
        $options = $this->collectOptions();

        /** @var array<string, array<int, array{key: string, label: string, hasValue: bool}>> $byGroup */
        $byGroup = [];
        foreach ($options as $option) {
            $byGroup[$option['group']][] = [
                'key' => $option['key'],
                'label' => gettext($option['label']),
                'hasValue' => $option['hasValue'],
            ];
        }

        $result = [];
        foreach (self::GROUP_ORDER as $group) {
            if (!isset($byGroup[$group])) {
                continue;
            }
            $result[] = [
                'group' => $group,
                'label' => gettext($group),
                'options' => $byGroup[$group],
            ];
            unset($byGroup[$group]);
        }
        // 既知グループ以外（拡張バリデータ独自など）は末尾に出現順で並べる
        foreach ($byGroup as $group => $groupOptions) {
            $result[] = [
                'group' => $group,
                'label' => gettext($group),
                'options' => $groupOptions,
            ];
        }

        return $result;
    }

    /**
     * 全バリデータの属性収集と特別オプションのマージを行い、表示順を保った一覧を返す。
     *
     * @return array<int, array{key: string, label: string, group: string, hasValue: bool}>
     */
    private function collectOptions(): array
    {
        /** @var array<string, array{key: string, label: string, group: string, hasValue: bool}> $options */
        $options = [];

        foreach ($this->sortedValidators() as $validator) {
            foreach ($this->extractFromValidator($validator) as $option) {
                // 既存キーへの再代入は挿入位置を維持する（後勝ちでメタデータのみ上書き）
                $options[$option['key']] = $option;
            }
        }

        foreach (self::SPECIAL_OPTIONS as $special) {
            $options[$special['key']] = $special;
        }

        return array_values($options);
    }

    /**
     * バリデータを処理順（system → user → custom → 拡張フック追加分）に並べ替えて返す。
     * SOURCE_ORDER に無い ns（拡張アプリがフックで attach したバリデータ）は
     * rank 100+ として末尾に attach 順で並ぶ。
     *
     * @return array<int, object>
     */
    private function sortedValidators(): array
    {
        $entries = [];
        $index = 0;
        foreach ($this->validators as $ns => $validator) {
            $entries[] = [
                'rank' => self::SOURCE_ORDER[$ns] ?? (100 + $index),
                'index' => $index,
                'validator' => $validator,
            ];
            $index++;
        }
        usort($entries, static function (array $a, array $b): int {
            return [$a['rank'], $a['index']] <=> [$b['rank'], $b['index']];
        });

        return array_map(static fn(array $entry): object => $entry['validator'], $entries);
    }

    /**
     * 1 つのバリデータから #[AsValidationOption] 付きメソッドを抽出する。
     *
     * @param object $validator
     * @return array<int, array{key: string, label: string, group: string, hasValue: bool}>
     */
    private function extractFromValidator(object $validator): array
    {
        $options = [];
        $reflection = new ReflectionClass($validator);
        foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
            $attributes = $method->getAttributes(AsValidationOption::class);
            if ($attributes === []) {
                continue;
            }
            /** @var AsValidationOption $meta */
            $meta = $attributes[0]->newInstance();
            $options[] = [
                'key' => $method->getName(),
                'label' => $meta->label,
                'group' => $meta->group,
                // 値欄（バリデーション引数）の要否は、第2引数以降を取るかで自動判定する
                'hasValue' => $method->getNumberOfParameters() > 1,
            ];
        }

        return $options;
    }
}
