"""Host-wiring anchor backends for the Flip A dual-anchor transition (LIVE_NEO_ON_HOST_FLIP_PLAN §B).

This is DEPLOYMENT WIRING, not an anchor semantic: it lives in `cl.http` (host layer) precisely because
`cl/anchor/*` is kernel-protected. Nothing here merges, reinterprets, or invents evidence — each leg
produces its OWN attestation under its OWN `assurance_level`, and the wrapper only records both, side by
side, in a reference-only JSONL round log.

Two backends:

  `NeoHostBackend`   — service-shaped adapter over `NEOAnchorBackend`: picks a fresh, strictly-increasing,
                       node-0-led target height per anchor round (the demo_api pattern — one root per block)
                       and delegates. Fail-closed: an `AnchorError` propagates, never a fabricated success.
  `DualAnchorBackend`— the BOUNDED transition wrapper: checkpoint leg first (the existing evidence of record;
                       its semantics are unchanged), then the NEO leg. A checkpoint success NEVER masks or
                       produces a NEO success: the NEO leg's outcome is recorded exactly (ok / FAILED with
                       the reason), a failed NEO leg simply does not count toward the exit gate, and the
                       returned attestation is always the CHECKPOINT one while in dual mode (plan §B.4).

Round-log discipline: fixed whitelist of keys, roots/heights/assurance only, error text sanitized+truncated
— NO secret ever reaches the log (gateway seed and transport key are additionally `repr=False` so they
cannot leak through tracebacks or dataclass reprs).
"""

from __future__ import annotations

import json
import os
import threading
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable, Optional, Union

from cl.anchor.backend import AnchorAttestation, AnchorBackend, AnchorError
from cl.anchor.neo import (
    NeoAnchorConfig,
    NEOAnchorBackend,
    expected_anchored_root,
    tcp_http_transport,
)

HOST_ANCHOR_MODES = ("checkpoint", "dual", "neo", "neova", "neova2")

# Devnet constants (overridable via env). The GATEWAY SEED and TRANSPORT KEY have NO defaults on purpose:
# dual/neo modes FAIL FAST at startup without them (credentials are an owner act, never a baked-in value).
_DEFAULT_CHAIN_ID_HEX = "e2" * 32
_DEFAULT_POLICY_SNAPSHOT_ID_HEX = "168914ec4246371f1163289812e8718cf2330864d80134b6b4c030741f8cc3db"
_DEFAULT_NAMESPACE_HEX = "55" * 32
_DEFAULT_ACCOUNT_ID_HEX = "66" * 32

_ERROR_TEXT_MAX = 300  # sanitized failure reason cap for the round log


@dataclass(frozen=True)
class NeoLegSettings:
    """The NEO leg's host configuration. Secrets carry `repr=False` so they never appear in reprs/tracebacks."""

    host: str
    port: int
    chain_id: bytes
    policy_snapshot_id: bytes
    gateway_seed: bytes = field(repr=False, default=b"")
    transport_key: str = field(repr=False, default="")
    namespace: bytes = bytes.fromhex(_DEFAULT_NAMESPACE_HEX)
    account_id: bytes = bytes.fromhex(_DEFAULT_ACCOUNT_ID_HEX)
    height_margin: int = 8
    poll_timeout_s: float = 90.0
    node_count: int = 4

    @classmethod
    def from_env(cls, env: Optional[dict] = None) -> "NeoLegSettings":
        """Build from `CL_NEO_*` env. FAIL-FAST on missing credentials: a host asked to run dual/neo without
        owner-set credentials must refuse to start, never silently fall back (plan §B.4: no silent fallback)."""
        e = os.environ if env is None else env
        seed_hex = e.get("CL_NEO_GATEWAY_SEED_HEX", "").strip()
        transport_key = e.get("CL_NEO_TRANSPORT_KEY", "").strip()
        if not seed_hex or not transport_key:
            raise RuntimeError(
                "CL_ANCHOR_BACKEND=dual|neo requires CL_NEO_GATEWAY_SEED_HEX and CL_NEO_TRANSPORT_KEY "
                "(owner-set at the host secret/env boundary) — refusing to start without credentials"
            )
        return cls(
            host=e.get("CL_NEO_HOST", "127.0.0.1"),
            port=int(e.get("CL_NEO_PORT", "8080")),
            chain_id=bytes.fromhex(e.get("CL_NEO_CHAIN_ID_HEX", _DEFAULT_CHAIN_ID_HEX)),
            policy_snapshot_id=bytes.fromhex(
                e.get("CL_NEO_POLICY_SNAPSHOT_ID_HEX", _DEFAULT_POLICY_SNAPSHOT_ID_HEX)
            ),
            gateway_seed=bytes.fromhex(seed_hex),
            transport_key=transport_key,
            namespace=bytes.fromhex(e.get("CL_NEO_NAMESPACE_HEX", _DEFAULT_NAMESPACE_HEX)),
            account_id=bytes.fromhex(e.get("CL_NEO_ACCOUNT_ID_HEX", _DEFAULT_ACCOUNT_ID_HEX)),
            height_margin=int(e.get("CL_NEO_HEIGHT_MARGIN", "8")),
            poll_timeout_s=float(e.get("CL_NEO_POLL_TIMEOUT_S", "90")),
            node_count=int(e.get("CL_NEO_NODE_COUNT", "4")),
        )


def _default_backend_factory(settings: NeoLegSettings, anchor_height: int) -> AnchorBackend:
    cfg = NeoAnchorConfig(
        chain_id=settings.chain_id,
        policy_snapshot_id=settings.policy_snapshot_id,
        gateway_seed=settings.gateway_seed,
        namespace=settings.namespace,
        account_id=settings.account_id,
        transport_key=settings.transport_key,
        anchor_height=anchor_height,
        poll_timeout_s=settings.poll_timeout_s,
    )
    return NEOAnchorBackend(cfg, tcp_http_transport(settings.host, settings.port))


class NeoHostBackend(AnchorBackend):
    """Service-shaped NEO backend: one fresh node-0-led target height per anchor round, strictly increasing
    under a process-local lock (single-instance assumption — same as demo_api; the kernel's multi-root fold
    verification is the backstop). `AnchorError` propagates — fail-closed, never a fabricated success."""

    def __init__(
        self,
        settings: NeoLegSettings,
        backend_factory: Callable[[NeoLegSettings, int], AnchorBackend] = _default_backend_factory,
        tip_reader: Optional[Callable[[], Optional[int]]] = None,
    ) -> None:
        self._settings = settings
        self._factory = backend_factory
        self._tip_reader = tip_reader or self._live_tip
        self._lock = threading.Lock()
        self._last_assigned_height = 0

    def _live_tip(self) -> Optional[int]:
        transport = tcp_http_transport(self._settings.host, self._settings.port)
        try:
            status, body = transport("GET", "/v1/chain", None, {})
        except OSError:
            return None
        if status != 200:
            return None
        try:
            return int(json.loads(body)["finalized_height"])
        except (KeyError, TypeError, ValueError, json.JSONDecodeError):
            return None

    def next_anchor_height(self) -> int:
        tip = self._tip_reader()
        if tip is None:
            raise AnchorError("live NEO tip unavailable — cannot target an anchor height (fail-closed)")
        base = tip + self._settings.height_margin
        with self._lock:
            base = max(base, self._last_assigned_height + 1)
            height = base + ((-base) % self._settings.node_count)  # next node-0-led height
            self._last_assigned_height = height
        return height

    def anchor(self, batch_root: bytes) -> AnchorAttestation:
        height = self.next_anchor_height()
        return self._factory(self._settings, height).anchor(batch_root)

    def host_anchor_status(self) -> dict:
        return {"mode": "neo", "last_assigned_height": self._last_assigned_height}


def _sanitize_error(exc: BaseException) -> str:
    return str(exc)[:_ERROR_TEXT_MAX]


class DualAnchorBackend(AnchorBackend):
    """Bounded dual-anchor transition wrapper (plan §B.3/§B.4). Checkpoint leg first — its existing
    semantics ARE the service's semantics while in dual mode (the returned attestation is the checkpoint
    one; `entry_for_root` delegates to it so anchor proofs keep carrying the signed checkpoint entry).
    The NEO leg runs second on the SAME batch_root; its outcome is recorded exactly and separately."""

    def __init__(
        self,
        checkpoint_backend: AnchorBackend,
        neo_backend: NeoHostBackend,
        round_log_path: Union[str, Path, None] = None,
    ) -> None:
        self._checkpoint = checkpoint_backend
        self._neo = neo_backend
        self._log_path = Path(round_log_path) if round_log_path else None
        self._lock = threading.Lock()
        self._rounds_total = 0
        self._rounds_paired_ok = 0
        self._neo_failures = 0
        self._divergences = 0
        self._last_round: Optional[dict] = None

    # --- the service's duck-typed checkpoint-entry seam is preserved -----------------------------
    def entry_for_root(self, batch_root: bytes):
        entry_for_root = getattr(self._checkpoint, "entry_for_root", None)
        return entry_for_root(batch_root) if callable(entry_for_root) else None

    def anchor(self, batch_root: bytes) -> AnchorAttestation:
        record: dict = {
            "round": None,  # assigned under the lock below
            "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
            "batch_root": batch_root.hex(),
            "checkpoint_result": None,
            "neo_result": None,
            "fold_ok": None,
            "divergence": False,
            "overall": None,
        }

        # Leg 1 — checkpoint (the evidence of record in dual mode; unchanged semantics, failure propagates).
        try:
            cp_att = self._checkpoint.anchor(batch_root)
        except AnchorError as exc:
            record["checkpoint_result"] = {"ok": False, "error": _sanitize_error(exc)}
            record["overall"] = "checkpoint_failed"
            self._commit(record, paired_ok=False)
            raise
        cp_divergence = cp_att.opaque_root != batch_root  # checkpoint commits the root DIRECTLY (no fold)
        record["checkpoint_result"] = {
            "ok": True,
            "opaque_root": cp_att.opaque_root.hex(),
            "height": cp_att.anchored_at_height,
            "assurance_level": cp_att.assurance_level,
        }

        # Leg 2 — NEO. Recorded exactly; a failure here NEVER becomes a success and never breaks ingest
        # availability in dual mode — the round simply does not count toward the exit gate (plan §B.4).
        neo_divergence = False
        try:
            neo_att = self._neo.anchor(batch_root)
        except (AnchorError, OSError) as exc:
            record["neo_result"] = {"ok": False, "error": _sanitize_error(exc)}
            record["fold_ok"] = False
            record["overall"] = "neo_failed"
        else:
            fold_ok = neo_att.opaque_root == expected_anchored_root(batch_root)
            neo_divergence = not fold_ok  # defense in depth; NEOAnchorBackend.query already enforces this
            record["neo_result"] = {
                "ok": True,
                "opaque_root": neo_att.opaque_root.hex(),
                "height": neo_att.anchored_at_height,
                "assurance_level": neo_att.assurance_level,
            }
            record["fold_ok"] = fold_ok
            record["overall"] = "paired_ok" if fold_ok else "divergence"

        if cp_divergence or neo_divergence:
            record["divergence"] = True
            record["overall"] = "divergence"

        self._commit(record, paired_ok=record["overall"] == "paired_ok")
        return cp_att

    def _commit(self, record: dict, *, paired_ok: bool) -> None:
        with self._lock:
            self._rounds_total += 1
            record["round"] = self._rounds_total
            if paired_ok:
                self._rounds_paired_ok += 1
            if record.get("neo_result") is not None and record["neo_result"].get("ok") is False:
                self._neo_failures += 1
            if record.get("divergence"):
                self._divergences += 1
            self._last_round = record
        if self._log_path is not None:
            try:  # best-effort append; a log write error never breaks an anchor round
                with open(self._log_path, "a", encoding="utf-8") as f:
                    f.write(json.dumps(record, sort_keys=True) + "\n")
            except OSError:
                pass

    def host_anchor_status(self) -> dict:
        """The ratified dual-mode status: checkpoint_result / neo_result / overall, clearly separated."""
        with self._lock:
            return {
                "mode": "dual",
                "rounds_total": self._rounds_total,
                "rounds_paired_ok": self._rounds_paired_ok,
                "neo_failures": self._neo_failures,
                "divergences": self._divergences,
                "last_round": self._last_round,
            }


def neova2_truck_backend_from_env(env: Optional[dict] = None) -> AnchorBackend:
    """Build the **LAB/EXPERIMENTAL** Track-A NEOva2 truck backend from `CL_NEOVA2_*`.

    This mode never produces an A2 anchor and is for laboratory app/Arena testing only —
    it additionally requires `CL_NEOVA2_LAB_MODE=1` as an explicit acknowledgement. FAIL-FAST
    on missing credentials too: the gateway signing seed is an owner act and has NO default,
    so a host asked to anchor on NEOva2 without one refuses to start rather than sign with a
    guessable key."""
    from pathlib import Path as _Path

    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

    from cl.anchor.neova2_truck import NEOva2TruckAnchorBackend, NEOva2TruckConfig

    e = os.environ if env is None else env
    if e.get("CL_NEOVA2_LAB_MODE", "").strip() != "1":
        raise RuntimeError(
            "CL_ANCHOR_BACKEND=neova2 is a LAB/EXPERIMENTAL Track-A bridge that never produces "
            "an A2 anchor — set CL_NEOVA2_LAB_MODE=1 to acknowledge that explicitly. Production "
            "anchoring belongs to Track B (NEOfortress -> CL2 -> NEOcomp/Shredder).")
    seed_hex = e.get("CL_NEOVA2_GATEWAY_SEED_HEX", "").strip()
    gateway_id_hex = e.get("CL_NEOVA2_GATEWAY_ID_HEX", "").strip()
    if not seed_hex or not gateway_id_hex:
        raise RuntimeError(
            "CL_ANCHOR_BACKEND=neova2 requires CL_NEOVA2_GATEWAY_SEED_HEX and "
            "CL_NEOVA2_GATEWAY_ID_HEX (owner-set at the host secret boundary) — "
            "refusing to start without a gateway identity")
    try:
        gateway_id = bytes.fromhex(gateway_id_hex)
        seed = bytes.fromhex(seed_hex)
    except ValueError as error:
        raise RuntimeError(
            "CL_NEOVA2_GATEWAY_ID_HEX (16 hex chars = 8 bytes) and CL_NEOVA2_GATEWAY_SEED_HEX "
            f"(64 hex chars = 32 bytes) must be hex: {error}") from error
    sequence_path = e.get("CL_NEOVA2_SEQ_PATH", "").strip()
    watermark_path = e.get("CL_NEOVA2_WATERMARK_PATH", "").strip()
    # The readback leg (see neova2_truck.py docstring). CL_NEOVA2_PROOF_URL points at the
    # fleet's proof path (neova2 bench/proof_server.py: GET /proof/<commitment-hex>);
    # CL_NEOVA2_PINNED_QWS_PATH pins the qualified-witness registry OUT OF BAND — the
    # bundle's embedded registry must match it byte-for-byte or the anchor is refused.
    proof_url = e.get("CL_NEOVA2_PROOF_URL", "").strip().rstrip("/")
    readback_fetch = None
    if proof_url:
        import json as _json
        import urllib.error
        import urllib.request

        def readback_fetch(commitment: bytes):
            try:
                with urllib.request.urlopen(
                        f"{proof_url}/proof/{commitment.hex()}", timeout=10) as resp:
                    return _json.load(resp)
            except urllib.error.HTTPError as error:
                if error.code in (404, 409):    # unknown / not yet witnessed: keep polling
                    return None
                raise
    pinned = None
    pinned_path = e.get("CL_NEOVA2_PINNED_QWS_PATH", "").strip()
    if proof_url and not pinned_path:
        raise RuntimeError(
            "CL_NEOVA2_PROOF_URL is set but CL_NEOVA2_PINNED_QWS_PATH is not: a readback "
            "requires a PINNED qualified-witness registry (obtained out of band) — a bundle "
            "judged against its own embedded keys is a self-signed proof. Refusing to start.")
    if pinned_path:
        import json as _json

        pinned = _json.loads(_Path(pinned_path).read_text())
    return NEOva2TruckAnchorBackend(NEOva2TruckConfig(
        host=e.get("CL_NEOVA2_DOOR_HOST", "127.0.0.1"),
        port=int(e.get("CL_NEOVA2_DOOR_PORT", "7600")),
        gateway_id=gateway_id,
        gateway_epoch=int(e.get("CL_NEOVA2_GATEWAY_EPOCH", "1")),
        signer=Ed25519PrivateKey.from_private_bytes(seed),
        sequence_path=_Path(sequence_path) if sequence_path else None,
        # The chain watermark (epoch + seq + last confirmed anchor, one file per chain).
        # Set → identity comes from the chain's memory; a stale env epoch is refused.
        watermark_path=_Path(watermark_path) if watermark_path else None,
        timeout_s=float(e.get("CL_NEOVA2_TIMEOUT_S", "30")),
        tls_cert=e.get("CL_NEOVA2_TLS_CERT", "").strip(),
        tls_key=e.get("CL_NEOVA2_TLS_KEY", "").strip(),
        tls_ca=e.get("CL_NEOVA2_TLS_CA", "").strip(),
        readback_fetch=readback_fetch,
        readback_poll_s=float(e.get("CL_NEOVA2_READBACK_POLL_S", "120")),
        readback_interval_s=float(e.get("CL_NEOVA2_READBACK_INTERVAL_S", "3")),
        pinned_witnesses=pinned,
    ), lab_mode_ack=True)   # acknowledged above, from the env dict this builder was given


def build_host_anchor_backend(
    mode: str,
    checkpoint_backend: AnchorBackend,
    *,
    env: Optional[dict] = None,
    round_log_path: Union[str, Path, None] = None,
) -> AnchorBackend:
    """Deploy-time backend selection (`CL_ANCHOR_BACKEND`). Unknown mode = fail-fast, never a silent default."""
    if mode not in HOST_ANCHOR_MODES:
        raise RuntimeError(
            f"unknown CL_ANCHOR_BACKEND {mode!r} — expected one of {'|'.join(HOST_ANCHOR_MODES)}"
        )
    if mode == "checkpoint":
        return checkpoint_backend
    if mode == "neova":
        from cl.anchor.neova import neova_backend_from_env
        return neova_backend_from_env(env)
    if mode == "neova2":
        return neova2_truck_backend_from_env(env)
    neo = NeoHostBackend(NeoLegSettings.from_env(env))
    if mode == "neo":
        return neo
    e = os.environ if env is None else env
    log_path = round_log_path if round_log_path is not None else e.get(
        "CL_DUAL_ROUND_LOG", "./dual_anchor_rounds.jsonl"
    )
    return DualAnchorBackend(checkpoint_backend, neo, round_log_path=log_path)
