"""Reverify 지시문을 수렴 상태와 라운드 계획에서 결정적으로 렌더한다.

Phase 5.5 의 재검증 워커는 리드가 손으로 쓴 지시문을 받아 왔다. 그 손에서 두
가지가 반복해서 어긋났다(실측 2026-09-09, 다른 세션의 첫 reverify 보고).

- 응답 형식 — 리드가 `- Verdict:` 불릿을 적었고 워커는 시킨 대로 썼다. 파서는
  이제 그 모양도 읽지만(`verdict_blocks._field_match`), 형식 블록을 리드가 매
  라운드 다시 쓰는 구조 자체가 결함이다.
- 근거 축약 — 리드가 `**Cited evidence**` 줄에 원 워커가 인용한 근거의 일부만
  옮겨 적었다. 검증자는 계약대로 그 줄만 열었고, `burden-not-met` 5건은 주장을
  검증한 것이 아니라 리드의 전사(轉寫)를 검증한 것이었다.

이 모듈은 그 지시문을 코드로 만든다. finding 마다 그룹의 요약·원 워커·인용
근거 줄에 더해, **원 워커의 결과 파일과 그 안의 항목 id, 그리고 원 워커의 감사
사이드카**(실행한 읽기 전용 명령과 출력이 기록된 파일)를 싣고, 그 둘을 열어도
된다고 명시한다. 검증자는 리드의 요약이 아니라 원 워커가 실제로 인용한 것을
판단한다. 응답 형식은 `verdict_blocks` 가 읽는 정본 모양 그대로다.

산출물은 프롬프트 materializer 의 `--instruction` 이 받는 본문이다. `## Instructions`
로 시작하므로 `complete_reverify_instruction` 이 모델·task type·금지 목록을 그
앞에 붙이고, 출력 계약(`templates/reverify-output-contract.md`)을 뒤에 덧붙인다.
"""
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Any, Mapping, Sequence

from .convergence_critic_prompt import analyser_results
from .worker_artifact_paths import WorkerArtifactPathError, audit_sidecar_rel


class ReverifyPromptError(ValueError):
    """지시문을 결정적으로 만들 수 없다."""


# 렌더된 지시문의 서명. 리드가 손으로 쓴 지시문은 이 줄이 없어 materialize 의
# `validate_reverify_prompt` 에서 거절된다 — 저작(convergence.md "write its output
# verbatim")만 있고 집행이 없으면 리드는 다시 손으로 쓴다.
RENDERED_BY_LINE = "**Rendered by:** okstra convergence reverify-prompt"


@dataclass(frozen=True)
class ReverifyFinding:
    """검증 큐의 finding 하나와, 그 원 워커의 실물 인용 위치."""

    finding_id: str
    summary: str
    origin_worker: str
    origin_item_id: str
    origin_evidence: str
    origin_result_path: str
    origin_audit_path: str


ADVERSARIAL_MANDATE = """Your job is to BREAK each finding below, not to confirm it. For EACH finding,
open the cited evidence directly and actively search for evidence that the claim
is wrong, overstated, or unproven. Then respond with exactly one verdict:

- **REFUTED**: You broke the claim. State the basis:
  - counter-evidence — you found contradicting evidence (give file:line or log line), OR
  - burden-not-met — you re-inspected the cited evidence and could neither confirm
    nor refute it (the claim has not proven itself).
- **SURVIVES**: You actively tried to refute it and failed — the claim withstood the
  attack. Name the attack you tried and why it failed.
- **SURVIVES-WITH-CAVEAT**: It holds, but a scope limit / extra condition / missing
  precondition exists (state it).
- **UNVERIFIABLE**: Capability, credential, network, or service state prevents you
  from opening or reproducing the cited evidence. Do not use REFUTED as a substitute.

Every verdict carries an `**Explanation**`, SURVIVES included — it is what you did,
not what the verdict already says. Only `**Basis**` is conditional. A block with a
verdict and no explanation is not collected and the whole response is refused.

The burden of proof is on the claim. If after inspecting the cited evidence you remain
uncertain, your verdict is REFUTED with basis = burden-not-met.

Inspect ONLY the evidence each finding cites and its immediate surroundings. Do NOT
re-read the task brief, instruction-set, or report template."""

_COLLABORATIVE_MANDATE = """Review the following findings discovered by other workers.
For EACH finding, respond with exactly one verdict:

- **AGREE**: The finding is valid based on the evidence presented
- **DISAGREE**: The finding is incorrect or unsupported (explain briefly why)
- **SUPPLEMENT**: The finding is valid AND you have additional supporting evidence or context
- **UNVERIFIABLE**: Capability, credential, network, or service state prevents you
  from checking this finding. Explain the unavailable capability; do not substitute DISAGREE.

Every verdict carries an `**Explanation**`, AGREE included — name the evidence you
checked. A block with a verdict and no explanation is not collected and the whole
response is refused.

Do NOT re-analyze the original source materials. Judge based on the evidence provided."""

# 근거 접근 규칙. `**Cited evidence**` 는 리드의 요약이고, 완전한 인용은 원 워커의
# 결과 항목이다. 검증자가 요약만 열고 "이 근거로는 입증되지 않는다" 고 답하던
# 자리를 막는다.
_EVIDENCE_ACCESS = """The `**Cited evidence**` line is the lead's summary of what the origin worker cited.
The complete citation is the origin worker's own item: before judging, open the
`**Origin item**` file at the named `### <item-id>` section and read every path, line,
command, and quote it cites. The `**Origin audit sidecar**` records the read-only
commands that worker ran and their output; it counts as cited evidence and you may
open it. Judge the claim against what the origin worker actually cited, never against
the summary line alone."""

ADVERSARIAL_RESPONSE = """### <finding-id>
**Verdict**: REFUTED | SURVIVES | SURVIVES-WITH-CAVEAT | UNVERIFIABLE
**Basis** (only if REFUTED): counter-evidence | burden-not-met
**Explanation** (required for every verdict, SURVIVES included): <2-3 sentences; for
SURVIVES say what you attacked and why the attack failed; for counter-evidence include
the file:line you found>"""

_COLLABORATIVE_RESPONSE = """### <finding-id>
**Verdict**: AGREE | DISAGREE | SUPPLEMENT | UNVERIFIABLE
**Explanation** (required for every verdict, AGREE included): <2-3 sentences>"""


def _nonempty_string(value: Any) -> str:
    return value if isinstance(value, str) and value.strip() else ""


def plan_row_for_worker(plan: Mapping[str, Any], worker_id: str) -> Mapping[str, Any]:
    """계획의 `dispatches[]` 에서 이 워커의 행 하나. 검증기와 같은 규칙으로 맞춘다.

    `validators/validate-run.py` `_plan_dispatch_finding_ids` 처럼 `worker` 가
    그대로 같거나 `<worker>-worker` 가 같으면 그 행이다.
    """
    dispatches = plan.get("dispatches")
    if not isinstance(dispatches, list):
        raise ReverifyPromptError("round plan has no dispatches array")
    matched = [
        row for row in dispatches
        if isinstance(row, Mapping)
        and (row.get("worker") == worker_id or f"{row.get('worker')}-worker" == worker_id)
    ]
    if len(matched) != 1:
        planned = ", ".join(
            _nonempty_string(row.get("worker")) or "?" for row in dispatches
            if isinstance(row, Mapping)
        )
        raise ReverifyPromptError(
            f"round plan dispatches nothing to `{worker_id}`; planned workers: "
            f"{planned or 'none'}"
        )
    finding_ids = matched[0].get("findingIds")
    if not isinstance(finding_ids, list) or not finding_ids:
        raise ReverifyPromptError(f"round plan row for `{worker_id}` has no findingIds")
    return matched[0]


def reverify_findings(
    groups: Mapping[str, Any],
    plan: Mapping[str, Any],
    worker_id: str,
    *,
    project_root: Path,
    run_dir: Path,
) -> list[ReverifyFinding]:
    """계획 행의 finding 을 계획 순서대로, 원 워커의 실물 인용 위치와 함께."""
    row = plan_row_for_worker(plan, worker_id)
    by_id = {
        _nonempty_string(group.get("findingId")): group
        for group in groups.get("groups") or []
        if isinstance(group, Mapping) and _nonempty_string(group.get("findingId"))
    }
    result_paths = {
        result.worker_id: result.result_path
        for result in analyser_results(groups, project_root=project_root, run_dir=run_dir)
    }
    findings: list[ReverifyFinding] = []
    for finding_id in row["findingIds"]:
        group = by_id.get(str(finding_id))
        if group is None:
            raise ReverifyPromptError(
                f"round plan names `{finding_id}`, which the grouping does not carry"
            )
        origin = _nonempty_string(group.get("originWorker"))
        discovered = group.get("discoveredBy")
        origin_item = (
            _nonempty_string((discovered.get(origin) or {}).get("itemId"))
            if isinstance(discovered, Mapping) and isinstance(discovered.get(origin), Mapping)
            else ""
        )
        result_path = result_paths.get(origin, "")
        if not origin or not origin_item or not result_path:
            raise ReverifyPromptError(
                f"finding `{finding_id}` has no resolvable origin item "
                f"(origin worker `{origin or '?'}`, item `{origin_item or '?'}`)"
            )
        try:
            audit_path = audit_sidecar_rel(result_path)
        except WorkerArtifactPathError as exc:
            raise ReverifyPromptError(str(exc)) from exc
        findings.append(ReverifyFinding(
            finding_id=str(finding_id),
            summary=_nonempty_string(group.get("summary")),
            origin_worker=origin,
            origin_item_id=origin_item,
            origin_evidence=_nonempty_string(group.get("originEvidence")),
            origin_result_path=result_path,
            origin_audit_path=audit_path,
        ))
    return findings


def reverify_prompt_body(
    *,
    task_key: str,
    round_number: int,
    adversarial: bool,
    findings: Sequence[ReverifyFinding],
) -> str:
    """reverify 지시문 파일 본문. 같은 입력이면 같은 바이트를 낸다."""
    if not _nonempty_string(task_key):
        raise ReverifyPromptError("convergence groups carry no taskKey")
    if not findings:
        raise ReverifyPromptError("no findings to verify")
    mode = "ADVERSARIAL re-verification" if adversarial else "re-verification"
    mandate = ADVERSARIAL_MANDATE if adversarial else _COLLABORATIVE_MANDATE
    response = ADVERSARIAL_RESPONSE if adversarial else _COLLABORATIVE_RESPONSE
    rows = [
        "## Instructions\n\n",
        f"{RENDERED_BY_LINE}\n\n",
        f"Perform {mode} for {task_key} (round {round_number}).\n\n",
        mandate, "\n\n",
        _EVIDENCE_ACCESS, "\n\n",
        "## Findings to verify\n",
    ]
    for finding in findings:
        rows.append(f"\n### {finding.finding_id}: {finding.summary or '(no summary)'}\n")
        rows.append(f"**Origin**: {finding.origin_worker}\n")
        rows.append(f"**Cited evidence**: {finding.origin_evidence or '(none recorded)'}\n")
        rows.append(
            f"**Origin item**: `{finding.origin_result_path}` — section "
            f"`### {finding.origin_item_id}`\n"
        )
        rows.append(f"**Origin audit sidecar**: `{finding.origin_audit_path}`\n")
    rows.append("\n## Response format\n\n")
    rows.append(
        "One block per finding, headed by the finding id at exactly three hashes. "
        "Field labels are bold with the colon outside (`**Verdict**: …`); the "
        "collector also reads `**Verdict:** …` and `- Verdict: …` as the same field.\n\n"
    )
    rows.append(response.replace("<finding-id>", findings[0].finding_id))
    rows.append("\n")
    if len(findings) > 1:
        rows.append(f"\n### {findings[1].finding_id}\n**Verdict**: ...\n")
    return "".join(rows)
