"""Durable NEO anchor recovery (§2.6).

An attempt's stable reference is persisted before external submission. Every
unfinalized recovery starts by querying NEO; only an independently verified A2
observation can create immutable LedgerEvidence.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Callable, Optional, Protocol

from cl.anchor.backend import AnchorAttestation, AnchorError
from cl.anchor.neo import NEOAnchorBackend, NeoSubmissionReference
from cl.canonical.hash_profile import DEFAULT_PROFILE, content_hash as hash_by_profile


DOMAIN_LOGICAL_ANCHOR_KEY = "COTRUGLI:ANCHOR:LOGICAL_KEY:V1"
DOMAIN_LEDGER_EVIDENCE = "COTRUGLI:ANCHOR:LEDGER_EVIDENCE:V1"


@dataclass(frozen=True)
class LogicalAnchorIdentity:
    backend_id: str
    network_id: str
    submission_profile: str
    package_root: bytes

    def __post_init__(self):
        if not self.backend_id or not self.network_id or not self.submission_profile:
            raise ValueError("logical anchor identity fields must not be empty")
        if len(self.package_root) != 32:
            raise ValueError("package_root must be 32 bytes")

    def canonical(self) -> dict:
        return {
            "hash_profile_id": DEFAULT_PROFILE.id().hex(),
            "backend_id": self.backend_id,
            "network_id": self.network_id,
            "submission_profile": self.submission_profile,
            "package_root": self.package_root.hex(),
        }

    def logical_key(self) -> bytes:
        return hash_by_profile(DEFAULT_PROFILE.id(), DOMAIN_LOGICAL_ANCHOR_KEY, self.canonical())


@dataclass(frozen=True)
class AnchorAttempt:
    logical_anchor_key: bytes
    identity: LogicalAnchorIdentity
    submission_reference: NeoSubmissionReference
    state: str
    last_observed_level: Optional[str] = None


@dataclass(frozen=True)
class LedgerEvidence:
    evidence_id: int
    logical_anchor_key: bytes
    evidence_hash: bytes
    payload_bytes: bytes
    attestation: AnchorAttestation


class AnchorRepository(Protocol):
    def prepare_attempt(
        self, identity: LogicalAnchorIdentity, reference: NeoSubmissionReference
    ) -> AnchorAttempt: ...

    def get_attempt(self, logical_anchor_key: bytes) -> AnchorAttempt: ...

    def record_progress(self, logical_anchor_key: bytes, level: str) -> AnchorAttempt: ...

    def commit_a2(self, attempt: AnchorAttempt, attestation: AnchorAttestation) -> LedgerEvidence: ...

    def get_evidence(self, logical_anchor_key: bytes) -> Optional[LedgerEvidence]: ...


@dataclass(frozen=True)
class RecoveryResult:
    attempt: AnchorAttempt
    evidence: Optional[LedgerEvidence] = None

    @property
    def finalized(self) -> bool:
        return self.evidence is not None


class DurableAnchorCoordinator:
    def __init__(self, repository: AnchorRepository, backend: NEOAnchorBackend) -> None:
        self._repository = repository
        self._backend = backend

    def prepare(self, identity: LogicalAnchorIdentity) -> AnchorAttempt:
        reference = self._backend.prepare_reference(identity.package_root)
        return self._repository.prepare_attempt(identity, reference)

    def recover(self, logical_anchor_key: bytes) -> RecoveryResult:
        attempt = self._repository.get_attempt(logical_anchor_key)
        existing = self._repository.get_evidence(logical_anchor_key)
        if existing is not None:
            return RecoveryResult(attempt, existing)

        # G13: local absence of A2 says nothing about NEO. Always re-query by the durable reference first.
        observation = self._backend.query(attempt.submission_reference)
        if observation.is_a2:
            evidence = self._repository.commit_a2(attempt, observation.attestation)
            return RecoveryResult(self._repository.get_attempt(logical_anchor_key), evidence)
        if observation.level == "NotFound":
            submitted = self._backend.submit(attempt.submission_reference)
            updated = self._repository.record_progress(logical_anchor_key, submitted.level)
            return RecoveryResult(updated)
        if observation.level in ("A0Received", "A1Sequenced"):
            return RecoveryResult(
                self._repository.record_progress(logical_anchor_key, observation.level)
            )
        raise AnchorError("NEO returned an unsupported recovery state")

    def run_until_a2(
        self,
        identity: LogicalAnchorIdentity,
        max_queries: int = 120,
        sleeper: Optional[Callable[[], None]] = None,
    ) -> LedgerEvidence:
        if max_queries < 1:
            raise ValueError("max_queries must be positive")
        attempt = self.prepare(identity)
        sleep = sleeper or (lambda: None)
        for index in range(max_queries):
            result = self.recover(attempt.logical_anchor_key)
            if result.finalized:
                return result.evidence
            if index + 1 < max_queries:
                sleep()
        raise AnchorError("durable anchor attempt did not reach independently verified A2")
