"""Mutation recovery decisions after sealed or unsealed implementer attempts."""
from __future__ import annotations

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

from .attempt_evidence import AttemptEvidenceSeal, _sha
from .dispatch_state import build_dispatch_id
from .execution_identity import ExecutionManifestError
from .execution_manifest import (
    _replace_manifest,
    _write_manifest_over_existing,
    read_execution_manifest,
)
from .run_context import task_mutex


class MutationRecoveryError(ValueError):
    """Raised when a recovery decision cannot be recorded or applied."""


@dataclass(frozen=True)
class MutationRecoveryInput:
    manifest_path: Path
    source_invocation_ref: str
    source_attempt_ref: str
    source_evidence_seal_ref: str | None
    original_baseline_digest: str
    terminal_source_diff_digest: str
    terminal_git_chain_digest: str
    verifier_role_execution_ref: str | None
    verifier_invocation_ref: str | None
    verifier_participant_ref: str | None
    implementer_participant_ref: str
    recovered_evidence_refs: tuple[Mapping[str, Any], ...]
    recovered_out_of_plan_edits: tuple[Mapping[str, Any], ...]
    current_source_diff_digest: str
    current_git_chain_digest: str


@dataclass(frozen=True)
class MutationRecoveryDecision:
    recovery_ref: str
    source_invocation_ref: str
    source_attempt_ref: str
    source_evidence_seal_ref: str | None
    original_baseline_digest: str
    terminal_source_diff_digest: str
    terminal_git_chain_digest: str
    verifier_role_execution_ref: str | None
    verifier_invocation_ref: str | None
    recovered_evidence_refs: tuple[Mapping[str, Any], ...]
    recovered_out_of_plan_edits: tuple[Mapping[str, Any], ...]
    unmet_obligations: tuple[str, ...]
    decision: Literal["carry-forward", "reject", "cleanup-confirmed"]
    reason: str = ""

    def to_payload(self) -> dict[str, Any]:
        return {
            "recoveryRef": self.recovery_ref,
            "sourceInvocationRef": self.source_invocation_ref,
            "sourceAttemptRef": self.source_attempt_ref,
            "sourceEvidenceSealRef": self.source_evidence_seal_ref,
            "originalBaselineDigest": self.original_baseline_digest,
            "terminalSourceDiffDigest": self.terminal_source_diff_digest,
            "terminalGitChainDigest": self.terminal_git_chain_digest,
            "verifierRoleExecutionRef": self.verifier_role_execution_ref,
            "verifierInvocationRef": self.verifier_invocation_ref,
            "recoveredEvidenceRefs": [dict(row) for row in self.recovered_evidence_refs],
            "recoveredOutOfPlanEdits": [
                dict(row) for row in self.recovered_out_of_plan_edits
            ],
            "unmetObligations": list(self.unmet_obligations),
            "decision": self.decision,
            "reason": self.reason,
        }

    @classmethod
    def from_payload(cls, payload: Mapping[str, Any]) -> "MutationRecoveryDecision":
        return cls(
            recovery_ref=str(payload["recoveryRef"]),
            source_invocation_ref=str(payload["sourceInvocationRef"]),
            source_attempt_ref=str(payload["sourceAttemptRef"]),
            source_evidence_seal_ref=payload.get("sourceEvidenceSealRef"),
            original_baseline_digest=str(payload.get("originalBaselineDigest") or ""),
            terminal_source_diff_digest=str(
                payload.get("terminalSourceDiffDigest") or ""
            ),
            terminal_git_chain_digest=str(payload.get("terminalGitChainDigest") or ""),
            verifier_role_execution_ref=payload.get("verifierRoleExecutionRef"),
            verifier_invocation_ref=payload.get("verifierInvocationRef"),
            recovered_evidence_refs=tuple(payload.get("recoveredEvidenceRefs") or ()),
            recovered_out_of_plan_edits=tuple(
                payload.get("recoveredOutOfPlanEdits") or ()
            ),
            unmet_obligations=tuple(payload.get("unmetObligations") or ()),
            decision=str(payload["decision"]),
            reason=str(payload.get("reason") or ""),
        )


def _seal_for(manifest, seal_ref: str | None) -> AttemptEvidenceSeal | None:
    if not seal_ref:
        return None
    row = next(
        (
            item
            for item in manifest.attempt_evidence_seals
            if item.get("sealRef") == seal_ref
        ),
        None,
    )
    return AttemptEvidenceSeal.from_payload(row) if row is not None else None


def _verify_sealed_bytes(seal: AttemptEvidenceSeal) -> None:
    stream_path = seal.host_event_stream.get("path")
    if isinstance(stream_path, str) and stream_path:
        data = Path(stream_path).read_bytes()
        if _sha(data) != seal.host_event_stream.get("sha256"):
            raise MutationRecoveryError("sealed bytes changed")
        if len(data) != seal.host_event_stream.get("byteLength"):
            raise MutationRecoveryError("sealed bytes changed")
    for artifact in seal.artifacts:
        path = Path(str(artifact["normalizedPath"]))
        data = path.read_bytes() if path.is_file() else b""
        if _sha(data) != artifact.get("sha256"):
            raise MutationRecoveryError("sealed bytes changed")


def evaluate_mutation_recovery(
    recovery: MutationRecoveryInput,
) -> MutationRecoveryDecision:
    """Decide carry-forward or reject from sealed evidence."""
    manifest = read_execution_manifest(recovery.manifest_path)
    attempt = next(
        (
            row
            for row in manifest.attempts
            if build_dispatch_id(row.invocation_ref, row.attempt)
            == recovery.source_attempt_ref
        ),
        None,
    )
    if attempt is None:
        raise MutationRecoveryError("source attempt is unknown")
    if attempt.status == "evidence-unsealed":
        return MutationRecoveryDecision(
            recovery_ref=f"recovery-reject-{attempt.attempt}",
            source_invocation_ref=recovery.source_invocation_ref,
            source_attempt_ref=recovery.source_attempt_ref,
            source_evidence_seal_ref=None,
            original_baseline_digest=recovery.original_baseline_digest,
            terminal_source_diff_digest=recovery.terminal_source_diff_digest,
            terminal_git_chain_digest=recovery.terminal_git_chain_digest,
            verifier_role_execution_ref=None,
            verifier_invocation_ref=None,
            recovered_evidence_refs=(),
            recovered_out_of_plan_edits=(),
            unmet_obligations=("evidence-unsealed",),
            decision="reject",
            reason="evidence-unsealed",
        )
    unmet: list[str] = []
    if not recovery.source_evidence_seal_ref:
        unmet.append("missing-source-seal")
    if not recovery.recovered_evidence_refs:
        unmet.append("missing-host-tdd-events")
    if (
        recovery.verifier_participant_ref
        and recovery.verifier_participant_ref == recovery.implementer_participant_ref
    ):
        unmet.append("verifier-not-independent")
    if not recovery.verifier_invocation_ref:
        unmet.append("missing-verifier-invocation")
    recovered_paths = {
        str(row.get("path"))
        for row in recovery.recovered_out_of_plan_edits
        if isinstance(row, Mapping) and row.get("path")
    }
    sealed = _seal_for(manifest, recovery.source_evidence_seal_ref)
    if sealed is not None:
        _verify_sealed_bytes(sealed)
        if recovered_paths - set(sealed.changed_paths):
            raise MutationRecoveryError("out-of-plan path set differs")
        if recovered_paths:
            raise MutationRecoveryError("out-of-plan path set differs")
    if unmet:
        return MutationRecoveryDecision(
            recovery_ref=f"recovery-reject-{attempt.attempt}",
            source_invocation_ref=recovery.source_invocation_ref,
            source_attempt_ref=recovery.source_attempt_ref,
            source_evidence_seal_ref=recovery.source_evidence_seal_ref,
            original_baseline_digest=recovery.original_baseline_digest,
            terminal_source_diff_digest=recovery.terminal_source_diff_digest,
            terminal_git_chain_digest=recovery.terminal_git_chain_digest,
            verifier_role_execution_ref=recovery.verifier_role_execution_ref,
            verifier_invocation_ref=recovery.verifier_invocation_ref,
            recovered_evidence_refs=recovery.recovered_evidence_refs,
            recovered_out_of_plan_edits=recovery.recovered_out_of_plan_edits,
            unmet_obligations=tuple(unmet),
            decision="reject",
            reason="unmet-obligations",
        )
    return MutationRecoveryDecision(
        recovery_ref=f"recovery-carry-{attempt.attempt}",
        source_invocation_ref=recovery.source_invocation_ref,
        source_attempt_ref=recovery.source_attempt_ref,
        source_evidence_seal_ref=recovery.source_evidence_seal_ref,
        original_baseline_digest=recovery.original_baseline_digest,
        terminal_source_diff_digest=recovery.terminal_source_diff_digest,
        terminal_git_chain_digest=recovery.terminal_git_chain_digest,
        verifier_role_execution_ref=recovery.verifier_role_execution_ref,
        verifier_invocation_ref=recovery.verifier_invocation_ref,
        recovered_evidence_refs=recovery.recovered_evidence_refs,
        recovered_out_of_plan_edits=recovery.recovered_out_of_plan_edits,
        unmet_obligations=(),
        decision="carry-forward",
        reason="",
    )


def confirm_mutation_cleanup(
    manifest_path: Path,
    recovery_ref: str,
    *,
    original_baseline_digest: str,
    current_baseline_digest: str,
    task_key: str,
) -> MutationRecoveryDecision:
    """Allow a new invocation after user cleanup matches the original baseline."""
    if current_baseline_digest != original_baseline_digest:
        raise MutationRecoveryError("cleanup does not restore the original baseline")
    with task_mutex(task_key):
        manifest = read_execution_manifest(manifest_path)
        source = next(
            (
                MutationRecoveryDecision.from_payload(row)
                for row in manifest.mutation_recovery_decisions
                if row.get("recoveryRef") == recovery_ref
            ),
            None,
        )
        if source is None or source.decision != "reject":
            raise MutationRecoveryError("cleanup requires a prior reject decision")
        confirmed = MutationRecoveryDecision(
            recovery_ref=f"{recovery_ref}-cleanup",
            source_invocation_ref=source.source_invocation_ref,
            source_attempt_ref=source.source_attempt_ref,
            source_evidence_seal_ref=None,
            original_baseline_digest=original_baseline_digest,
            terminal_source_diff_digest="",
            terminal_git_chain_digest="",
            verifier_role_execution_ref=None,
            verifier_invocation_ref=None,
            recovered_evidence_refs=(),
            recovered_out_of_plan_edits=(),
            unmet_obligations=(),
            decision="cleanup-confirmed",
            reason="cleanup-confirmed",
        )
        updated = _replace_manifest(
            manifest,
            mutation_recovery_decisions=(
                *manifest.mutation_recovery_decisions,
                confirmed.to_payload(),
            ),
        )
        _write_manifest_over_existing(manifest_path, updated)
        return confirmed


def start_new_invocation(recovery: MutationRecoveryInput) -> None:
    """Refuse a new invocation while an unresolved mutation has no decision."""
    manifest = read_execution_manifest(recovery.manifest_path)
    unresolved = [
        row
        for row in manifest.attempts
        if row.status in {"mutation-present-unresolved", "evidence-unsealed"}
    ]
    confirmed = {
        row.get("sourceAttemptRef")
        for row in manifest.mutation_recovery_decisions
        if row.get("decision") in {"carry-forward", "cleanup-confirmed"}
    }
    pending = [
        row
        for row in unresolved
        if build_dispatch_id(row.invocation_ref, row.attempt) not in confirmed
    ]
    if pending:
        raise MutationRecoveryError("recovery decision required")
