"""run 갈래의 교정 원장 대조 — 매니페스트가 정한 스키마·소스로 `check_corrections` 를 돌린다.

`materialize --corrections`, `check-corrections`, `apply-corrections` 가 같은 대조
함수를 부른다. 스키마는 조립(`report_assembly.assemble_report`)이 쓰는 것과 같은
계약 3.0 완성 리포트 스키마이고, task 의미 검증기의 입력(요구 id 순서·참여
analyser)은 작성자가 받는 합성 묶음에서 가져온다 — 두 소비자가 다른 규칙을 보지
않게 하기 위해서다.

`apply-corrections` 는 replace·remove·add·move 와 계산된 단계 수를 직접 적용한다.
rewrite 는 작성자가 제출한 교체 값을 대조한 뒤 적용하고, 활동 원장에
`lead-correction-applied` 행과 교체 값 파일 근거를 남긴다.
"""
from __future__ import annotations

import os
from pathlib import Path
import runpy
import tempfile
from typing import Any, Callable, Mapping

from ..activity import (
    ActivityProjectionError,
    agent_activity_rows,
    record_activity,
)
from ...final_report_schema import load_schema_version
from ...json_boundary import load_owned_object
from ...implementation_options import validate_implementation_option_selection
from ...report_contract import TASK_TYPE_DATA_PROPERTY
from ...report_assembly import validate_plan_draft
from ...report_corrections import (
    CorrectionsCheck,
    check_corrections,
    load_corrections,
    render_applied_narrative,
)
from ...report_inputs import report_narrative_path, uses_report_contract_v3
from ...paths import STAGE_VALIDATOR_RELATIVE, find_asset_root
from ...report_narrative import task_narrative_errors
from ...report_synthesis_packet import (
    ReportSynthesisPacketError,
    ReportSynthesisPacket,
    build_report_synthesis_packet,
)
from .inputs import AgentPromptCliError, _authorized_path, _relative


LEAD_CORRECTION_ACTIVITY_KIND = "lead-correction-applied"
# 활동 원장의 `agent` 허용값 중 리드를 뜻하는 고정 이름(`activity._validate_activity_ownership`).
_LEAD_AGENT = "okstra-lead"


def run_corrections_check(
    *,
    project_root: Path,
    manifest: Mapping[str, Any],
    active_context: Mapping[str, Any],
    team_state: Mapping[str, Any],
    corrections_path: Path,
    narrative_path: Path,
    rewrite_results: Mapping[str, Any] | None = None,
) -> CorrectionsCheck:
    """원장을 이 run 의 기준 서사에 대조한다. 결함은 `CorrectionsCheck.defects` 에 모인다."""
    ledger, load_defects = load_corrections(corrections_path)
    if load_defects:
        return CorrectionsCheck(ledger, (), None, False, {}, tuple(load_defects))
    if not uses_report_contract_v3(manifest):
        return CorrectionsCheck(ledger, (), None, False, {}, (
            "owner=lead correction=ledger path=- reason=corrections require report "
            "contract 3.0 (a Markdown narrative); this run uses an older contract",
        ))
    contract = manifest.get("agentContract")
    authorized = contract.get("authorizedPaths") if isinstance(contract, Mapping) else None
    authorized = authorized if isinstance(authorized, Mapping) else {}
    try:
        base_path = _authorized_path(
            project_root,
            str(ledger.get("baseNarrativePath") or ""),
            authorized.get("resultRoots"),
            "base narrative",
            must_exist=True,
        )
    except AgentPromptCliError as exc:
        return CorrectionsCheck(ledger, (), None, False, {}, (
            f"owner=lead correction=ledger path={ledger.get('baseNarrativePath')} reason={exc}",
        ))
    try:
        packet = build_report_synthesis_packet(
            project_root=project_root,
            manifest=manifest,
            active_context=active_context,
            team_state=team_state,
            narrative_path=narrative_path,
        )
    except ReportSynthesisPacketError as exc:
        return CorrectionsCheck(ledger, (), None, False, {}, tuple(
            f"owner={issue.owner} correction=ledger path={issue.path} "
            f"reason=synthesis packet source defect: {issue.reason}"
            for issue in exc.issues
        ))
    return check_corrections(
        ledger=ledger,
        base_narrative=base_path.read_text(encoding="utf-8"),
        schema=load_schema_version("3.0"),
        block_rules=packet.block_rules,
        semantic_validator=_correction_semantic_validator(
            project_root, manifest, narrative_path, packet,
            rewrite_results is None and any(item.get("kind") == "rewrite" for item in ledger["corrections"]),
        ),
        rewrite_results=rewrite_results,
    )


def _correction_semantic_validator(
    project_root: Path, manifest: Mapping[str, Any], narrative_path: Path,
    packet: ReportSynthesisPacket, pending_rewrites: bool,
) -> Callable[[dict[str, Any]], list[str]] | None:
    task_type = str(manifest.get("taskType") or "")
    schema = load_schema_version("3.0")
    semantic_validator = lambda data: task_narrative_errors(data, schema, task_type)
    if task_type == "implementation-option-selection":
        block_key = TASK_TYPE_DATA_PROPERTY[task_type]
        original_ids = packet.original_requirement_ids
        analysers = packet.participating_analysers

        def semantic_validator(data: dict[str, Any]) -> list[str]:
            block = data.get(block_key)
            return task_narrative_errors(data, schema, task_type) + [
                f"{block_key}: {error}"
                for error in validate_implementation_option_selection(
                    block if isinstance(block, Mapping) else {}, original_ids, analysers,
                )
            ]
    elif task_type == "implementation-planning":
        validator_root = find_asset_root(STAGE_VALIDATOR_RELATIVE)
        if validator_root is None:
            raise AgentPromptCliError("cannot locate implementation planning validator")
        stage_validator = runpy.run_path(str(validator_root.joinpath(*STAGE_VALIDATOR_RELATIVE)))
        granted = stage_validator["user_bypassed_stages_for_plan"](narrative_path)

        def semantic_validator(data: dict[str, Any]) -> list[str]:
            planning = data.get("implementationPlanning")
            stage_errors = [] if pending_rewrites else stage_validator["collect_data_validation_errors"](
                planning if isinstance(planning, dict) else {}, granted,
            )
            return [
                *task_narrative_errors(data, schema, task_type),
                *validate_plan_draft(data, project_root, manifest),
                *[f"implementationPlanning: {error}" for error in stage_errors],
            ]

    return semantic_validator


def corrections_payload(
    check: CorrectionsCheck, *, project_root: Path, corrections_path: Path,
) -> dict[str, Any]:
    """`check-corrections --json` 의 출력."""
    return {
        "ok": check.ok,
        "correctionsPath": _relative(project_root, corrections_path),
        "mechanical": check.mechanical,
        "baseNarrativeSha256": check.ledger.get("baseNarrativeSha256"),
        "corrections": [
            {
                **item,
                "constraints": list(check.constraints.get(str(item.get("id")), ())),
            }
            for item in check.corrections
        ],
        "defects": list(check.defects),
    }


def run_corrections_apply(
    *,
    project_root: Path,
    manifest: Mapping[str, Any],
    manifest_path: Path,
    active_context: Mapping[str, Any],
    team_state: Mapping[str, Any],
    corrections_path: Path,
    rewrite_results_path: Path | None = None,
) -> dict[str, Any]:
    """기계적 원장을 서사에 쓰고 활동 행을 남긴다 — `apply-corrections` 의 본체.

    순서: 대조(결함이면 거절) → rewrite 없음 확인 → 활동 계약·중복 적용 확인 →
    서사 원자적 쓰기 → 활동 행. 활동 계약이 없는 run 은 서사를 쓰기 전에
    거절한다: 행 없는 기계 수정은 감사에서 작성자의 것으로 읽힌다.
    """
    if not uses_report_contract_v3(manifest):
        raise AgentPromptCliError(
            "apply-corrections requires report contract 3.0 (a Markdown "
            "narrative); this run uses an older contract"
        )
    narrative_path = report_narrative_path(project_root, manifest)
    events_value = manifest.get("leadEventsPath")
    if manifest.get("activityContractVersion") != 1 or not (
        isinstance(events_value, str) and events_value.strip()
    ):
        raise AgentPromptCliError(
            "apply-corrections records a `lead-correction-applied` activity row, "
            "and this run manifest declares no activity contract "
            "(activityContractVersion 1 with leadEventsPath); dispatch the "
            "writer with --corrections instead"
        )
    corrections_rel = _relative(project_root, corrections_path)
    rewrite_results = (
        load_owned_object(rewrite_results_path, artifact="report writer rewrite results")
        if rewrite_results_path is not None else None
    )
    check = run_corrections_check(
        project_root=project_root,
        manifest=manifest,
        active_context=active_context,
        team_state=team_state,
        corrections_path=corrections_path,
        narrative_path=narrative_path,
        rewrite_results=rewrite_results,
    )
    return _apply_checked_corrections(
        project_root, manifest_path, narrative_path, corrections_path, check, rewrite_results_path,
    )


def _apply_checked_corrections(
    project_root: Path, manifest_path: Path, narrative_path: Path,
    corrections_path: Path, check: CorrectionsCheck, rewrite_results_path: Path | None,
) -> dict[str, Any]:
    corrections_rel = _relative(project_root, corrections_path)
    if check.defects:
        raise AgentPromptCliError(
            "report-writer corrections defects: " + "; ".join(check.defects)
        )
    if not check.mechanical:
        pending = ", ".join(
            str(item.get("id")) for item in check.corrections
            if item.get("kind") == "rewrite"
        )
        raise AgentPromptCliError(
            f"corrections ledger has rewrite entries ({pending}): a rewrite "
            "needs a writer round — materialize the report-writer prompt with "
            "--corrections instead of applying"
        )
    base_path = project_root / str(check.ledger.get("baseNarrativePath") or "")
    if os.path.normpath(base_path) == os.path.normpath(narrative_path):
        raise AgentPromptCliError(
            "baseNarrativePath is the live narrative "
            f"({_relative(project_root, narrative_path)}); applying would "
            "overwrite the only copy of the attempt being corrected. Copy it "
            "to worker-results/report-writer-narrative-a<N>-<task-type>-<seq>.md "
            "first and point the ledger at that copy"
        )
    previous = _already_applied(project_root, manifest_path, corrections_rel)
    if previous is not None:
        raise AgentPromptCliError(
            f"corrections ledger {corrections_rel} was already applied as "
            f"activity {previous}; a further correction needs a new ledger "
            "whose baseNarrativePath is the narrative that apply wrote"
        )
    applied_ids = [str(item.get("id")) for item in check.corrections]
    narrative_rel = _relative(project_root, narrative_path)
    rendered = render_applied_narrative(check, load_schema_version("3.0"))
    if narrative_path.is_file() and narrative_path.read_text(encoding="utf-8") not in (
        base_path.read_text(encoding="utf-8"), rendered,
    ):
        raise AgentPromptCliError("live narrative changed after the correction base was preserved; create a new ledger")
    _write_text_atomic(narrative_path, rendered)
    return _record_correction_application(
        project_root, manifest_path, narrative_rel, corrections_rel, applied_ids, rewrite_results_path,
    )


def _record_correction_application(
    project_root: Path, manifest_path: Path, narrative_rel: str,
    corrections_rel: str, applied_ids: list[str], rewrite_results_path: Path | None,
) -> dict[str, Any]:
    details = {
        "kind": LEAD_CORRECTION_ACTIVITY_KIND,
        "agent": _LEAD_AGENT,
        "summary": (
            f"Applied {len(applied_ids)} correction(s) "
            f"({', '.join(applied_ids)}) from {corrections_rel} to the report "
            + ("narrative using supplied writer replacements" if rewrite_results_path
               else "narrative without a writer round")
        ),
        "planItemIds": [],
        "resultPath": narrative_rel,
        "commands": [],
        "evidenceRefs": [corrections_rel, *applied_ids] + (
            [_relative(project_root, rewrite_results_path)] if rewrite_results_path else []
        ),
        "outcome": "completed",
    }
    try:
        event = record_activity(project_root, manifest_path, details)
    except ActivityProjectionError as exc:
        raise AgentPromptCliError(
            f"narrative written to {narrative_rel} but the activity row was "
            f"refused: {exc}; re-run apply-corrections once the cause is fixed "
            "(the same ledger applies again to the same base)"
        ) from exc
    return {
        "ok": True,
        "correctionsPath": corrections_rel,
        "narrativePath": narrative_rel,
        "appliedCorrectionIds": applied_ids,
        "activityId": event.details.get("activityId"),
        "activityLine": (
            f"ACTIVITY: id={event.details.get('activityId')} agent={_LEAD_AGENT} "
            f"kind={LEAD_CORRECTION_ACTIVITY_KIND} corrections={corrections_rel} "
            f"result={narrative_rel}"
        ),
    }


def _already_applied(
    project_root: Path, manifest_path: Path, corrections_rel: str,
) -> str | None:
    """이 원장을 이미 적용한 활동 행의 id. 없으면 None."""
    try:
        rows = agent_activity_rows(project_root, manifest_path)
    except ActivityProjectionError as exc:
        raise AgentPromptCliError(f"activity ledger is unreadable: {exc}") from exc
    for row in rows:
        refs = row.get("evidenceRefs")
        if (
            row.get("kind") == LEAD_CORRECTION_ACTIVITY_KIND
            and isinstance(refs, list)
            and corrections_rel in refs
        ):
            return str(row.get("activityId"))
    return None


def _write_text_atomic(path: Path, text: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    descriptor, temporary = tempfile.mkstemp(
        prefix=f".{path.name}.", suffix=".tmp", dir=path.parent,
    )
    try:
        with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
            handle.write(text)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(temporary, path)
    finally:
        if os.path.exists(temporary):
            os.unlink(temporary)
