"""위저드 프롬프트 문구 SOT — `prompts/wizard/prompts.ko.json` 로더와 치환."""
from __future__ import annotations

from pathlib import Path

from okstra_ctl.domain.wizard.interaction import WizardOption, WizardPrompt
from okstra_ctl.registry.host_registry import default_host_registry
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object

from .ids import _RECOMMENDATION_CAP
from .state import Option, Prompt, WizardError, WizardState


# --------------------------------------------------------------------------- #
# Wizard prompt JSON SOT (Phase A1)
# --------------------------------------------------------------------------- #

_WIZARD_ROOT_CACHE: dict[str, dict] = {}


def _load_wizard_root(workspace_root: str) -> dict:
    """Load and cache the full wizard prompts JSON root for the given workspace_root.

    Returns the entire parsed JSON object (with `schema_version`, `locale`, `steps`,
    optional `confirmation` etc. at top level). Callers that only need the steps
    dict should index `["steps"]` on the return value; `_msg` reads top-level
    sections like `confirmation`.
    """
    if workspace_root in _WIZARD_ROOT_CACHE:
        return _WIZARD_ROOT_CACHE[workspace_root]
    path = Path(workspace_root) / "prompts" / "wizard" / "prompts.ko.json"
    if not path.is_file():
        raise WizardError(
            f"wizard prompt SOT not found: {path}. "
            "Re-run `okstra install` or check the workspace_root."
        )
    try:
        raw = load_owned_object(path, artifact="wizard prompts")
    except JsonBoundaryError as exc:
        raise WizardError(f"wizard prompt JSON malformed: {path}: {exc}") from exc
    if not isinstance(raw, dict):
        raise WizardError(f"wizard prompt JSON root must be an object: {path}")
    if raw.get("schema_version") != 1:
        raise WizardError(
            f"wizard prompt schema_version mismatch (expected 1): {path}"
        )
    if raw.get("locale") != "ko":
        raise WizardError(
            f"wizard prompt locale unsupported (expected 'ko'): {path}"
        )
    steps = raw.get("steps")
    if not isinstance(steps, dict):
        raise WizardError(f"wizard prompt 'steps' missing or not a dict: {path}")
    _WIZARD_ROOT_CACHE[workspace_root] = raw
    return raw


def _p(workspace_root: str, step_id: str, **vars: str) -> dict:
    """Look up a wizard prompt entry by step_id and interpolate placeholders.

    Returns a dict with keys: 'label' (str, possibly interpolated),
    'echo_template' (str, raw — contains `{value}` for the user's answer),
    'options' (dict[value → label], may be empty).

    Returned dict also includes echo_variants and errors sub-dicts (may be empty).

    Raises WizardError if the step_id is unknown or a required placeholder
    is missing from the provided vars.
    """
    steps = _load_wizard_root(workspace_root)["steps"]
    raw = steps.get(step_id)
    if raw is None:
        raise WizardError(f"unknown wizard step_id: {step_id!r}")
    label_template = raw.get("label", "")
    fv_label_template = raw.get("label_final_verification", "")
    unlinked_template = raw.get("label_unlinked", "")
    no_answers_template = raw.get("label_no_answers", "")
    try:
        label = label_template.format(**vars)
        label_final_verification = fv_label_template.format(**vars)
        label_unlinked = unlinked_template.format(**vars)
        label_no_answers = no_answers_template.format(**vars)
    except KeyError as exc:
        missing = exc.args[0] if exc.args else "<unknown>"
        raise WizardError(
            f"missing placeholder {missing!r} for wizard step {step_id!r}"
        ) from exc
    return {
        "label": label,
        "label_final_verification": label_final_verification,
        "label_unlinked": label_unlinked,
        "label_no_answers": label_no_answers,
        "echo_template": raw.get("echo_template", ""),
        "options": raw.get("options", {}),
        "options_final_verification": raw.get("options_final_verification", {}),
        "options_html_approval": raw.get("options_html_approval", {}),
        "html_approval_note": raw.get("html_approval_note", ""),
        "html_approval_note_default_option": raw.get(
            "html_approval_note_default_option", ""),
        "echo_variants": raw.get("echo_variants", {}),
        "errors": raw.get("errors", {}),
        "labels": raw.get("labels", {}),
        "sidecar_note": raw.get("sidecar_note", {}),
        "echo_suffixes": raw.get("echo_suffixes", {}),
        "recent_label_prefix": raw.get("recent_label_prefix", ""),
    }


def _msg(workspace_root: str, section: str, key: str, **vars: str) -> str:
    """Look up a top-level message (e.g., `confirmation.header`) and interpolate.

    Returns the string value at `<section>.<key>`. Raises WizardError if the
    section or key is unknown, or if a required placeholder is missing.

    Use for wizard-level messages (confirmation block etc.). For step-scoped
    messages (echo_variants, errors), use `_p()` instead.
    """
    root = _load_wizard_root(workspace_root)
    sect = root.get(section)
    if not isinstance(sect, dict):
        raise WizardError(f"unknown wizard section: {section!r}")
    template = sect.get(key)
    if template is None:
        raise WizardError(f"unknown wizard message: {section}.{key!r}")
    try:
        return template.format(**vars)
    except KeyError as exc:
        missing = exc.args[0] if exc.args else "<unknown>"
        raise WizardError(
            f"missing placeholder {missing!r} for wizard message {section}.{key!r}"
        ) from exc


def _static_options(t: dict) -> list[tuple[str, str]]:
    """Return (value, label) pairs from the SOT's static options dict, excluding
    suffix-decoration tokens (`_<NAME>_SUFFIX` / `_<NAME>_LABEL`).

    Used by hybrid `_build_*` functions that mix dynamic and static options to
    avoid leaking `_RECOMMENDED_SUFFIX` / `_OPTIONAL_SUFFIX` / `_DEFAULT_SUFFIX`
    as a literal option entry.
    """
    return [
        (k, v) for k, v in t.get("options", {}).items()
        if not (k.startswith("_") and (k.endswith("_SUFFIX") or k.endswith("_LABEL")))
    ]


def _opt(
    value: str,
    label: str = "",
    description: str = "",
    *,
    recommended: bool = False,
) -> Option:
    return Option(
        value=value,
        label=label or value,
        description=description,
        recommended=recommended,
    )


def _recommendation_budget(state: WizardState) -> int:
    """추천 후보를 몇 개까지 실을지. 마지막 "직접 입력" 자리는 이미 뺀 수다.

    상한은 두 곳에서 온다. 하나는 화면 UX 규칙인 `_RECOMMENDATION_CAP`,
    다른 하나는 호스트의 네이티브 단일선택기가 받는 옵션 수다. 후자를 넘기는
    순간 `CapabilityInteractionPort.plan` 이 `numbered-single` 로 내려 사용자는
    선택지가 아니라 번호 목록 텍스트를 본다 — 한도는 호스트마다 다르다
    (codex 3, claude-code 4, grok 15; 각 호스트 `relay.md` 의 `nativeLimits`).
    선택기 자체가 없는 호스트(antigravity·kimi·external 은 `plain_text_input`
    뿐)는 어차피 번호 목록이므로 UX 규칙만 적용한다.
    """
    if "native_single_select" not in set(state.available_functions):
        return _RECOMMENDATION_CAP
    port = default_host_registry().resolve(state.host_runtime).interaction()
    return max(1, min(_RECOMMENDATION_CAP, port.native_option_limit - 1))


def _pick_snippet(value: str, style: str) -> str:
    if len(value) <= 60:
        return value
    return value[:60] + "…" if style == "prefix" else "…" + value[-60:]


def _domain_prompt(prompt: Prompt) -> WizardPrompt:
    return WizardPrompt(
        step=prompt.step,
        kind=prompt.kind,
        label=prompt.label,
        options=tuple(
            WizardOption(
                option.value, option.label, option.description,
                option.recommended,
            )
            for option in prompt.options
        ),
        help=prompt.help,
        echo_template=prompt.echo_template,
        multi=prompt.multi,
        questions=tuple(_domain_prompt(question) for question in prompt.questions),
    )
