"""Phase / workflow state computation.

bash `compute_workflow_render_context` 의 python 구현. (task-type, current
task/run status, render-only flag) 만 받아서 WORKFLOW_* 필드 dict 를 돌려준다.

다음 phase 는 이 모듈이 계산하지 않는다. 포인터의 shape·승격·투영은
`okstra_ctl.next_phase` 하나가 소유하고, 값은 리포트가 저작한 라우팅에서
투영된다.

task-type 별 PHASE_ALLOWED_OUTPUTS 본문 매핑은 모듈 상수(`PHASE_RULES`)로 보유하고,
PHASE_FORBIDDEN_ACTIONS 는 `prompts/profiles/forbidden-actions.json` SSOT 에서 로드한다.
"""
from __future__ import annotations

import json
from pathlib import Path
from .json_boundary import load_owned_object

PHASE_SEQUENCE = [
    "requirements-discovery",
    "error-analysis",
    "implementation-option-selection",
    "implementation-planning",
    "implementation",
    "final-verification",
    "release-handoff",
]

REQUIREMENTS_DISCOVERY_ROUTING_TARGETS = frozenset(
    {"error-analysis", "implementation-option-selection"}
)

ERROR_ANALYSIS_ROUTING_DIRECTIONS = {
    "error-analysis": "continue-investigation",
    "implementation-option-selection": "begin-option-selection",
}

# Phase 별 allowed outputs. bash heredoc 원문 그대로 옮긴 값 — 들여쓰기와 백틱은
# prompt template 에 그대로 박혀 lead 가 읽는다. forbidden actions 는 이 dict 가
# 아니라 prompts/profiles/forbidden-actions.json (load_phase_forbidden) 이 SSOT.
PHASE_RULES: dict[str, dict[str, str]] = {
    "technical-verification": {
        "allowed": (
            "  - falsifiable experiment plans for the frozen unresolved facts\n"
            "  - dependency installation, source experiments, tests and builds only in this run's experiment copies\n"
            "  - command logs, observed signals and per-fact supported/refuted/inconclusive/not-run results\n"
            "  - return to implementation-option-selection with evidence, without adoption or plan approval"
        ),
    },
    "requirements-discovery": {
        "allowed": (
            "  - work-category classification (bugfix / feature / refactor / ops / improvement)\n"
            "  - routing decision as the `routing` object `{nextTaskType, readyWhen, rationale}`: `nextTaskType` is `error-analysis` or `implementation-option-selection` and nothing else — this phase never hands off directly to planning or implementation. Prose alone does not route the task — Phase 7 projects `workflow.nextRecommendedPhase` from `nextTaskType`\n"
            "  - missing-input list and clarification requests\n"
            "  - approval / confirmation checkpoints recorded for the next phase\n"
            "  - one endStateCoverage row per brief end-state id (this phase authors no goal of its own)"
        ),
    },
    "improvement-discovery": {
        "allowed": (
            "  - improvement candidate discovery within the lens whitelist defined in `scripts/okstra_ctl/improvement_lenses.py`\n"
            "  - top-N ranking (default 8, brief `candidate-cap` overrides within 1..12)\n"
            "  - per-candidate columns Lens / Scope / Severity / Effort / Consensus / Source workers / Recommended next-phase / Expected behavior after / Evidence (path:line)\n"
            "  - Phase 1.5 reflect-back grilling log at `runs/improvement-discovery/<seq>/state/phase-1.5-grilling.md`\n"
            "  - Phase 5.5 consensus classification (full / partial / contested / worker-unique / unverified)"
        ),
    },
    "project-analysis": {
        "allowed": (
            "  - components, dependency directions, entry points, repositories, and external integrations\n"
            "  - shallow feature index and scan scope\n"
        ),
    },
    "feature-analysis": {
        "allowed": (
            "  - confirmed target and normal, alternate, and failure flows\n"
            "  - rules, state transitions, external calls, and test coverage scope\n"
        ),
    },
    "change-impact-analysis": {
        "allowed": (
            "  - preserved behavior, impacted items, and dependency propagation\n"
            "  - test and operational impact\n"
            "  - constraints and open decisions passed to implementation-planning\n"
        ),
    },
    "error-analysis": {
        "allowed": (
            "  - symptom and trigger evidence\n"
            "  - root-cause hypotheses with supporting citations\n"
            "  - reproduction gaps and missing observability\n"
            "  - validation paths to confirm or refute hypotheses\n"
            "  - one endStateCoverage row per brief end-state id (this phase authors no goal of its own)"
        ),
    },
    "implementation-option-selection": {
        "allowed": (
            "  - candidate-comparison evidence for up to three options per worker\n"
            "  - preselected-validation of exactly ONE candidate: every analyser returns its own feasibility verdict on that one direction, `candidateAudit` stays empty, and no analyser generates an alternative. It is not a re-evaluation of the merged set — `okstra_ctl.implementation_options._validate_mode` rejects a preselected-validation report carrying more than one option or a non-empty audit\n"
            "  - a ranked display of at most three options with requirement mappings\n"
            "  - an audit record for every rejected candidate\n"
            "  - one endStateCoverage row per brief end-state id (this phase authors no goal of its own)"
        ),
    },
    "implementation-planning": {
        "allowed": (
            "  - pre-planning context exploration notes (files/interfaces inspected, recent commits scanned, ambiguities flagged)\n"
            "  - selected-direction planning: realize the one validated selected direction without reopening candidate comparison, with a File Structure list (Create/Modify/Delete with one-line responsibility per file), affected interfaces, and blast-radius estimate\n"
            "  - legacy candidate-comparison planning only: at least two implementation option candidates, each with a File Structure list (Create/Modify/Delete with one-line responsibility per file), affected interfaces, and blast-radius estimate\n"
            "  - legacy candidate-comparison planning only: a trade-off matrix across options (complexity, risk, reversibility, test cost, rollout cost) and recommended option with rationale tied to isolation / single-responsibility / YAGNI principles\n"
            "  - bite-sized stepwise execution order for the selected direction or legacy recommended option (each step ~2-5 min, exact file paths and commands, TDD ordering when applicable, no placeholders)\n"
            "  - dependency / migration risk assessment, validation checklist (pre / mid / post with exact commands), rollback strategy with revert path and trigger signal\n"
            "  - every unresolved ambiguity registered as a `Blocks=approval` row in the `## 1. Clarification Items` table (do NOT create a separate `Open Questions` block under `5.5.x` — the unified table is the single home)\n"
            "  - report record `frontmatter.approved: false` awaiting `--approve` or the in-session wizard\n"
            "  - self-review confirmation (spec coverage, placeholder scan, internal consistency, ambiguity, scope)\n"
            "  - one endStateCoverage row per brief end-state id, each mapped to the R-NNN row that carries it"
        ),
    },
    "implementation": {
        "allowed": (
            "  - approved-plan reference and quoted user-approval evidence\n"
            "  - commit list with SHA, message, and the plan step each commit satisfies\n"
            "  - `git diff --stat <base>..HEAD` summary plus per-file one-line change summary\n"
            "  - `Out-of-plan edits` block listing every file touched outside the approved plan with rationale (empty block preferred)\n"
            '  - validation evidence: actual stdout/stderr and exit code for every pre / mid / post command from the plan (no paraphrased "tests pass")\n'
            "  - TDD evidence for TDD-applicable steps: failing-test output before implementation commit and passing-test output after, with framing SHAs\n"
            "  - per-verifier sections for every verifier in the resolved roster, with an independent verdict (PASS / CONCERNS / FAIL) and cited diff snippets; dissent is preserved by the Okstra lead\n"
            "  - rollback verification (advisory, never blocks — record the revert path for a human; `result` is ok / not-applicable / advisory — human-run)\n"
            "  - routing recommendation as the `routingRecommendation` object `{target, rationale}`: `target` is one of `final-verification`, `error-analysis`, `implementation-planning`, `implementation`, and `rationale` is why that one and nothing else. Prose alone does not route the task — Phase 7 projects `workflow.nextRecommendedPhase` from `target`"
        ),
    },
    "final-verification": {
        "allowed": (
            "  - acceptance verdict with requirement coverage assessment\n"
            "  - residual risk and regression notes\n"
            "  - routing recommendation as the `routingRecommendation` object `{target, rationale}`: `target` is one of `release-handoff`, `release-handoff(stage-group)`, `error-analysis`, `implementation-option-selection`, `implementation-planning`, `implementation`, `done`, and `rationale` ties that choice to the verdict and the blocker list. Both `release-handoff` forms require a release-ready verdict — `accepted`, or `conditional-accept` with every condition declaring `blocksReleaseHandoff: false` (`okstra_ctl.release_gate.release_handoff_allowed`); plain `release-handoff` additionally requires `whole-task` verification scope. Prose alone does not route the task — Phase 7 projects `workflow.nextRecommendedPhase` from `target`"
        ),
    },
    "release-handoff": {
        "allowed": (
            "  - entering this phase only when the cited final-verification report's verdict is release-ready — `Verdict Token` exactly `accepted`, or exactly `conditional-accept` with every Conditional Acceptance Condition row declaring `blocksReleaseHandoff: false` (`okstra_ctl.release_gate.release_handoff_allowed`)\n"
            "  - asking the user (via `AskUserQuestion` / interactive prompt) which delivery action to take: `local checkout`, `push + PR`, or `skip` (end the run)\n"
            "  - asking the user to pick a PR base branch from `staging` | `preprod` | `prod` | `main` | `dev` | a user-supplied branch name\n"
            "  - drafting PR title and PR body **inline as the Okstra lead** (no drafter worker and no delegated dispatch); the lead reviews its own draft with the user before any mutating git / gh command runs\n"
            "  - read-only git inspection: `git status`, `git diff`, `git log`, `git rev-parse`\n"
            "  - pushing the current feature branch to its origin remote via `git push -u origin <current-branch>` (the feature branch only — NEVER the base branch)\n"
            "  - creating a pull request via `gh pr create --base <chosen-base> --head <current-branch>`; if a PR with the same head already exists, surface its URL and skip creation\n"
            "  - the lead writes the final report directly (no `Report writer worker` dispatch); the report still conforms to the standard final-report template"
        ),
    },
}

PHASE_RULES_UNKNOWN = {
    "allowed": "  - outputs defined by the active task type",
}


def render_forbidden(items: list[str]) -> str:
    """Render a forbidden-action list to the `  - item` bullet block injected
    into the launch prompt boundary and the profile body."""
    return "\n".join(f"  - {item}" for item in items)


def load_phase_forbidden(workspace_root: Path) -> dict[str, str]:
    """Load the forbidden-actions SSOT and render each phase to its bullet block.

    `workspace_root` is the resolved okstra asset root (repo or runtime); the
    JSON lives beside the profile `.md` files at
    `prompts/profiles/forbidden-actions.json`.
    """
    path = Path(workspace_root) / "prompts" / "profiles" / "forbidden-actions.json"
    data = load_owned_object(path, artifact="forbidden actions profile")
    if "unknown" not in data:
        # compute_workflow_state falls back to the "unknown" entry for any
        # unmapped task_type; without it a missing/renamed key would surface as
        # a KeyError deep in prompt assembly instead of here at the SSOT seam.
        raise ValueError(
            f"forbidden-actions SSOT at {path} is missing the required "
            "'unknown' fallback entry"
        )
    return {phase: render_forbidden(items) for phase, items in data.items()}


def phase_position(task_type: str) -> str:
    """이 phase 가 생애주기 몇 번째인지 — `"5 of 7"`.

    리드는 자기가 어느 칸을 도는지만 알고 지도를 못 본다. 다음 phase 이름은
    이 run 의 리포트가 정하므로 계속 감추지만(`WORKFLOW_NEXT_RECOMMENDED_PHASE`
    가 빈 문자열인 이유), 위치를 감출 이유는 없다 — 위치를 모르면 중간 단계를
    도는 run 이 자기가 마지막인 것처럼 결론을 낸다.

    `PHASE_SEQUENCE` 밖의 사이드트랙 task-type(improvement-discovery 등)은
    생애주기 칸이 없으므로 빈 문자열이다.
    """
    if task_type not in PHASE_SEQUENCE:
        return ""
    return f"{PHASE_SEQUENCE.index(task_type) + 1} of {len(PHASE_SEQUENCE)}"


def compute_workflow_state(
    *,
    task_type: str,
    current_run_status: str,
    current_task_status: str,
    render_only: bool,
    forbidden_by_phase: dict[str, str],
    work_category: str = "",
) -> dict:
    """WORKFLOW_* + PHASE_* 값을 dict 로 돌려준다."""
    if current_run_status == "in-progress":
        phase_state = "in-progress"
    elif current_run_status == "prepared":
        phase_state = "prepared"
    elif current_run_status in ("error", "timeout", "contract-violated"):
        phase_state = "blocked"
    elif current_run_status == "completed":
        phase_state = "completed"
    else:
        if current_task_status in (
            "instruction-set-generated",
            "ready-for-lead",
            # Compatibility with task manifests prepared before provider-neutral statuses.
            "ready-for-claude",
            "claude-session-started",
        ):
            phase_state = "prepared"
        else:
            phase_state = "not-started"

    if render_only:
        checkpoint_label = "instruction-set-prepared"
    elif current_run_status == "in-progress":
        checkpoint_label = "interactive-session-handoff"
    else:
        checkpoint_label = "run-prepared"

    rules = PHASE_RULES.get(task_type, PHASE_RULES_UNKNOWN)
    last_completed = task_type if current_run_status == "completed" else ""

    resolved_work_category = (work_category or "").strip() or "unknown"

    return {
        "WORKFLOW_WORK_CATEGORY": resolved_work_category,
        "WORKFLOW_CURRENT_PHASE": task_type,
        "WORKFLOW_CURRENT_PHASE_STATE": phase_state,
        "WORKFLOW_PHASE_POSITION": phase_position(task_type),
        # 값이 아니라 자리표시자다. 실제 포인터는 리포트 라우팅에서 투영돼
        # 매니페스트의 `workflow.nextRecommendedPhase` 구조체로 저장된다.
        # 이 ctx 값은 그 구조체로 대체되지 않는다 — 남은 소비자는
        # `render.py` 의 `_active_workflow` 하나이고, 그것은 이 빈 문자열을
        # 승격해 phase 없는 pending 포인터를 active-run-context 에 적는다.
        # `prompts/launch.template.md` 는 더 이상 이 토큰을 쓰지 않는다.
        # 다음 phase 이름을 문장에 끼워 넣던 두 줄이, 이름 없이 읽히는 문장으로
        # 바뀌었기 때문이다.
        "WORKFLOW_NEXT_RECOMMENDED_PHASE": "",
        "WORKFLOW_LAST_COMPLETED_PHASE": last_completed,
        "WORKFLOW_AWAITING_APPROVAL": "false",
        "WORKFLOW_LAST_SAFE_CHECKPOINT_LABEL": checkpoint_label,
        "PHASE_ALLOWED_OUTPUTS": rules["allowed"],
        "PHASE_FORBIDDEN_ACTIONS": forbidden_by_phase.get(
            task_type, forbidden_by_phase["unknown"]
        ),
    }
