"""미확정 사실의 시험 입력과 관측 결과를 연결하며 도입 승인은 만들지 않는다."""

from __future__ import annotations

import hashlib
import re
from collections.abc import Mapping
from pathlib import Path
from typing import Any, TypedDict

from .clarification_items import USER_INPUT_BLOCKS, progress_blocking_ids
from .final_report_schema import load_schema_for_data, validate
from .implementation_direction import validate_task_artifact_path
from .json_boundary import load_owned_object_snapshot, serialize_owned_object


class VerificationFact(TypedDict):
    id: str
    candidateId: str
    factIndex: int
    fact: str
    whyItMatters: str
    evidence: list[str]


class VerificationInput(TypedDict):
    sourceReport: str
    sourceDataSha256: str
    scope: str
    facts: list[VerificationFact]


class TechnicalVerificationError(ValueError):
    """시험 입력 또는 관측 결과가 원본 사실과 일치하지 않는다."""


def technical_verification_facts(data: Mapping[str, Any]) -> list[VerificationFact]:
    """안전 차단이 없는 후보의 명시적인 기술 검증 사실만 추출한다."""
    blockers = progress_blocking_ids(
        data.get("clarificationItems"), USER_INPUT_BLOCKS, report_data=data
    )
    if blockers:
        raise TechnicalVerificationError(
            "unresolved user decisions: " + ", ".join(blockers)
        )
    selection = data.get("implementationOptionSelection") or {}
    facts: list[VerificationFact] = []
    for candidate in selection.get("candidateAudit") or []:
        if candidate.get("safetyBlockers"):
            continue
        for index, fact in enumerate(candidate.get("unresolvedFeasibilityFacts") or []):
            if fact.get("resolutionKind") != "technical-verification":
                continue
            facts.append(
                {
                    "id": f"TV-{len(facts) + 1:03d}",
                    "candidateId": candidate["id"],
                    "factIndex": index,
                    "fact": fact["fact"],
                    "whyItMatters": fact["whyItMatters"],
                    "evidence": fact["evidence"],
                }
            )
    if not facts:
        raise TechnicalVerificationError("no eligible technical-verification facts")
    return facts


def resolve_technical_verification_input(
    report: Path, project_root: Path, task_root: Path, task_key: str
) -> VerificationInput:
    """같은 작업의 후보 비교 기록을 시험 입력으로 고정한다."""
    report = report if report.is_absolute() else project_root / report
    validate_task_artifact_path(report, task_root, "technical verification source")
    expected_parent = task_root / "runs/implementation-option-selection/reports"
    if report.parent != expected_parent or not re.fullmatch(
        r"final-report-implementation-option-selection-\d{3,}\.data\.json", report.name
    ):
        raise TechnicalVerificationError(
            "source must be this task's option-selection record"
        )
    snapshot = load_owned_object_snapshot(
        report, artifact="technical verification source"
    )
    data = snapshot.value
    errors = validate(data, load_schema_for_data(data))
    if errors:
        raise TechnicalVerificationError("invalid source report: " + "; ".join(errors))
    if data["header"]["taskKey"] != task_key:
        raise TechnicalVerificationError(
            "source taskKey does not match verification task"
        )
    return {
        "sourceReport": report.relative_to(project_root).as_posix(),
        "sourceDataSha256": hashlib.sha256(snapshot.raw_bytes).hexdigest(),
        "scope": "technical-evidence-only",
        "facts": technical_verification_facts(data),
    }


def write_technical_verification_input(
    payload: VerificationInput, run_root: Path, seq: str
) -> Path:
    """실행 순번별 입력을 저장해 후속 실행이 이전 시험 범위를 덮지 않게 한다."""
    path = run_root / "state" / f"technical-verification-input-{seq}.json"
    serialized = serialize_owned_object(path, payload, artifact="technical verification input")
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("x", encoding="utf-8") as output:
        output.write(serialized)
    return path


def _check_execution_evidence(
    check: Mapping[str, Any], project_root: Path, run_root: Path, seq: str
) -> list[str]:
    """관측 판정은 실행 기록과 실행별 시험 디렉터리를 요구한다."""
    errors = []
    commands = check.get("commands") or []
    if check.get("status") != "not-run" and not commands:
        errors.append(f"{check['id']}: an observed result requires command evidence")
    for command in commands:
        try:
            log = project_root / command["logPath"]
            experiment_root = run_root / "experiments" / seq
            validate_task_artifact_path(log, experiment_root, "verification log")
            if not log.read_text(encoding="utf-8").strip():
                errors.append(f"{check['id']}: verification log is empty")
            cwd = project_root / command["cwd"]
            cwd.resolve(strict=True).relative_to(experiment_root.resolve(strict=True))
            relative = cwd.relative_to(experiment_root)
            current = run_root
            for part in ("experiments", seq, *relative.parts):
                current = current / part
                if current.is_symlink():
                    raise TechnicalVerificationError(
                        "experiment cwd contains a symlink"
                    )
            if not cwd.is_dir() or cwd.is_symlink():
                errors.append(f"{check['id']}: experiment cwd must be a directory")
        except (OSError, ValueError) as exc:
            errors.append(f"{check['id']}: invalid execution evidence: {exc}")
        if check.get("status") == "supported" and command.get("exitCode") != 0:
            errors.append(f"{check['id']}: a supported result has a failed command")
    return errors


def validate_technical_verification_report(
    data: Mapping[str, Any], report_path: Path, project_root: Path
) -> list[str]:
    """발행·최종 검증에서 같은 사실 집합과 실제 시험 산출물을 대조한다."""
    if (data.get("header") or {}).get("taskType") != "technical-verification":
        return []
    match = re.fullmatch(
        r"final-report-technical-verification-(\d{3,})\.data\.json", report_path.name
    )
    if match is None:
        return ["technical verification report must use its canonical record path"]
    run_root = report_path.parent.parent
    path = run_root / "state" / f"technical-verification-input-{match[1]}.json"
    try:
        validate_task_artifact_path(path, run_root, "technical verification input")
        source = load_owned_object_snapshot(
            path, artifact="technical verification input"
        ).value
    except (OSError, ValueError) as exc:
        return [str(exc)]
    block = data.get("technicalVerification") or {}
    errors = []
    for key in ("sourceReport", "sourceDataSha256", "scope"):
        if block.get(key) != source.get(key):
            errors.append(f"technicalVerification.{key} must match the run input")
    expected = {fact["id"]: fact for fact in source["facts"]}
    checks = block.get("checks") or []
    ids = [check.get("id") for check in checks]
    if len(ids) != len(set(ids)) or set(ids) != set(expected):
        errors.append(
            "technicalVerification.checks must cover each input fact exactly once"
        )
    for check in checks:
        fact = expected.get(check.get("id"))
        if fact is None:
            continue
        for key in ("candidateId", "factIndex", "fact"):
            if check.get(key) != fact[key]:
                errors.append(f"{check['id']}: {key} must preserve the input fact")
        errors.extend(
            _check_execution_evidence(check, project_root, run_root, match[1])
        )
    if (block.get("routing") or {}).get(
        "nextTaskType"
    ) != "implementation-option-selection":
        errors.append(
            "technical verification returns only to implementation-option-selection"
        )
    return errors
