"""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]] = {
    "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)"
        ),
    },
    "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 that re-evaluates the merged candidate set\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 the `accepted` verdict; 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 Token` is exactly `accepted`\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 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.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"]
        ),
    }
