"""analysis 단계 — analysis target, feature/project evidence, design prep 의 build/submit."""
from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any, Optional

from okstra_ctl.analysis_inputs import (
    ANALYSIS_TASK_TYPES,
    AnalysisInputError,
    AnalysisReportCandidate,
    list_evidence_candidates,
    load_analysis_report_candidate,
    resolve_analysis_target,
    resolve_evidence_inputs,
)
from okstra_ctl.design_prep import (
    DesignPrepError,
    load_design_prep_items,
    resolve_design_prep,
    write_design_prep_input,
)

from .ids import (
    PICK_SKIP,
    PICK_TYPE_CUSTOM,
    S_ANALYSIS_TARGET,
    S_ANALYSIS_TARGET_PICK,
    S_DESIGN_PREP_CONFIRM,
    S_DESIGN_PREP_DECISION,
    S_DESIGN_PREP_OVERRIDES,
    S_FEATURE_EVIDENCE,
    S_FEATURE_EVIDENCE_PICK,
    S_PROJECT_EVIDENCE,
    S_PROJECT_EVIDENCE_PICK,
    _RESUME_REUSE_PHASES,
)
from .state import Prompt, WizardError, WizardState
from .prompts import _opt, _p, _static_options
from .sources import _analysis_current_commit, _has_prior_run_inputs, _resolve_path


def _analysis_evidence_paths(state: WizardState) -> list[Path]:
    return [
        Path(path)
        for path in (state.feature_evidence_path, state.project_evidence_path)
        if path
    ]


def _resolve_analysis_evidence(state: WizardState):
    try:
        return resolve_evidence_inputs(
            Path(state.project_root),
            state.task_type,
            _analysis_evidence_paths(state),
            _analysis_current_commit(state),
        )
    except AnalysisInputError as exc:
        raise WizardError(str(exc)) from exc


def _accept_analysis_evidence(
    state: WizardState, raw_path: str, field_name: str
) -> AnalysisReportCandidate:
    report_path = _resolve_path(raw_path, Path(state.project_root))
    try:
        candidate = load_analysis_report_candidate(
            Path(state.project_root), report_path
        )
    except AnalysisInputError as exc:
        raise WizardError(str(exc)) from exc
    previous = getattr(state, field_name)
    setattr(state, field_name, str(candidate.report_path))
    try:
        _resolve_analysis_evidence(state)
    except WizardError:
        setattr(state, field_name, previous)
        raise
    return candidate


def _evidence_option_label(candidate: AnalysisReportCandidate) -> str:
    return f"{candidate.task_key} · {candidate.task_type} · run {candidate.run_seq}"


def _build_analysis_evidence_pick(
    state: WizardState, *, step: str, relation: str
) -> Prompt:
    t = _p(state.workspace_root, step)
    candidates = list_evidence_candidates(
        Path(state.project_root), state.task_type, relation
    )[:2]
    options = [
        _opt(str(candidate.report_path), _evidence_option_label(candidate))
        for candidate in candidates
    ]
    options.extend([
        _opt(PICK_SKIP, t["options"][PICK_SKIP]),
        _opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]),
    ])
    return Prompt(step=step, kind="pick", label=t["label"], options=options,
                  echo_template=t["echo_template"])


def _submit_analysis_evidence_pick(
    state: WizardState,
    value: str,
    *,
    step: str,
    field_name: str,
    pending_name: str,
    target_after_skip: bool = False,
) -> Optional[str]:
    t = _p(state.workspace_root, step)
    if value == PICK_SKIP:
        setattr(state, field_name, "")
        setattr(state, pending_name, False)
        if target_after_skip:
            state.analysis_target_pending_text = True
        return t["echo_variants"]["skip"]
    if value == PICK_TYPE_CUSTOM:
        setattr(state, pending_name, True)
        return None
    candidate = _accept_analysis_evidence(state, value, field_name)
    setattr(state, pending_name, False)
    return t["echo_variants"]["selected"].format(path=candidate.report_path)


def _build_feature_evidence_pick(state: WizardState) -> Prompt:
    return _build_analysis_evidence_pick(
        state, step=S_FEATURE_EVIDENCE_PICK, relation="feature-baseline"
    )


def _submit_feature_evidence_pick(state: WizardState, value: str) -> Optional[str]:
    return _submit_analysis_evidence_pick(
        state, value, step=S_FEATURE_EVIDENCE_PICK,
        field_name="feature_evidence_path",
        pending_name="feature_evidence_pending_text",
    )


def _build_feature_evidence(state: WizardState) -> Prompt:
    t = _p(state.workspace_root, S_FEATURE_EVIDENCE)
    return Prompt(step=S_FEATURE_EVIDENCE, kind="text", label=t["label"],
                  echo_template=t["echo_template"])


def _submit_feature_evidence(state: WizardState, value: str) -> Optional[str]:
    candidate = _accept_analysis_evidence(state, value, "feature_evidence_path")
    state.feature_evidence_pending_text = False
    return f"feature-evidence: {candidate.report_path}"


def _build_project_evidence_pick(state: WizardState) -> Prompt:
    return _build_analysis_evidence_pick(
        state, step=S_PROJECT_EVIDENCE_PICK, relation="project-context"
    )


def _submit_project_evidence_pick(state: WizardState, value: str) -> Optional[str]:
    return _submit_analysis_evidence_pick(
        state, value, step=S_PROJECT_EVIDENCE_PICK,
        field_name="project_evidence_path",
        pending_name="project_evidence_pending_text",
        target_after_skip=state.task_type == "feature-analysis",
    )


def _build_project_evidence(state: WizardState) -> Prompt:
    t = _p(state.workspace_root, S_PROJECT_EVIDENCE)
    return Prompt(step=S_PROJECT_EVIDENCE, kind="text", label=t["label"],
                  echo_template=t["echo_template"])


def _submit_project_evidence(state: WizardState, value: str) -> Optional[str]:
    candidate = _accept_analysis_evidence(state, value, "project_evidence_path")
    state.project_evidence_pending_text = False
    return f"project-evidence: {candidate.report_path}"


def _feature_description(feature: dict[object, object]) -> str:
    name = str(feature.get("name") or "")
    summary = str(feature.get("summary") or "")
    entry_point = str(
        feature.get("entryPoint") or feature.get("entrypoint") or ""
    )
    return " · ".join(value for value in (name, summary, entry_point) if value)


def _build_analysis_target_pick(state: WizardState) -> Prompt:
    t = _p(state.workspace_root, S_ANALYSIS_TARGET_PICK)
    try:
        candidate = load_analysis_report_candidate(
            Path(state.project_root), Path(state.project_evidence_path)
        )
    except AnalysisInputError as exc:
        raise WizardError(str(exc)) from exc
    options = [
        _opt(str(feature["id"]), str(feature["id"]), _feature_description(feature))
        for feature in candidate.feature_index
        if isinstance(feature, dict) and isinstance(feature.get("id"), str)
    ][:3]
    options.append(_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]))
    return Prompt(step=S_ANALYSIS_TARGET_PICK, kind="pick", label=t["label"],
                  options=options, echo_template=t["echo_template"])


def _accept_analysis_target(state: WizardState, value: str) -> str:
    candidates: dict[Path, AnalysisReportCandidate] = {}
    for path in _analysis_evidence_paths(state):
        try:
            candidate = load_analysis_report_candidate(Path(state.project_root), path)
        except AnalysisInputError as exc:
            raise WizardError(str(exc)) from exc
        candidates[candidate.report_path] = candidate
    try:
        target = resolve_analysis_target(
            value, _resolve_analysis_evidence(state), candidates
        )
    except AnalysisInputError as exc:
        raise WizardError(str(exc)) from exc
    requested_value = target["requestedValue"]
    if not isinstance(requested_value, str):
        raise WizardError("analysis target resolver returned an invalid requestedValue")
    state.analysis_target = requested_value
    state.analysis_target_pending_text = False
    return requested_value


def _submit_analysis_target_pick(state: WizardState, value: str) -> Optional[str]:
    if value == PICK_TYPE_CUSTOM:
        state.analysis_target_pending_text = True
        return None
    return f"analysis-target: {_accept_analysis_target(state, value)}"


def _build_analysis_target(state: WizardState) -> Prompt:
    t = _p(state.workspace_root, S_ANALYSIS_TARGET)
    return Prompt(step=S_ANALYSIS_TARGET, kind="text", label=t["label"],
                  echo_template=t["echo_template"])


def _submit_analysis_target(state: WizardState, value: str) -> Optional[str]:
    return f"analysis-target: {_accept_analysis_target(state, value)}"


def _design_prep_items_by_id(state: WizardState) -> dict[str, dict[str, Any]]:
    try:
        items = load_design_prep_items(Path(state.approved_plan_path))
    except DesignPrepError as exc:
        raise WizardError(str(exc)) from exc
    return {str(item["id"]): item for item in items}


def _selected_impl_stage_numbers(state: WizardState) -> set[int]:
    numbers: set[int] = set()
    for token in (state.selected_stages or state.selected_stage or "").split(","):
        token = token.strip()
        if token.isdigit():
            numbers.add(int(token))
    return numbers


def _blocking_unanswered_prep_ids(state: WizardState) -> list[str] | None:
    """고른 스테이지에서 구현을 막는 unanswered blocked id. 로드 불가면 None."""
    report_path = Path(state.approved_plan_path)
    if not re.fullmatch(
        r"final-report-implementation-planning-\d+\.data\.json",
        report_path.name,
    ):
        return None
    if not report_path.is_file():
        return None
    try:
        items = load_design_prep_items(report_path)
        effective = resolve_design_prep(report_path)
    except DesignPrepError as exc:
        raise WizardError(str(exc)) from exc
    decisions = {
        str(item["id"]): item.get("decision")
        for item in effective.effective_items
    }
    stages = _selected_impl_stage_numbers(state)
    # provisional 미응답은 구현을 막지 않는다. 작업 가정을 그대로 쓴다.
    return [
        str(item["id"])
        for item in items
        if item.get("status") == "blocked"
        and decisions.get(str(item["id"])) == "unanswered"
        and (
            not stages
            or stages.intersection(
                stage for stage in (item.get("stageRefs") or [])
                if isinstance(stage, int)
            )
        )
    ]


def _prune_design_prep_session(state: WizardState, wanted: list[str]) -> None:
    """이미 만든 큐에서 더 이상 막지 않는 id 를 뺀다. 새 id 는 넣지 않는다."""
    wanted_set = set(wanted)
    if state.design_prep_current and state.design_prep_current not in wanted_set:
        state.design_prep_current = ""
        state.design_prep_decision = ""
        state.design_prep_overrides_json = ""
        state.design_prep_notes = ""
    state.design_prep_queue = [
        item_id for item_id in state.design_prep_queue if item_id in wanted_set
    ]
    if not state.design_prep_current:
        _advance_design_prep_item(state)


def _ensure_design_prep_queue(state: WizardState) -> None:
    wanted = _blocking_unanswered_prep_ids(state)
    if wanted is None:
        return
    if state.design_prep_current or state.design_prep_queue:
        _prune_design_prep_session(state, wanted)
        return
    if S_DESIGN_PREP_DECISION in state.answered:
        return
    state.design_prep_queue = list(wanted)
    _advance_design_prep_item(state)


def _advance_design_prep_item(state: WizardState) -> None:
    state.design_prep_current = (
        state.design_prep_queue.pop(0) if state.design_prep_queue else ""
    )
    state.design_prep_decision = ""
    state.design_prep_overrides_json = ""
    state.design_prep_notes = ""


def _current_design_prep_item(state: WizardState) -> dict[str, Any]:
    item = _design_prep_items_by_id(state).get(state.design_prep_current)
    if item is None:
        raise WizardError(
            f"design preparation item is missing: {state.design_prep_current}"
        )
    return item


def _design_prep_decision_applies(state: WizardState) -> bool:
    if state.task_type != "implementation" or not state.approved_plan_path:
        return False
    _ensure_design_prep_queue(state)
    return bool(state.design_prep_current and not state.design_prep_decision)


def _build_design_prep_decision(state: WizardState) -> Prompt:
    item = _current_design_prep_item(state)
    proposal = item.get("aiProposal") or {}
    t = _p(
        state.workspace_root,
        S_DESIGN_PREP_DECISION,
        item_id=str(item["id"]),
        title=str(item["title"]),
        stage_refs=json.dumps(item.get("stageRefs") or [], ensure_ascii=False),
        proposal_summary=str(proposal.get("summary") or ""),
        details=json.dumps(proposal.get("details") or [], ensure_ascii=False),
        assumptions=json.dumps(proposal.get("assumptions") or [], ensure_ascii=False),
        guardrails=json.dumps(item.get("guardrails") or [], ensure_ascii=False),
        request_path=str(item.get("requestPath") or ""),
    )
    return Prompt(
        step=S_DESIGN_PREP_DECISION,
        kind="pick",
        label=t["label"],
        options=[_opt(value, label) for value, label in _static_options(t)],
        echo_template=t["echo_template"],
    )


def _submit_design_prep_decision(
    state: WizardState,
    value: str,
) -> Optional[str]:
    t = _p(state.workspace_root, S_DESIGN_PREP_DECISION,
           item_id="", title="", stage_refs="", proposal_summary="",
           details="", assumptions="", guardrails="", request_path="")
    if value not in ("accept-draft", "modify-draft", "reject-draft", "later"):
        raise WizardError(t["errors"]["invalid_decision"])
    if value == "later":
        item_id = state.design_prep_current
        _advance_design_prep_item(state)
        return t["echo_variants"]["later"].format(item_id=item_id)
    state.design_prep_decision = value
    state.design_prep_overrides_json = ""
    state.design_prep_notes = ""
    return t["echo_template"].format(value=value)


def _design_prep_overrides_applies(state: WizardState) -> bool:
    if state.design_prep_decision == "modify-draft":
        return not state.design_prep_overrides_json
    if state.design_prep_decision == "reject-draft":
        return not state.design_prep_notes
    return False


def _build_design_prep_overrides(state: WizardState) -> Prompt:
    t = _p(
        state.workspace_root,
        S_DESIGN_PREP_OVERRIDES,
        item_id=state.design_prep_current,
        decision=state.design_prep_decision,
    )
    return Prompt(
        step=S_DESIGN_PREP_OVERRIDES,
        kind="text",
        label=t["label"],
        echo_template=t["echo_template"],
    )


def _submit_design_prep_overrides(
    state: WizardState,
    value: str,
) -> Optional[str]:
    t = _p(state.workspace_root, S_DESIGN_PREP_OVERRIDES,
           item_id=state.design_prep_current,
           decision=state.design_prep_decision)
    if state.design_prep_decision == "reject-draft":
        if not value.strip():
            raise WizardError(t["errors"]["note_required"])
        state.design_prep_notes = value.strip()
        return t["echo_variants"]["note"]
    try:
        overrides = json.loads(value)
    except json.JSONDecodeError as exc:
        raise WizardError(t["errors"]["invalid_json"].format(error=exc)) from exc
    if not isinstance(overrides, dict):
        raise WizardError(t["errors"]["object_required"])
    state.design_prep_overrides_json = json.dumps(
        overrides, ensure_ascii=False, sort_keys=True
    )
    return t["echo_variants"]["overrides"]


def _design_prep_confirm_applies(state: WizardState) -> bool:
    if state.task_type == "implementation" and state.approved_plan_path:
        _ensure_design_prep_queue(state)
    if state.design_prep_decision == "accept-draft":
        return True
    if state.design_prep_decision == "modify-draft":
        return bool(state.design_prep_overrides_json)
    if state.design_prep_decision == "reject-draft":
        return bool(state.design_prep_notes)
    return False


def _build_design_prep_confirm(state: WizardState) -> Prompt:
    item = _current_design_prep_item(state)
    t = _p(
        state.workspace_root,
        S_DESIGN_PREP_CONFIRM,
        item_id=state.design_prep_current,
        decision=state.design_prep_decision,
        item_snapshot=json.dumps(item, ensure_ascii=False, indent=2, sort_keys=True),
        overrides=state.design_prep_overrides_json or "{}",
        notes=state.design_prep_notes or "(none)",
    )
    return Prompt(
        step=S_DESIGN_PREP_CONFIRM,
        kind="pick",
        label=t["label"],
        options=[_opt(value, label) for value, label in _static_options(t)],
        echo_template=t["echo_template"],
    )


def _submit_design_prep_confirm(
    state: WizardState,
    value: str,
) -> Optional[str]:
    item_id = state.design_prep_current
    t = _p(state.workspace_root, S_DESIGN_PREP_CONFIRM,
           item_id=item_id, decision=state.design_prep_decision,
           item_snapshot="", overrides="", notes="")
    if value == "no":
        state.design_prep_decision = ""
        state.design_prep_overrides_json = ""
        state.design_prep_notes = ""
        return t["echo_variants"]["revise"].format(item_id=item_id)
    if value != "yes":
        raise WizardError(t["errors"]["confirmation_required"])
    overrides = json.loads(state.design_prep_overrides_json or "{}")
    try:
        path = write_design_prep_input(
            Path(state.approved_plan_path),
            item_id,
            state.design_prep_decision,
            overrides,
            state.design_prep_notes,
            captured_by="okstra-wizard",
        )
    except DesignPrepError as exc:
        raise WizardError(str(exc)) from exc
    match = re.search(r"-r(\d+)-([0-9a-f-]{36})\.md$", path.name)
    if match is None:
        raise WizardError(f"design preparation writer returned invalid path: {path}")
    echo = t["echo_variants"]["written"].format(
        item_id=item_id,
        revision=int(match.group(1)),
        input_id=match.group(2),
        path=path,
    )
    _advance_design_prep_item(state)
    return echo


def _analysis_inputs_after_reuse(state: WizardState) -> bool:
    return (
        state.reuse_previous is not None
        or state.task_type not in _RESUME_REUSE_PHASES
        or not _has_prior_run_inputs(state)
    )


def _restore_analysis_evidence_inputs(
    state: WizardState, evidence_raw: object
) -> tuple[tuple[Any, ...], dict[Path, AnalysisReportCandidate]]:
    if state.task_type not in ANALYSIS_TASK_TYPES or not isinstance(evidence_raw, str):
        return (), {}
    report_paths = [
        _resolve_path(item.strip(), Path(state.project_root))
        for item in evidence_raw.split(",")
        if item.strip()
    ]
    try:
        candidates = [
            load_analysis_report_candidate(Path(state.project_root), path)
            for path in report_paths
        ]
        evidence_inputs = resolve_evidence_inputs(
            Path(state.project_root),
            state.task_type,
            [candidate.report_path for candidate in candidates],
            _analysis_current_commit(state),
        )
    except AnalysisInputError as exc:
        raise WizardError(str(exc)) from exc
    state.feature_evidence_path = ""
    state.project_evidence_path = ""
    for evidence in evidence_inputs:
        if evidence.relation == "feature-baseline":
            state.feature_evidence_path = str(evidence.report_path)
        elif evidence.relation == "project-context":
            state.project_evidence_path = str(evidence.report_path)
    return evidence_inputs, {
        candidate.report_path: candidate for candidate in candidates
    }
