<?php

namespace Acms\Services\Template\Twig\Config;

use Acms\Services\Facades\Config;
use Acms\Traits\Utilities\FieldTrait;
use Field;
use Field_Validation;
use Twig\Markup;

/*
 * Field と Field_Validation はそれぞれ独立した static $aryField を持つ別シングルトンに
 * なっており、`Field::singleton('post')` と `Field_Validation::singleton('post')` は
 * **別インスタンス**を返す (Field.php:111 と Field.php:1149 をそれぞれ参照)。
 * ACMS の POST シングルトンは `Application::getPostParameter()` 内で
 * `Field_Validation::singleton('post')` に格納されているため、POST 値を読むときは
 * 必ず Field_Validation 側を参照する必要がある。
 */

/**
 * Twig 関数 acms_config() の戻り値となる値オブジェクト。
 *
 * 設定画面 Twig から「現在のスコープに対応する重ね合わせ Config Field」を扱う窓口。
 *
 * 設計方針:
 *  - Field オブジェクトは Twig に露出させない (どのメソッドも Field を返さない)
 *  - __toString / Stringable は実装しない (Twig の暗黙文字列化を避ける)
 *  - get / array / selected / checked は Field からの直取得 (軽量経路)
 *  - field / group / all / invalid は FieldTrait::buildFieldTrait の戻り値を返す
 *  - Field と built は同インスタンス内でのみ Lazy + memoize
 *
 * 値の取得は **POST 値 overload あり** をデフォルトとする (Symfony Form / Rails / Django と
 * 同じ「フォーム描画時は submitted 優先」哲学)。永続化済みの値だけ取りたい場合は
 * `cfg.persisted` で POST overload を行わない別アクセサを取得する。
 */
final class ConfigAccessor
{
    use FieldTrait;

    private const ENCODING = 'UTF-8';

    /**
     * @var array{ruleId?: int, moduleId?: int, configSetId?: int, scope?: string}
     */
    private array $opts;

    /**
     * persisted モード (POST overload を行わないモード) かどうか。
     */
    private bool $persistedMode = false;

    /**
     * @var Field|null
     */
    private ?Field $field = null;

    /**
     * @var array<string, mixed>|null
     */
    private ?array $built = null;

    /**
     * @var ConfigAccessor|null persisted モードの遅延キャッシュ
     */
    private ?ConfigAccessor $persistedCache = null;

    /**
     * @param array{ruleId?: int, moduleId?: int, configSetId?: int, scope?: string} $opts
     */
    public function __construct(array $opts = [])
    {
        $this->opts = $opts;
    }

    /**
     * 単一値を取得する (軽量経路)。
     *
     * @param string $key
     * @param mixed $default
     * @param int|null $index 配列値の特定要素を取りたい場合に指定
     * @return mixed
     */
    public function get(string $key, mixed $default = null, ?int $index = null): mixed
    {
        $field = $this->resolveField();
        if ($index !== null) {
            $arr = $field->getArray($key);
            return $arr[$index] ?? $default;
        }
        $value = $field->get($key, $default);
        return $value;
    }

    /**
     * 配列値を取得する (軽量経路)。
     *
     * @param string $key
     * @return array<int, mixed>
     */
    public function array(string $key): array
    {
        return $this->resolveField()->getArray($key);
    }

    /**
     * select 要素の selected 属性を出力する。
     *
     * 戻り値は Twig\Markup でラップしているため autoescape をバイパスする。
     *
     * @param string $key
     * @param string $value
     * @param int|null $index
     * @return Markup
     */
    public function selected(string $key, string $value, ?int $index = null): Markup
    {
        $current = (string) ($this->get($key, '', $index) ?? '');
        $html = $current === $value ? 'selected="selected"' : '';
        return new Markup($html, self::ENCODING);
    }

    /**
     * input[type=checkbox|radio] の checked 属性を出力する。
     *
     * @param string $key
     * @param string $value
     * @param int|null $index
     * @return Markup
     */
    public function checked(string $key, string $value, ?int $index = null): Markup
    {
        $current = (string) ($this->get($key, '', $index) ?? '');
        $html = $current === $value ? 'checked="checked"' : '';
        return new Markup($html, self::ENCODING);
    }

    /**
     * buildFieldTrait の 1 件ぶん (キー単位) を返す。
     *
     * 存在しないキーは ['value' => null, 'array' => []] を返す。
     *
     * @param string $key
     * @return array<string, mixed>
     */
    public function field(string $key): array
    {
        $built = $this->resolveBuilt();
        if (!isset($built[$key])) {
            return ['value' => null, 'array' => []];
        }
        $entry = $built[$key];
        return is_array($entry) ? $entry : ['value' => $entry, 'array' => [$entry]];
    }

    /**
     * @group 行ループの配列を返す。
     *
     * @param string $name
     * @return array<int, array<string, mixed>>
     */
    public function group(string $name): array
    {
        $built = $this->resolveBuilt();
        $loop = $built[$name] ?? [];
        return is_array($loop) ? $loop : [];
    }

    /**
     * buildFieldTrait の戻り値全体を返す。
     *
     * @return array<string, mixed>
     */
    public function all(): array
    {
        return $this->resolveBuilt();
    }

    /**
     * Field_Validation 経由のバリデーションエラーを問い合わせる。
     *
     * @param string $key
     * @param string|null $method 個別メソッドの失敗を見たい場合に指定 (例: 'required')
     * @param int|null $index グループ内 index
     * @return bool
     */
    public function invalid(string $key, ?string $method = null, ?int $index = null): bool
    {
        $built = $this->resolveBuilt();
        if (!isset($built[$key]) || !is_array($built[$key])) {
            return false;
        }
        $entry = $built[$key];
        if ($index !== null && isset($entry[$index]) && is_array($entry[$index])) {
            $entry = $entry[$index];
        }
        if ($method !== null) {
            return (bool) ($entry['v'][$method] ?? false);
        }
        return (bool) ($entry['invalid'] ?? false);
    }

    /**
     * POST から保存通知 (notice_mess) の生文字列を取得する。
     *
     * ACMS_POST_Config が保存後に redirect 経由で post field にセットする値で、
     * 保存後 GET の表示で利用する。値の例: 'saved', 'reset', '' (未保存) など。
     *
     * @return string
     */
    public function notice(): string
    {
        return Field_Validation::singleton('post')->get('notice_mess', '');
    }

    /**
     * 保存通知が表示対象かどうか (notice() が非空)。
     */
    public function saved(): bool
    {
        return $this->notice() !== '';
    }

    /**
     * 直近のリクエストが Config 保存往復だったか。
     *
     * POST シングルトンの 'config' サブフィールドが non-null なら true。
     */
    public function posted(): bool
    {
        $postConfig = Field_Validation::singleton('post')->getChild('config');
        return !$postConfig->isNull();
    }

    /**
     * Field 全体が valid か。
     *
     * Field_Validation の場合のみ実際にバリデーション結果を見る。通常 Field では常に true。
     */
    public function valid(): bool
    {
        $field = $this->resolveField();
        if ($field instanceof Field_Validation) {
            return $field->isValidAll();
        }
        return true;
    }

    /**
     * バリデーション失敗フィールド一覧 (フィールド名 → 失敗メソッド名のリスト)。
     *
     * 集計ロジックは Field_Validation::getInvalidMethods に閉じている (ドメイン側の
     * 責務)。Twig 側ではフィールド名だけ欲しい場合 `cfg.errors|keys` で取れる。
     *
     * @return array<string, list<string>>
     */
    public function errors(): array
    {
        $field = $this->resolveField();
        if (!$field instanceof Field_Validation) {
            return [];
        }
        return $field->errors();
    }

    /**
     * persisted モード版 ConfigAccessor (POST overload を行わない) を返す。
     *
     * 「ユーザー入力ではなく、純粋に保存済みの値だけを参照したい」escape hatch 用途。
     * 通常のフォーム描画では `acms_config()` のデフォルト (POST overload あり) を使う。
     *
     * persisted 版でさらに persisted を呼んでも自分自身を返す (終端)。
     */
    public function persisted(): self
    {
        if ($this->persistedMode) {
            return $this;
        }
        if ($this->persistedCache === null) {
            $accessor = new self($this->opts);
            $accessor->persistedMode = true;
            $this->persistedCache = $accessor;
        }
        return $this->persistedCache;
    }

    /**
     * 重ね合わせ済み Field を Lazy 取得 (memoize)。
     */
    private function resolveField(): Field
    {
        if ($this->field === null) {
            $this->field = $this->buildOverlaidField($this->opts);
        }
        return $this->field;
    }

    /**
     * buildFieldTrait の戻り値を Lazy 取得 (memoize)。
     *
     * @return array<string, mixed>
     */
    private function resolveBuilt(): array
    {
        if ($this->built === null) {
            $scope = $this->opts['scope'] ?? null;
            $built = $this->buildFieldTrait($this->resolveField(), $scope);
            $this->built = is_array($built) ? $built : [];
        }
        return $this->built;
    }

    /**
     * Admin_Config::getConfig 相当の重ね合わせ Field を返す。
     *
     * Phase 5.9 で Acms\Services\Config\Helper の薄いラッパーに切り出して
     * Admin_Config と共通利用する。それまでは ConfigAccessor 内で直接組み立てる。
     *
     * @param array{ruleId?: int, moduleId?: int, configSetId?: int, scope?: string} $opts
     * @return Field
     */
    private function buildOverlaidField(array $opts): Field
    {
        $ruleId = $opts['ruleId'] ?? null;
        $moduleId = $opts['moduleId'] ?? null;
        $configSetId = $opts['configSetId'] ?? null;
        if ($moduleId !== null && $moduleId > 0) {
            $configSetId = null;
        }

        $config = Config::loadDefaultField();
        if ($configSetId !== null && $configSetId > 0) {
            $config->overload(Config::loadConfigSet($configSetId));
        } else {
            $config->overload(Config::loadBlogConfig(BID));
        }

        $extra = null;
        if ($ruleId !== null && $ruleId > 0 && ($moduleId === null || $moduleId <= 0)) {
            $extra = Config::loadRuleConfig($ruleId, $configSetId);
        } elseif ($moduleId !== null && $moduleId > 0) {
            $extra = Config::loadModuleConfig($moduleId, $ruleId);
        }
        if ($extra !== null) {
            $config->overload($extra);
        }

        if ($this->persistedMode) {
            return $config;
        }
        return $this->applyPostOverload($config);
    }

    /**
     * DB 重ね合わせ済みの Config Field の上に POST.config を最後に overload する。
     *
     * Symfony Form / Rails と同じく「フォーム描画では submitted を最優先」する哲学。
     * バリデーションエラーで再描画されたときにユーザー入力を保持できる。
     *
     * POST が Field_Validation 由来なら、後段で valid()/errors() が動くように
     * Field_Validation 側を最終 Field として返す (config-side も逆 overload する)。
     */
    private function applyPostOverload(Field $config): Field
    {
        $postConfig = Field_Validation::singleton('post')->getChild('config');
        if ($postConfig->isNull()) {
            return $config;
        }
        $config->overload($postConfig);
        if ($postConfig instanceof Field_Validation) {
            $postConfig->overload($config);
            return $postConfig;
        }
        return $config;
    }
}
