"""단계 등록표 — 순서가 곧 질문 순서인 `STEPS`, `STEP_BY_ID`, 등록표를 되감는 `_reset_from` 과 되감기를 일으키는 단계(edit target, brief carry)."""
from __future__ import annotations

from typing import Optional

from .ids import (
    BRIEF_CARRY_ABORT,
    BRIEF_CARRY_SWITCH_ENTRY,
    PICK_TYPE_CUSTOM,
    S_ANALYSIS_TARGET,
    S_ANALYSIS_TARGET_PICK,
    S_ANTIGRAVITY_MODEL,
    S_APPROVED_PLAN,
    S_APPROVED_PLAN_PICK,
    S_APPROVE_PLAN_CONFIRM,
    S_BASE_REF_PICK,
    S_BASE_REF_TEXT,
    S_BRIEF_CARRY,
    S_BRIEF_KEEP,
    S_BRIEF_PATH,
    S_BRIEF_PATH_PICK,
    S_CLARIFICATION,
    S_CLARIFICATION_PICK,
    S_CLAUDE_MODEL,
    S_CODEX_MODEL,
    S_CONFIRM,
    S_CRITIC_PICK,
    S_CRITIC_TEXT,
    S_DEFAULTS_OR_CUSTOM,
    S_DESIGN_PREP_CONFIRM,
    S_DESIGN_PREP_DECISION,
    S_DESIGN_PREP_OVERRIDES,
    S_DIRECTIVE,
    S_DIRECTIVE_PICK,
    S_EDIT_TARGET,
    S_EXECUTOR,
    S_EXECUTOR_MODEL,
    S_FEATURE_EVIDENCE,
    S_FEATURE_EVIDENCE_PICK,
    S_FIX_CYCLE_CONFIRM,
    S_GROK_MODEL,
    S_HANDOFF_STAGE_PICK,
    S_KIMI_MODEL,
    S_LEAD_MODEL,
    S_PROJECT_EVIDENCE,
    S_PROJECT_EVIDENCE_PICK,
    S_PR_TEMPLATE,
    S_PR_TEMPLATE_PICK,
    S_PR_TEMPLATE_SCOPE,
    S_RELATED_TASKS,
    S_RELATED_TASKS_PICK,
    S_REPORT_WRITER_MODEL,
    S_REPORT_LANGUAGE,
    S_REUSE_PREVIOUS,
    S_REVERIFY_SCOPE_PICK,
    S_REVERIFY_SCOPE_STAGES,
    S_SELECTED_DIRECTION_PICK,
    S_STAGE_PICK,
    S_TASK_GROUP,
    S_TASK_GROUP_TEXT,
    S_TASK_ID,
    S_TASK_ID_TEXT,
    S_TASK_PICK,
    S_TASK_TYPE,
    S_TASK_TYPE_TEXT,
    S_WORKERS_CUSTOM,
    S_WORKERS_OVERRIDE,
    _BRIEF_ENTRY_TASK_TYPES,
    _RESUME_REUSE_PHASES,
    _STAGE_SCOPED_TASK_TYPES,
)
from .state import (
    Option,
    Prompt,
    Step,
    WizardError,
    WizardState,
    _base_ref_ready,
    _base_ref_required,
    _identity_ready,
    _is_role_selection_step,
    _reset_field,
    _role_selection_enabled,
)
from .prompts import _opt, _p
from .sources import (
    _brief_resolved,
    _has_prior_run_inputs,
    _latest_implementation_planning_report,
    _resolved_roster,
    _reverify_scope_pick_required,
    _suggest_latest_final_report,
)
from .roles import _clear_role_selection, _reset_role_selection_from, next_role_prompt
from .steps_identity import (
    _build_base_ref_pick,
    _build_base_ref_text,
    _build_brief_carry,
    _build_brief_keep,
    _build_brief_path,
    _build_brief_path_pick,
    _build_task_group,
    _build_task_group_text,
    _build_task_id,
    _build_task_id_text,
    _build_task_pick,
    _build_task_type,
    _build_task_type_text,
    _submit_base_ref_pick,
    _submit_base_ref_text,
    _submit_brief_keep,
    _submit_brief_path,
    _submit_brief_path_pick,
    _submit_task_group,
    _submit_task_group_text,
    _submit_task_id,
    _submit_task_id_text,
    _submit_task_pick,
    _submit_task_type,
    _submit_task_type_text,
)
from .steps_analysis import (
    _analysis_inputs_after_reuse,
    _build_analysis_target,
    _build_analysis_target_pick,
    _build_design_prep_confirm,
    _build_design_prep_decision,
    _build_design_prep_overrides,
    _build_feature_evidence,
    _build_feature_evidence_pick,
    _build_project_evidence,
    _build_project_evidence_pick,
    _design_prep_confirm_applies,
    _design_prep_decision_applies,
    _design_prep_overrides_applies,
    _submit_analysis_target,
    _submit_analysis_target_pick,
    _submit_design_prep_confirm,
    _submit_design_prep_decision,
    _submit_design_prep_overrides,
    _submit_feature_evidence,
    _submit_feature_evidence_pick,
    _submit_project_evidence,
    _submit_project_evidence_pick,
)
from .steps_plan import (
    _build_approve_plan_confirm,
    _build_approved_plan,
    _build_approved_plan_pick,
    _build_fix_cycle_confirm,
    _build_handoff_stage_pick,
    _build_reverify_scope_pick,
    _build_reverify_scope_stages,
    _build_selected_direction_pick,
    _build_stage_pick,
    _fix_cycle_confirm_required,
    _planning_rerun_selected,
    _reverify_scope_step_pending,
    _selected_direction_candidates,
    _submit_approve_plan_confirm,
    _submit_approved_plan,
    _submit_approved_plan_pick,
    _submit_fix_cycle_confirm,
    _submit_handoff_stage_pick,
    _submit_reverify_scope_pick,
    _submit_reverify_scope_stages,
    _submit_selected_direction_pick,
    _submit_stage_pick,
)
from .steps_options import (
    _report_language_required,
    _build_report_language,
    _submit_report_language,
    _build_clarification,
    _build_clarification_pick,
    _build_directive,
    _build_directive_pick,
    _build_pr_template,
    _build_pr_template_pick,
    _build_pr_template_scope,
    _build_related_tasks,
    _build_related_tasks_pick,
    _submit_clarification,
    _submit_clarification_pick,
    _submit_directive,
    _submit_directive_pick,
    _submit_pr_template,
    _submit_pr_template_pick,
    _submit_pr_template_scope,
    _submit_related_tasks,
    _submit_related_tasks_pick,
)
from .steps_roles import (
    _build_antigravity_model,
    _build_claude_model,
    _build_codex_model,
    _build_critic_pick,
    _build_critic_text,
    _build_defaults_or_custom,
    _build_executor,
    _build_executor_model,
    _build_grok_model,
    _build_kimi_model,
    _build_lead_model,
    _build_report_writer_model,
    _build_reuse_previous,
    _build_workers_custom,
    _build_workers_override,
    _submit_antigravity_model,
    _submit_claude_model,
    _submit_codex_model,
    _submit_critic_pick,
    _submit_critic_text,
    _submit_defaults_or_custom,
    _submit_executor,
    _submit_executor_model,
    _submit_grok_model,
    _submit_kimi_model,
    _submit_lead_model,
    _submit_report_writer_model,
    _submit_reuse_previous,
    _submit_workers_custom,
    _submit_workers_override,
)
from .confirmation import _build_confirm, _submit_confirm


def _submit_brief_carry(state: WizardState, value: str) -> Optional[str]:
    t = _p(state.workspace_root, "brief_carry", task_type=state.task_type)
    if value == BRIEF_CARRY_SWITCH_ENTRY:
        _reset_from(state, S_TASK_TYPE)
        return t["echo_variants"]["switch_entry"]
    if value == PICK_TYPE_CUSTOM:
        state.brief_path_pending_text = True
        return None
    if value == BRIEF_CARRY_ABORT:
        state.aborted = True
        return t["echo_variants"]["abort"]
    raise WizardError(
        f"expected '{BRIEF_CARRY_SWITCH_ENTRY}', {PICK_TYPE_CUSTOM!r}, "
        f"or '{BRIEF_CARRY_ABORT}', got: {value!r}"
    )


def _build_edit_target(state: WizardState) -> Prompt:
    t = _p(state.workspace_root, "edit_target")
    # offer every step that has been answered.
    options: list[Option] = []
    selection_ids = [
        *state.answered,
        *(
            step_id
            for step_id in state.role_selection_order
            if step_id not in state.answered
        ),
    ]
    for sid in selection_ids:
        if sid in (S_CONFIRM, S_EDIT_TARGET):
            continue
        options.append(_opt(sid, sid))
    return Prompt(
        step=S_EDIT_TARGET, kind="pick",
        label=t["label"],
        options=options,
        echo_template=t["echo_template"],
    )


def _submit_edit_target(state: WizardState, value: str) -> Optional[str]:
    if _is_role_selection_step(value):
        _reset_role_selection_from(state, value)
    elif any(s.id == value for s in STEPS):
        _reset_from(state, value)
    else:
        raise WizardError(f"unknown step: {value!r}")
    state.edit_target = value
    state.confirmed = None
    state.edit_target = ""
    return f"edit-target: {value} (rewinding)"


# --- step registry ---

STEPS: list[Step] = [
    Step(S_REPORT_LANGUAGE,
         applies=_report_language_required,
         build=_build_report_language, submit=_submit_report_language,
         owns=()),
    Step(S_TASK_PICK,
         applies=lambda s: s.is_new_task is None,
         build=_build_task_pick, submit=_submit_task_pick,
         owns=("is_new_task", "task_group", "task_id", "task_type",
               "existing_brief_path", "profile_workers",
               "profile_optional_workers",
               "task_group_suggestion", "task_id_suggestion",
               "task_group_pending_text", "task_id_pending_text")),
    Step(S_TASK_GROUP,
         applies=lambda s: (bool(s.is_new_task)
                            and not s.task_group
                            and not s.task_group_pending_text),
         build=_build_task_group, submit=_submit_task_group,
         owns=("task_group", "task_group_pending_text")),
    Step(S_TASK_GROUP_TEXT,
         applies=lambda s: (bool(s.is_new_task)
                            and not s.task_group
                            and s.task_group_pending_text),
         build=_build_task_group_text, submit=_submit_task_group_text,
         owns=("task_group", "task_group_pending_text")),
    # 신규 task 흐름 순서: task-group → brief → task-type → task-id.
    # brief 는 "이 task 가 무슨 일인가" 를 말하는 유일한 입력이다. 반대 순서
    # (task-type 먼저) 에서는 사용자가 작업을 지정하기 전에 단계를 골라야 했고,
    # 추천도 같은 그룹의 다른 task 이력에서만 나왔다 — 이번 task 와 무관한 값이다.
    # 기존 task 는 매니페스트 포인터가 단계를 알고 있고 brief 질문 자체가 entry
    # task-type 에서만 뜨므로 종전대로 task-type 이 먼저다.
    Step(S_BRIEF_KEEP,
         applies=lambda s: (not s.is_new_task
                            and s.task_type in _BRIEF_ENTRY_TASK_TYPES
                            and bool(s.existing_brief_path)
                            and s.keep_existing_brief is None
                            and S_TASK_TYPE in s.answered),
         build=_build_brief_keep, submit=_submit_brief_keep,
         owns=("keep_existing_brief",)),
    Step(S_BRIEF_PATH_PICK,
         applies=lambda s: (
             not s.brief_path
             and not s.brief_path_pending_text
             and S_BRIEF_PATH_PICK not in s.answered
             and (
                 (s.is_new_task is True and bool(s.task_group))
                 or (s.is_new_task is False
                     and S_TASK_TYPE in s.answered
                     and s.task_type in _BRIEF_ENTRY_TASK_TYPES
                     and (s.keep_existing_brief is False
                          or not s.existing_brief_path))
             )
         ),
         build=_build_brief_path_pick, submit=_submit_brief_path_pick,
         owns=("brief_path_pending_text",)),
    Step(S_BRIEF_PATH,
         applies=lambda s: s.brief_path_pending_text
                            and S_BRIEF_PATH not in s.answered,
         build=_build_brief_path, submit=_submit_brief_path,
         owns=("brief_path", "task_group_suggestion", "task_id_suggestion")),
    Step(S_TASK_TYPE,
         applies=lambda s: (s.is_new_task is not None
                            and (s.is_new_task is False
                                 or (bool(s.task_group) and bool(s.brief_path)))
                            and S_TASK_TYPE not in s.answered),
         build=_build_task_type, submit=_submit_task_type,
         owns=("task_type", "profile_workers", "profile_optional_workers",
               "reuse_worktree")),
    Step(S_TASK_TYPE_TEXT,
         applies=lambda s: (S_TASK_TYPE in s.answered
                            and not s.task_type
                            and S_TASK_TYPE_TEXT not in s.answered),
         build=_build_task_type_text, submit=_submit_task_type_text,
         owns=("task_type", "profile_workers", "profile_optional_workers",
               "reuse_worktree")),
    # downstream task-type 인데 carry-in 할 brief 가 없을 때의 fallback.
    # release-handoff 는 brief 자체가 없는 phase 라 대상에서 빠진다.
    Step(S_BRIEF_CARRY,
         applies=lambda s: (S_TASK_TYPE in s.answered
                            and bool(s.task_type)
                            and s.task_type not in _BRIEF_ENTRY_TASK_TYPES
                            and s.task_type != "release-handoff"
                            and not s.brief_path
                            and not s.brief_path_pending_text),
         build=_build_brief_carry, submit=_submit_brief_carry,
         owns=("brief_path_pending_text",)),
    Step(S_TASK_ID,
         applies=lambda s: (bool(s.is_new_task)
                            and bool(s.task_group)
                            and _brief_resolved(s)
                            and not s.task_id
                            and not s.task_id_pending_text),
         build=_build_task_id, submit=_submit_task_id,
         owns=("task_id", "task_id_pending_text")),
    Step(S_TASK_ID_TEXT,
         applies=lambda s: (bool(s.is_new_task)
                            and bool(s.task_group)
                            and _brief_resolved(s)
                            and not s.task_id
                            and s.task_id_pending_text),
         build=_build_task_id_text, submit=_submit_task_id_text,
         owns=("task_id", "task_id_pending_text")),
    Step(S_REUSE_PREVIOUS,
         applies=lambda s: (_identity_ready(s)
                            and s.use_defaults is None
                            and s.reuse_previous is None
                            and s.task_type in _RESUME_REUSE_PHASES
                            and _has_prior_run_inputs(s)),
         build=_build_reuse_previous, submit=_submit_reuse_previous,
         owns=("reuse_previous",)),
    Step(S_FEATURE_EVIDENCE_PICK,
         applies=lambda s: (s.task_type == "change-impact-analysis"
                            and not s.feature_evidence_path
                            and not s.feature_evidence_pending_text
                            and S_FEATURE_EVIDENCE_PICK not in s.answered
                            and _brief_resolved(s)
                            and _analysis_inputs_after_reuse(s)),
         build=_build_feature_evidence_pick, submit=_submit_feature_evidence_pick,
         owns=("feature_evidence_path", "feature_evidence_pending_text")),
    Step(S_FEATURE_EVIDENCE,
         applies=lambda s: (s.task_type == "change-impact-analysis"
                            and s.feature_evidence_pending_text
                            and S_FEATURE_EVIDENCE not in s.answered),
         build=_build_feature_evidence, submit=_submit_feature_evidence,
         owns=("feature_evidence_path", "feature_evidence_pending_text")),
    Step(S_PROJECT_EVIDENCE_PICK,
         applies=lambda s: (s.task_type in ("feature-analysis", "change-impact-analysis")
                            and not s.project_evidence_path
                            and not s.project_evidence_pending_text
                            and S_PROJECT_EVIDENCE_PICK not in s.answered
                            and _brief_resolved(s)
                            and _analysis_inputs_after_reuse(s)
                            and (s.task_type != "change-impact-analysis"
                                 or S_FEATURE_EVIDENCE_PICK in s.answered)),
         build=_build_project_evidence_pick, submit=_submit_project_evidence_pick,
         owns=("project_evidence_path", "project_evidence_pending_text",
               "analysis_target", "analysis_target_pending_text")),
    Step(S_PROJECT_EVIDENCE,
         applies=lambda s: (s.task_type in ("feature-analysis", "change-impact-analysis")
                            and s.project_evidence_pending_text
                            and S_PROJECT_EVIDENCE not in s.answered),
         build=_build_project_evidence, submit=_submit_project_evidence,
         owns=("project_evidence_path", "project_evidence_pending_text",
               "analysis_target", "analysis_target_pending_text")),
    Step(S_ANALYSIS_TARGET_PICK,
         applies=lambda s: (s.task_type == "feature-analysis"
                            and bool(s.project_evidence_path)
                            and not s.analysis_target
                            and not s.analysis_target_pending_text
                            and S_ANALYSIS_TARGET_PICK not in s.answered),
         build=_build_analysis_target_pick, submit=_submit_analysis_target_pick,
         owns=("analysis_target", "analysis_target_pending_text")),
    Step(S_ANALYSIS_TARGET,
         applies=lambda s: (s.task_type == "feature-analysis"
                            and not s.analysis_target
                            and s.analysis_target_pending_text
                            and S_ANALYSIS_TARGET not in s.answered),
         build=_build_analysis_target, submit=_submit_analysis_target,
         owns=("analysis_target", "analysis_target_pending_text")),
    Step(S_BASE_REF_PICK,
         applies=lambda s: (S_TASK_TYPE in s.answered
                            and _base_ref_required(s)
                            and S_BASE_REF_PICK not in s.answered
                            and _brief_resolved(s)
                            and (not s.is_new_task or bool(s.task_id))),
         build=_build_base_ref_pick, submit=_submit_base_ref_pick,
         owns=("base_ref", "base_ref_pending_text")),
    Step(S_BASE_REF_TEXT,
         applies=lambda s: s.base_ref_pending_text,
         build=_build_base_ref_text, submit=_submit_base_ref_text,
         owns=("base_ref", "base_ref_pending_text")),
    Step(S_APPROVED_PLAN_PICK,
         applies=lambda s: (s.task_type in _STAGE_SCOPED_TASK_TYPES
                            and not s.approved_plan_path
                            and not s.approved_plan_pending_text
                            and S_APPROVED_PLAN_PICK not in s.answered
                            and bool(s.brief_path)
                            and _base_ref_ready(s)
                            and not s.base_ref_pending_text
                            and _latest_implementation_planning_report(s) is not None),
         build=_build_approved_plan_pick, submit=_submit_approved_plan_pick,
         owns=("approved_plan_path", "approved_plan_pending_text")),
    Step(S_APPROVED_PLAN,
         applies=lambda s: (s.task_type in _STAGE_SCOPED_TASK_TYPES
                            and not s.approved_plan_path
                            and bool(s.brief_path)
                            and _base_ref_ready(s)
                            and not s.base_ref_pending_text
                            and (s.approved_plan_pending_text
                                 or _latest_implementation_planning_report(s) is None)),
         build=_build_approved_plan, submit=_submit_approved_plan,
         owns=("approved_plan_path", "approved_plan_pending_text")),
    Step(S_APPROVE_PLAN_CONFIRM,
         applies=lambda s: (s.task_type in _STAGE_SCOPED_TASK_TYPES
                            and bool(s.approve_plan_candidate)
                            and not s.approved_plan_path
                            and S_APPROVE_PLAN_CONFIRM not in s.answered),
         build=_build_approve_plan_confirm, submit=_submit_approve_plan_confirm,
         owns=("approve_plan_candidate",)),
    Step(S_STAGE_PICK,
         applies=lambda s: (s.task_type in _STAGE_SCOPED_TASK_TYPES
                            and bool(s.approved_plan_path)
                            and S_STAGE_PICK not in s.answered),
         build=_build_stage_pick, submit=_submit_stage_pick,
         owns=("selected_stage", "selected_stages")),
    Step(S_DESIGN_PREP_DECISION,
         applies=_design_prep_decision_applies,
         build=_build_design_prep_decision,
         submit=_submit_design_prep_decision,
         owns=("design_prep_queue", "design_prep_current",
               "design_prep_decision", "design_prep_overrides_json",
               "design_prep_notes"),
         repeatable=True),
    Step(S_DESIGN_PREP_OVERRIDES,
         applies=_design_prep_overrides_applies,
         build=_build_design_prep_overrides,
         submit=_submit_design_prep_overrides,
         owns=("design_prep_overrides_json", "design_prep_notes"),
         repeatable=True),
    Step(S_DESIGN_PREP_CONFIRM,
         applies=_design_prep_confirm_applies,
         build=_build_design_prep_confirm,
         submit=_submit_design_prep_confirm,
         owns=("design_prep_decision", "design_prep_overrides_json",
               "design_prep_notes"),
         repeatable=True),
    Step(S_HANDOFF_STAGE_PICK,
         applies=lambda s: (s.task_type == "release-handoff"
                            and bool(s.task_group)
                            and bool(s.task_id)
                            and S_HANDOFF_STAGE_PICK not in s.answered),
         build=_build_handoff_stage_pick, submit=_submit_handoff_stage_pick,
         owns=("handoff_mode", "handoff_stages", "approved_plan_path")),
    Step(S_EXECUTOR,
         applies=lambda s: (s.task_type == "implementation"
                            and not _role_selection_enabled(s)
                            and bool(s.approved_plan_path)
                            and not s.executor),
         build=_build_executor, submit=_submit_executor,
         owns=("executor",)),
    Step(S_CRITIC_PICK,
         applies=lambda s: (not _role_selection_enabled(s)
                            and s.task_type in ("requirements-discovery", "error-analysis", "implementation-planning", "final-verification")
                            and not s.critic
                            and not s.critic_pending_text
                            and S_CRITIC_PICK not in s.answered),
         build=_build_critic_pick, submit=_submit_critic_pick,
         owns=("critic", "critic_pending_text")),
    Step(S_CRITIC_TEXT,
         applies=lambda s: (not _role_selection_enabled(s)
                            and s.critic_pending_text
                            and S_CRITIC_TEXT not in s.answered),
         build=_build_critic_text, submit=_submit_critic_text,
         owns=("critic", "critic_pending_text")),
    Step(S_DEFAULTS_OR_CUSTOM,
         applies=lambda s: (_identity_ready(s)
                            and not _role_selection_enabled(s)
                            and s.use_defaults is None),
         build=_build_defaults_or_custom, submit=_submit_defaults_or_custom,
         owns=("use_defaults",)),
    # Worker roster is ALWAYS prompted (independent of the defaults /
    # customize branch) when the profile has analyser-candidate workers
    # (required ∪ optional, minus report-writer). Users repeatedly hit
    # the failure mode where the lead silently picked "Use defaults"
    # and the worker prompt never appeared — defaults should govern
    # model choice, not worker selection. `implementation` task-type is
    # still skipped because it runs lead + executor only.
    Step(S_WORKERS_OVERRIDE,
         applies=lambda s: (s.task_type != "implementation"
                            and not _role_selection_enabled(s)
                            and any(
                                w != "report-writer"
                                for w in (s.profile_workers
                                          + s.profile_optional_workers)
                            )
                            and S_WORKERS_OVERRIDE not in s.answered),
         build=_build_workers_override, submit=_submit_workers_override,
         owns=("workers_override", "workers_custom_pending")),
    # "직접 선택" 을 고른 run 에서만 등장한다 — 기본 로스터에서 워커를 빼는
    # 축소가 가능한 유일한 화면.
    Step(S_WORKERS_CUSTOM,
         applies=lambda s: (not _role_selection_enabled(s)
                            and s.workers_custom_pending
                            and S_WORKERS_CUSTOM not in s.answered),
         build=_build_workers_custom, submit=_submit_workers_custom,
         owns=("workers_override", "workers_custom_pending")),
    Step(S_LEAD_MODEL,
         applies=lambda s: (s.use_defaults is False
                            and not _role_selection_enabled(s)
                            and S_LEAD_MODEL not in s.answered),
         build=_build_lead_model, submit=_submit_lead_model,
         owns=("lead_provider", "lead_model")),
    Step(S_EXECUTOR_MODEL,
         applies=lambda s: (s.use_defaults is False
                            and not _role_selection_enabled(s)
                            and s.task_type == "implementation"
                            and S_EXECUTOR_MODEL not in s.answered),
         build=_build_executor_model, submit=_submit_executor_model,
         owns=("claude_model", "codex_model", "antigravity_model")),
    Step(S_CLAUDE_MODEL,
         applies=lambda s: (s.use_defaults is False
                            and not _role_selection_enabled(s)
                            and s.task_type != "implementation"
                            and "claude" in _resolved_roster(s)
                            and S_CLAUDE_MODEL not in s.answered),
         build=_build_claude_model, submit=_submit_claude_model,
         owns=("claude_model",)),
    Step(S_CODEX_MODEL,
         applies=lambda s: (s.use_defaults is False
                            and not _role_selection_enabled(s)
                            and s.task_type != "implementation"
                            and "codex" in _resolved_roster(s)
                            and S_CODEX_MODEL not in s.answered),
         build=_build_codex_model, submit=_submit_codex_model,
         owns=("codex_model",)),
    Step(S_ANTIGRAVITY_MODEL,
         applies=lambda s: (s.use_defaults is False
                            and not _role_selection_enabled(s)
                            and s.task_type != "implementation"
                            and "antigravity" in _resolved_roster(s)
                            and S_ANTIGRAVITY_MODEL not in s.answered),
         build=_build_antigravity_model, submit=_submit_antigravity_model,
         owns=("antigravity_model",)),
    Step(S_GROK_MODEL,
         applies=lambda s: (s.use_defaults is False
                            and not _role_selection_enabled(s)
                            and s.task_type != "implementation"
                            and "grok" in _resolved_roster(s)
                            and S_GROK_MODEL not in s.answered),
         build=_build_grok_model, submit=_submit_grok_model,
         owns=("grok_model",)),
    Step(S_KIMI_MODEL,
         applies=lambda s: (s.use_defaults is False
                            and not _role_selection_enabled(s)
                            and s.task_type != "implementation"
                            and "kimi" in _resolved_roster(s)
                            and S_KIMI_MODEL not in s.answered),
         build=_build_kimi_model, submit=_submit_kimi_model,
         owns=("kimi_model",)),
    Step(S_REPORT_WRITER_MODEL,
         applies=lambda s: (s.use_defaults is False
                            and not _role_selection_enabled(s)
                            and (s.task_type == "implementation"
                                 or "report-writer" in _resolved_roster(s))
                            and S_REPORT_WRITER_MODEL not in s.answered),
         build=_build_report_writer_model, submit=_submit_report_writer_model,
         owns=("report_writer_provider", "report_writer_model")),
    # directive(이번 run 의 추가 지시)는 기본값/커스터마이즈와 무관하게 항상
    # 묻는다 — 매 run 마다 줄 수 있는 입력이므로 'Use defaults' 분기 뒤에 숨기지
    # 않는다. (use_defaults is not None: defaults_or_custom 답 이후에만 등장)
    Step(S_DIRECTIVE_PICK,
         applies=lambda s: (S_DIRECTIVE_PICK not in s.answered
                            and s.use_defaults is not None),
         build=_build_directive_pick, submit=_submit_directive_pick,
         owns=("directive", "directive_pending_text")),
    Step(S_DIRECTIVE,
         applies=lambda s: (s.directive_pending_text
                            and S_DIRECTIVE not in s.answered),
         build=_build_directive, submit=_submit_directive,
         owns=("directive", "directive_pending_text")),
    Step(S_RELATED_TASKS_PICK,
         applies=lambda s: (s.use_defaults is False
                            and S_RELATED_TASKS_PICK not in s.answered),
         build=_build_related_tasks_pick, submit=_submit_related_tasks_pick,
         owns=("related_tasks_raw", "related_tasks_pending_text")),
    Step(S_RELATED_TASKS,
         applies=lambda s: (s.use_defaults is False
                            and s.related_tasks_pending_text
                            and S_RELATED_TASKS not in s.answered),
         build=_build_related_tasks, submit=_submit_related_tasks,
         owns=("related_tasks_raw", "related_tasks_pending_text")),
    # clarification(직전 phase final-report 입력)은 customize 전용이 아니다.
    # 이어가기에 필수인 직전 final-report 가 존재하면 use_defaults 와 무관하게
    # 입력 기회를 노출한다. (과거: use_defaults 게이트 뒤에 숨어 "Use defaults"
    # 를 고르면 직전 리포트가 통째로 누락됐다.)
    # implementation-planning 의 `--clarification-response` 는 같은 phase 의
    # 계획서만 받는다(`run._validate_planning_entry_inputs`). 그 계획서가 없는
    # 첫 계획 run 에서 이 화면은 `없음 / 직접 입력` 뿐이고, 직접 입력으로 넣을
    # 수 있는 파일도 없다 — 질문이 아니라 막다른 길이다(실측 2026-09-09, grok:
    # 사용자는 직전 후보비교 run 의 user-responses/ 답변을 넘길 자리를 여기서
    # 찾았다). 그 답변은 방향 선택 단계(`selected_direction_pick`)가 리포트와
    # 함께 전달하므로, 계획 run 은 이어받을 계획서가 있을 때만 여기를 묻는다.
    Step(S_CLARIFICATION_PICK,
         applies=lambda s: (S_CLARIFICATION_PICK not in s.answered
                            and s.use_defaults is not None
                            and (bool(_suggest_latest_final_report(s))
                                 if s.task_type == "implementation-planning"
                                 else (s.use_defaults is False
                                       or bool(_suggest_latest_final_report(s))))),
         build=_build_clarification_pick, submit=_submit_clarification_pick,
         owns=("clarification_response_path", "clarification_pending_text")),
    Step(S_CLARIFICATION,
         applies=lambda s: (s.clarification_pending_text
                            and S_CLARIFICATION not in s.answered),
         build=_build_clarification, submit=_submit_clarification,
         owns=("clarification_response_path", "clarification_pending_text")),
    # clarification 뒤에 온다. `_planning_rerun_selected` 는 clarification 답을 읽어
    # "이 런은 기존 계획서의 재실행" 인지 판정하는데, 이 단계가 clarification 앞에
    # 있던 동안 그 값은 언제나 비어 있었다 — 가드가 참이 될 수 없는 자리에 놓여
    # 있었다. 그 결과 재실행이어야 할 런이 매번 방향 선택으로 들어갔고,
    # implementation-option-selection 리포트가 없는 레거시 후보비교 task 는
    # 거기서 끝났다.
    Step(S_SELECTED_DIRECTION_PICK,
         applies=lambda s: (
             s.task_type == "implementation-planning"
             and not s.selected_direction_path
             and not _planning_rerun_selected(s)
             and _brief_resolved(s)
             and _base_ref_ready(s)
             and not s.base_ref_pending_text
             and S_SELECTED_DIRECTION_PICK not in s.answered
             # 고를 것이 없으면 묻지 않는다. 물으면 build 가 WizardError 를 내고
             # `current: null` 로 끝나 재프롬프트조차 불가능해진다 — 사용자가
             # 되돌릴 수 없는 막다른 길이다. 방향 없이 진행한 런은
             # `run._validate_planning_entry_inputs` 가 두 입력을 모두 이름 붙여
             # 거절하므로, 실패는 복구 가능한 자리로 옮겨간다.
             and bool(_selected_direction_candidates(s))
         ),
         build=_build_selected_direction_pick,
         submit=_submit_selected_direction_pick,
         owns=("selected_direction_path",)),
    Step(S_PR_TEMPLATE_PICK,
         applies=lambda s: (s.use_defaults is False
                            and s.task_type == "release-handoff"
                            and S_PR_TEMPLATE_PICK not in s.answered),
         build=_build_pr_template_pick, submit=_submit_pr_template_pick,
         owns=("pr_template_path", "pr_template_scope", "pr_template_pending_text")),
    Step(S_PR_TEMPLATE,
         applies=lambda s: (s.use_defaults is False
                            and s.task_type == "release-handoff"
                            and s.pr_template_pending_text
                            and S_PR_TEMPLATE not in s.answered),
         build=_build_pr_template, submit=_submit_pr_template,
         owns=("pr_template_path", "pr_template_scope", "pr_template_pending_text")),
    Step(S_PR_TEMPLATE_SCOPE,
         applies=lambda s: (s.use_defaults is False
                            and s.task_type == "release-handoff"
                            and bool(s.pr_template_path)
                            and S_PR_TEMPLATE_SCOPE not in s.answered),
         build=_build_pr_template_scope, submit=_submit_pr_template_scope,
         owns=("pr_template_scope",)),
    # 재검증 범위는 clarification-response 가 정해진 뒤에야 판정할 수 있고,
    # 확인 블록은 full 재검증 비용을 물기 전 마지막 되돌림 지점이다. 그래서
    # 이 질문은 confirm 바로 앞에 선다.
    Step(S_REVERIFY_SCOPE_PICK,
         applies=lambda s: (_ready_for_confirm(s)
                            and not s.reverify_scope_pending_text
                            and not s.reverify_scope
                            and _reverify_scope_pick_required(s)),
         build=_build_reverify_scope_pick, submit=_submit_reverify_scope_pick,
         owns=("reverify_scope", "reverify_scope_pending_text")),
    Step(S_REVERIFY_SCOPE_STAGES,
         applies=lambda s: (s.reverify_scope_pending_text
                            and S_REVERIFY_SCOPE_STAGES not in s.answered),
         build=_build_reverify_scope_stages,
         submit=_submit_reverify_scope_stages,
         owns=("reverify_scope", "reverify_scope_pending_text")),
    Step(S_FIX_CYCLE_CONFIRM,
         applies=lambda s: (_ready_for_confirm(s)
                            and not _reverify_scope_step_pending(s)
                            and _fix_cycle_confirm_required(s)
                            and not s.fix_cycle),
         build=_build_fix_cycle_confirm, submit=_submit_fix_cycle_confirm,
         owns=("fix_cycle",)),
    Step(S_CONFIRM,
         applies=lambda s: (_ready_for_confirm(s)
                            and not _reverify_scope_step_pending(s)
                            and (not _fix_cycle_confirm_required(s)
                                 or bool(s.fix_cycle))
                            and s.confirmed is None),
         build=_build_confirm, submit=_submit_confirm,
         owns=("confirmed", "edit_target", "confirmation_prompt", "confirmation_stages", "confirmation_scope", "user_authorization")),
    Step(S_EDIT_TARGET,
         applies=lambda s: s.confirmed is False and not s.edit_target,
         build=_build_edit_target, submit=_submit_edit_target,
         owns=()),
]


STEP_BY_ID = {s.id: s for s in STEPS}


def _ready_for_confirm(s: WizardState) -> bool:
    if _role_selection_enabled(s):
        try:
            if next_role_prompt(s) is not None:
                return False
        except WizardError:
            return False
        followup_ids = [
            S_DIRECTIVE_PICK,
            S_DIRECTIVE,
            S_RELATED_TASKS_PICK,
            S_RELATED_TASKS,
            S_CLARIFICATION_PICK,
            S_CLARIFICATION,
            S_PR_TEMPLATE_PICK,
            S_PR_TEMPLATE,
            S_PR_TEMPLATE_SCOPE,
        ]
        return not any(STEP_BY_ID[step_id].applies(s) for step_id in followup_ids)
    if s.use_defaults is None:
        return False
    # Worker roster is required regardless of the defaults / customize
    # branch. Skipping this check let the lead drop into confirm with no
    # worker prompt ever shown — the very failure mode this gate exists
    # to prevent.
    workers_step = STEP_BY_ID[S_WORKERS_OVERRIDE]
    if workers_step.applies(s):
        return False
    if STEP_BY_ID[S_WORKERS_CUSTOM].applies(s):
        return False
    if s.use_defaults:
        return True
    # customize: every customize-branch step must be answered or not-applicable.
    custom_ids = [S_LEAD_MODEL, S_EXECUTOR_MODEL,
                  S_CLAUDE_MODEL, S_CODEX_MODEL, S_ANTIGRAVITY_MODEL,
                  S_REPORT_WRITER_MODEL, S_DIRECTIVE, S_RELATED_TASKS,
                  S_CLARIFICATION, S_PR_TEMPLATE, S_PR_TEMPLATE_SCOPE]
    for sid in custom_ids:
        step = STEP_BY_ID[sid]
        if step.applies(s):
            return False
    return True


def _reset_from(state: WizardState, target_step: str) -> None:
    """Clear state owned by target_step and all later steps; remove their
    entries from `answered`. Used when the user picks Edit."""
    idx = next((i for i, s in enumerate(STEPS) if s.id == target_step), -1)
    if idx < 0:
        return
    role_boundary = next(
        i for i, step in enumerate(STEPS)
        if step.id == S_DEFAULTS_OR_CUSTOM
    )
    if idx < role_boundary:
        _clear_role_selection(state)
    # A later step may own a field an earlier answered step also owns —
    # `handoff_stage_pick` owns `approved_plan_path` because it resolves the
    # plan on its own. Clearing it while rewinding to a step in front of it
    # drops an answer the user never revisited; the rewound step then fails its
    # own `applies` guard, so no step is left to ask and the wizard reports
    # done while `outcome` still refuses it as incomplete.
    owned_earlier = {
        fname
        for step in STEPS[:idx]
        if step.id in state.answered
        for fname in step.owns
    }
    cleared_ids: set[str] = set()
    for position, step in enumerate(STEPS[idx:]):
        cleared_ids.add(step.id)
        is_rewind_target = position == 0
        for fname in step.owns:
            # The target's own fields always clear — that is the answer the
            # user came back to replace, even when an earlier step declares it
            # too (`task_pick` derives `task_type` for an existing task).
            if is_rewind_target or fname not in owned_earlier:
                _reset_field(state, fname)
    state.answered = [a for a in state.answered if a not in cleared_ids]
    direct_input_pending = {
        S_FEATURE_EVIDENCE: "feature_evidence_pending_text",
        S_PROJECT_EVIDENCE: "project_evidence_pending_text",
        S_ANALYSIS_TARGET: "analysis_target_pending_text",
        S_WORKERS_CUSTOM: "workers_custom_pending",
    }
    pending_field = direct_input_pending.get(target_step)
    if pending_field is not None:
        setattr(state, pending_field, True)
