"""NEOAnchorBackend — the real anchoring client against NEO's EXISTING HTTP API (§2.5, guardrail #4).

Lives entirely in cotrugli-ledger (Python); calls NEO's existing `/v1/submit` + `/v1/status` + `/v1/block`
— ZERO Rust changes. Anchoring a `batch_root`:
  1. build a gateway-signed admission with `event_commitment_hash = batch_root` (cl.anchor.neo_admission,
     fixint bincode, golden-verified byte-for-byte),
  2. POST `/v1/submit` (the opaque root is the only thing crossing — guardrail #2),
  3. poll `/v1/status/<height>/<event>` to **A2EvidenceFinal** (guardrail #1 — A0/A1 are NOT anchored),
  4. fetch `/v1/block/<height>` independently and INDEPENDENTLY re-derive `fold([batch_root])`, asserting it
     equals the finalized header's `evidence_commitments_root` (re-derivation, not echo — guardrail #3).

NEO folds the submitted commitment into `evidence_commitments_root = submit_manifest_evidence_root([batch_root])
= SHA3-256(bincode(sorted [batch_root]))`, so the anchored root is the FOLD and `batch_root`'s inclusion is
proven by re-deriving that fold here.
"""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass
from typing import Callable, Optional, Tuple

from cl.anchor.backend import AnchorAttestation, AnchorBackend, AnchorError
from cl.anchor.neo_admission import NeoAdmissionRecord, _u64_le, sign_admission

# An HTTP transport: (method, path, body|None, headers) -> (status, body_text). Injectable so the client is
# unit-testable against a mock and runs against a real node over TCP in the live e2e.
HttpTransport = Callable[[str, str, Optional[bytes], dict], Tuple[int, str]]


def submit_manifest_evidence_root(commitments: list) -> bytes:
    """Re-derivation of NEO's `submit_manifest_evidence_root`: SHA3-256(bincode(sorted commitments)).
    bincode(Vec<[u8;32]>) fixint = u64 LE length ‖ each raw 32 bytes. Validated against the live header in the
    e2e (if it diverges, the anchored-root check fails)."""
    if not commitments or any(len(commitment) != 32 for commitment in commitments):
        raise ValueError("NEO evidence commitments must be non-empty 32-byte values")
    ordered = sorted(commitments)
    preimage = _u64_le(len(ordered)) + b"".join(ordered)
    return hashlib.sha3_256(preimage).digest()


def _arr(b: bytes) -> list:
    return list(b)


@dataclass(frozen=True)
class NeoAnchorConfig:
    chain_id: bytes
    policy_snapshot_id: bytes
    gateway_seed: bytes
    namespace: bytes
    account_id: bytes
    transport_key: str  # x-gateway-key header (transport protection only)
    anchor_height: int  # the node-0-led target height the batch anchors at
    poll_timeout_s: float = 60.0


@dataclass(frozen=True)
class NeoSubmissionReference:
    """Stable NEO lookup identity, persisted before submission for crash recovery."""

    event_commitment_hash: bytes
    target_height: int

    def __post_init__(self):
        if len(self.event_commitment_hash) != 32 or self.target_height < 1:
            raise ValueError("invalid NEO submission reference")

    def as_dict(self) -> dict:
        return {
            "event_commitment_hash": self.event_commitment_hash.hex(),
            "target_height": self.target_height,
        }

    @classmethod
    def from_dict(cls, value: dict) -> "NeoSubmissionReference":
        try:
            return cls(bytes.fromhex(value["event_commitment_hash"]), int(value["target_height"]))
        except (KeyError, TypeError, ValueError) as error:
            raise AnchorError("invalid persisted NEO submission reference") from error


@dataclass(frozen=True)
class NeoAnchorObservation:
    level: str
    reference: NeoSubmissionReference
    attestation: Optional[AnchorAttestation] = None

    @property
    def is_a2(self) -> bool:
        return self.level == "A2EvidenceFinal" and self.attestation is not None


class NEOAnchorBackend(AnchorBackend):
    def __init__(self, config: NeoAnchorConfig, transport: HttpTransport):
        self._cfg = config
        self._http = transport

    def _admission(self, reference: NeoSubmissionReference) -> NeoAdmissionRecord:
        from cl.anchor.neo_admission import gateway_public_key

        return NeoAdmissionRecord(
            chain_id=self._cfg.chain_id,
            gateway_id=gateway_public_key(self._cfg.gateway_seed),
            gateway_epoch=1,
            gateway_sequence=0,
            target_height=reference.target_height,
            earliest_valid_height=1,
            expiry_height=reference.target_height + 1000,
            admission_index=0,
            priority_class=0,
            event_commitment_hash=reference.event_commitment_hash,
            idempotency_key=reference.event_commitment_hash,
            namespace=self._cfg.namespace,
            account_id=self._cfg.account_id,
            policy_snapshot_id=self._cfg.policy_snapshot_id,
        )

    def _admission_json(self, rec: NeoAdmissionRecord, signature: bytes) -> bytes:
        body = {
            "chain_id": _arr(rec.chain_id),
            "gateway_id": _arr(rec.gateway_id),
            "gateway_epoch": rec.gateway_epoch,
            "gateway_sequence": rec.gateway_sequence,
            "target_height": rec.target_height,
            "earliest_valid_height": rec.earliest_valid_height,
            "expiry_height": rec.expiry_height,
            "admission_index": rec.admission_index,
            "priority_class": rec.priority_class,
            "event_commitment_hash": _arr(rec.event_commitment_hash),
            "idempotency_key": _arr(rec.idempotency_key),
            "namespace": _arr(rec.namespace),
            "account_id": _arr(rec.account_id),
            "policy_snapshot_id": _arr(rec.policy_snapshot_id),
            "gateway_signature": _arr(signature),
        }
        return json.dumps(body).encode("utf-8")

    def prepare_reference(self, batch_root: bytes) -> NeoSubmissionReference:
        if len(batch_root) != 32:
            raise AnchorError("batch_root must be 32 bytes")
        return NeoSubmissionReference(bytes(batch_root), self._cfg.anchor_height)

    def submit(self, reference: NeoSubmissionReference) -> NeoAnchorObservation:
        self._validate_reference(reference)
        rec = self._admission(reference)
        signature = sign_admission(self._cfg.gateway_seed, rec)
        status, sbody = self._http(
            "POST", "/v1/submit", self._admission_json(rec, signature),
            {"x-gateway-key": self._cfg.transport_key},
        )
        try:
            response = json.loads(sbody)
            accepted_root = bytes.fromhex(response["event_commitment_hash"])
        except (KeyError, TypeError, ValueError, json.JSONDecodeError) as error:
            raise AnchorError("malformed NEO submit response") from error
        if (
            status != 200
            or response.get("accepted") is not True
            or response.get("level") != "A0Received"
            or accepted_root != reference.event_commitment_hash
        ):
            raise AnchorError(f"/v1/submit rejected: {status} {sbody}")
        return NeoAnchorObservation("A0Received", reference)

    def query(self, reference: NeoSubmissionReference) -> NeoAnchorObservation:
        """Query NEO by durable reference. A2 is returned only after independent header/fold verification."""
        self._validate_reference(reference)
        event_hex = reference.event_commitment_hash.hex()
        height = reference.target_height
        st, body = self._http("GET", f"/v1/status/{height}/{event_hex}", None, {})
        if st != 200:
            raise AnchorError(f"/v1/status/{height}/{event_hex}: {st} {body}")
        try:
            doc = json.loads(body)
            level = doc["level"]
        except (KeyError, TypeError, json.JSONDecodeError) as error:
            raise AnchorError("malformed NEO status response") from error
        try:
            status_event = bytes(doc["event_commitment_hash"])
            status_height = int(doc["target_height"])
        except (KeyError, TypeError, ValueError) as error:
            raise AnchorError("NEO status does not carry its event and target height") from error
        if status_event != reference.event_commitment_hash or status_height != height:
            raise AnchorError("NEO status does not bind the durable submission reference")
        if level in ("NotFound", "A0Received", "A1Sequenced"):
            return NeoAnchorObservation(level, reference)
        if level != "A2EvidenceFinal":
            raise AnchorError(f"unsupported NEO status level: {level}")
        try:
            status_block_hash = bytes(doc["block_hash"])
            cert_hash = bytes(doc["finality_certificate_hash"])
        except (KeyError, TypeError, ValueError) as error:
            raise AnchorError("malformed NEO A2 response") from error
        if len(status_block_hash) != 32 or len(cert_hash) != 32:
            raise AnchorError("NEO A2 response contains a malformed hash")

        bst, bbody = self._http("GET", f"/v1/block/{height}", None, {})
        if bst != 200:
            raise AnchorError(f"/v1/block/{height}: {bst} {bbody}")
        try:
            block_doc = json.loads(bbody)
            header = block_doc["header"]
            observed_root = bytes(header["evidence_commitments_root"])
            observed_block_hash = bytes.fromhex(block_doc["block_hash"])
            observed_height = int(block_doc["height"])
        except (KeyError, TypeError, ValueError, json.JSONDecodeError) as error:
            raise AnchorError("malformed NEO block response") from error
        if observed_height != height or observed_block_hash != status_block_hash:
            raise AnchorError("NEO status and independently fetched block disagree")
        expected_root = expected_anchored_root(reference.event_commitment_hash)
        if observed_root != expected_root:
            raise AnchorError("NEO header evidence root != independently re-derived fold([batch_root])")
        return NeoAnchorObservation(
            level,
            reference,
            AnchorAttestation(
                opaque_root=observed_root,
                anchored_at_height=height,
                finality_certificate_hash=cert_hash,
                assurance_level="signature_authentic",
                block_hash=status_block_hash,
            ),
        )

    def _validate_reference(self, reference: NeoSubmissionReference) -> None:
        if reference.target_height != self._cfg.anchor_height:
            raise AnchorError("submission reference targets a different configured height")

    def anchor(self, batch_root: bytes) -> AnchorAttestation:
        import time

        reference = self.prepare_reference(batch_root)
        self.submit(reference)

        deadline = time.monotonic() + self._cfg.poll_timeout_s
        while True:
            observation = self.query(reference)
            if observation.is_a2:
                return observation.attestation
            if time.monotonic() >= deadline:
                raise AnchorError(f"batch_root never reached A2EvidenceFinal within {self._cfg.poll_timeout_s}s")
            time.sleep(0.2)


def expected_anchored_root(batch_root: bytes) -> bytes:
    """What `evidence_commitments_root` MUST be if NEO anchored exactly our batch_root: fold([batch_root])."""
    return submit_manifest_evidence_root([batch_root])


def tcp_http_transport(host: str, port: int, timeout_s: float = 5.0) -> HttpTransport:
    """A raw-TCP HTTP/1.1 transport (no client dependency) for the live NEO node."""
    import socket

    def _transport(method: str, path: str, body, headers: dict):
        with socket.create_connection((host, port), timeout=timeout_s) as s:
            s.settimeout(timeout_s)
            head = f"{method} {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n"
            for k, v in headers.items():
                head += f"{k}: {v}\r\n"
            if body is not None:
                head += f"Content-Type: application/json\r\nContent-Length: {len(body)}\r\n"
            head += "\r\n"
            s.sendall(head.encode("utf-8") + (body or b""))
            buf = b""
            while True:
                chunk = s.recv(65536)
                if not chunk:
                    break
                buf += chunk
        text = buf.decode("utf-8", "replace")
        split = text.find("\r\n\r\n")
        if split < 0:
            return 0, ""
        status_line = text[:split].splitlines()[0]
        try:
            status = int(status_line.split()[1])
        except (IndexError, ValueError):
            status = 0
        return status, text[split + 4:]

    return _transport
