"""Governor bridge implementation (`AuroraScannerGateBridge`) — AFL-JSONL-1 forensic ledger, CID, and HMAC attestation.

All infrastructure is local — no external dependencies required.

- AFL-JSONL-1 hash-chained forensic ledger (ForensicLedger) — backend="ledger"
- Flat JSONL audit via audit_io (BuiltinBridge-compatible format) — backend="jsonl"
- Content-addressed identifiers via FallbackCIDProvider (FNV-1a 64-bit)
- HMAC-signed AttestedOutput for compliance/audit
- PEF continuity chain across all ledger entries

kernel_step() was removed as a co-author of policy decisions when
CanonicalScannerGateBridge was introduced. No external dependencies are
required for any governance decision or audit operation.
"""

from __future__ import annotations

import datetime
import hashlib
import json
import uuid
from pathlib import Path
from typing import TYPE_CHECKING, Any

from aurora_lens.govern.bridge import GovernanceBridge, build_forensic_event, enforce, ESCALATION_ROUTES, _ACTION_TO_PATHWAY
from aurora_lens.govern.audit_io import append_audit_entry
from aurora_lens.log_slice import consume_log_slice
from aurora_lens.govern.decision import GovernanceDecision, InterventionAction
from aurora_lens.govern.policy import InterventionPolicy, DEFAULT_STRICT
from aurora_lens.govern.cid import PEFFingerprint, FallbackCIDProvider
from aurora_lens.govern.forensic_ledger import ForensicLedger
from aurora_lens.govern.attestation import AttestedOutput, sign_attested_output, verify_governed_output
from aurora_lens.verify.flags import Flag, FlagType

if TYPE_CHECKING:
    from aurora_lens.adapters.base import LLMAdapter
    from aurora_lens.pef.state import PEFState


# Retained for backward compatibility — always True now that all infrastructure is local.
_SCANNER_GATE_AVAILABLE = True

# ── Bridge ───────────────────────────────────────────────────────────

class AuroraScannerGateBridge(GovernanceBridge):
    """Governor bridge with AFL-JSONL-1 forensic ledger and HMAC attestation.

    Provides:
    - ForensicLedger for AFL-JSONL-1 hash-chained audit (tamper-evident)
    - AttestedOutput for HMAC-signed governance decisions
    - FallbackCIDProvider for content-addressed identifiers (FNV-1a 64-bit)

    Uses InterventionPolicy for flag→action mapping.
    See ``CanonicalScannerGateBridge`` for the canonical Governor pipeline (PolicyResolver
    as sole decision authority).
    """

    def __init__(
        self,
        policy: InterventionPolicy | None = None,
        audit_path: str | Path | None = None,
        secret_key: bytes | None = None,
        constraints: dict | None = None,
        max_revision_attempts: int = 1,
        backend: str = "ledger",
        default_policy: str = "strict",
    ):
        """
        Args:
            policy: Intervention policy (defaults to DEFAULT_STRICT).
            audit_path: Path for audit log.
            secret_key: HMAC key for AttestedOutput signing.
                        If None, attestation is skipped.
            constraints: Retained for interface compatibility.
            max_revision_attempts: Hard limit on revision loop (1–10).
            backend: Audit format — "ledger" (AFL-JSONL-1) or "jsonl" (flat JSONL).
            default_policy: Deployment default policy name ("strict" | "moderate").
                            Used as policy_profile fallback in flat JSONL entries.
        """
        self._policy = policy or DEFAULT_STRICT
        self._secret_key = secret_key
        self._max_revision_attempts = max(1, min(10, max_revision_attempts))
        self._last_attestation: AttestedOutput | None = None
        self._constraints = constraints or {}
        self._backend = backend if backend in ("ledger", "jsonl") else "ledger"
        self._default_policy = default_policy

        # Store audit path string for JSONL writing
        self._audit_path_str: str | None = str(audit_path) if audit_path else None

        # CID provider (FNV-1a 64-bit)
        self._cid_provider = FallbackCIDProvider()

        # Forensic ledger (hash-chained, tamper-evident) — only for "ledger" backend
        if audit_path and self._backend == "ledger":
            self._trace_id = (
                f"aurora-lens:"
                f"{datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%S')}"
            )
            self._ledger: ForensicLedger | None = ForensicLedger(
                log_path=str(audit_path),
                trace_id=self._trace_id,
            )
        else:
            self._trace_id = None
            self._ledger = None

        # Chain state for the flat JSONL path (D2 hash chain).
        # Initialized to "genesis" so the first entry is anchored.
        self._prev_cid: str = "genesis"

        # Process-startup identifier — stable for the lifetime of this bridge instance.
        # Allows audit log entries to be attributed to a specific process/restart.
        self._run_id: str = str(uuid.uuid4())

    # ── GovernanceBridge interface ────────────────────────────────

    async def decide(
        self,
        flags: list[Flag],
        response_text: str,
        pef: PEFState,
    ) -> GovernanceDecision:
        """Evaluate flags via policy + kernel validation."""
        # InterventionPolicy maps flags → proposed action.
        # GovernanceKernel is attestation/ledger substrate only — it does not
        # co-author the policy decision. See CanonicalScannerGateBridge for the
        # canonical Governor pipeline (PolicyResolver replaces InterventionPolicy).
        proposed_action, rule = self._policy.evaluate_with_rule(flags)
        rule_id = rule.rule_id if rule else None
        action = proposed_action

        rationale = self._build_rationale(flags, action)

        # Generate CID directly from cid_provider (no kernel_step() needed).
        fp = PEFFingerprint(
            kind=action.name,
            head=rationale[:32],
            rationale=f"flags:{len(flags)}",
        )
        cid = self._cid_provider.cid_for_pef(fp)
        note = (
            self._build_governance_note(flags)
            if action == InterventionAction.SOFT_CORRECT
            else None
        )

        safe_alt = rule.safe_alt if rule else None
        resource = None
        if rule and action == InterventionAction.HARD_STOP:
            route = ESCALATION_ROUTES.get(rule.flag_type.name, (None, None, None, None))
            resource = route[3] if len(route) > 3 else None

        return GovernanceDecision(
            action=action,
            flags=flags,
            rationale=rationale,
            policy=self._policy.name,
            attempt=0,
            governance_note=note,
            cid=cid,
            rule_id=rule_id,
            safe_alt=safe_alt,
            resource=resource,
            pathway_id=_ACTION_TO_PATHWAY.get(action),
        )

    async def intervene(
        self,
        decision: GovernanceDecision,
        adapter: LLMAdapter,
        user_input: str,
        pef_context: str,
        history: list[dict[str, str]] | None = None,
    ) -> str:
        """Execute the intervention. Uses enforce() for deterministic output."""
        model_output = decision.original_response or ""
        msg = enforce(decision, model_output)
        decision.governed_response = msg

        if decision.escalation_level >= 1 and decision.action != InterventionAction.PASS:
            self._emit_forensic_envelope(decision, pef_context)
        return msg

    def _emit_forensic_envelope(
        self,
        decision: GovernanceDecision,
        pef_context: str,
    ) -> None:
        """Emit forensic/audit envelope for every non-ADMIT outcome.

        Level 1 (CLARIFY), 2 (REFUSE), 3 (STOP) — all produce an audit artifact.
        """
        if self._ledger is None:
            return
        if decision.action == InterventionAction.PASS:
            return

        failed_constraints = [f.flag_type.name for f in decision.flags]
        state_hash = hashlib.sha256(pef_context.encode()).hexdigest()[:32]
        ts = datetime.datetime.now(datetime.timezone.utc).isoformat()

        # Phase 7: policy_rule, trigger_spans, action_taken, category, domain, subdomain, requires_role
        trigger_spans = [f.evidence[:200] for f in decision.flags[:3]]
        _LEVEL_TO_ACTION = {1: "CLARIFY", 2: "REFUSE", 3: "STOP"}
        action_taken = _LEVEL_TO_ACTION.get(decision.escalation_level, "REFUSE")
        category = decision.flags[0].flag_type.value if decision.flags else "uncategorized"
        route = ESCALATION_ROUTES.get(decision.flags[0].flag_type.name, (None, None, None, None)) if decision.flags else (None, None, None, None)
        domain = route[0] if route[0] else "governance"
        subdomain = route[1]
        requires_role = route[2]

        envelope = {
            "type": "forensic_envelope",
            "trace_id": self._trace_id or "",
            "timestamp": ts,
            "escalation_level": decision.escalation_level,
            "domain": domain,
            "attempted_action": decision.action.name,
            "action_taken": action_taken,
            "category": category,
            "subdomain": subdomain,
            "requires_role": requires_role,
            "failed_constraints": failed_constraints,
            "trigger_spans": trigger_spans,
            "state_hash": state_hash,
        }
        if decision.rule_id is not None:
            envelope["policy_rule"] = decision.rule_id
        from aurora_lens.context import auth_label_var, session_id_var
        sid = session_id_var.get(None)
        if sid is not None:
            envelope["session_id"] = sid
        auth_label = auth_label_var.get(None)
        envelope["consumer_label"] = auth_label  # Always present; null when auth disabled
        if auth_label is not None:
            envelope["auth_label"] = auth_label

        log_slice = consume_log_slice()
        if log_slice is not None:
            envelope.update(log_slice)

        # Deterministic serialization: sort keys before ledger append (matches BuiltinBridge sort_keys=True)
        envelope = dict(sorted(envelope.items()))

        # Self-hash: SHA-256 of canonical JSON of envelope fields (excluding event_hash itself).
        envelope["event_hash"] = "sha256:" + hashlib.sha256(
            json.dumps(envelope, separators=(",", ":"), sort_keys=True, ensure_ascii=False).encode("utf-8")
        ).hexdigest()

        fp = PEFFingerprint(
            kind="forensic_envelope",
            head=state_hash[:16],
            rationale=f"{decision.action.name}:{ts}",
        )
        cid = self._cid_provider.cid_for_pef(fp)
        self._ledger.append(
            op="forensic_envelope",
            payload_data=envelope,
            cid=cid,
            scope={
                "domain": "governance",
                "subdomain": "aurora-lens",
                "event": f"NON_ADMIT_{decision.action.name}",
            },
        )

    def log_decision(
        self,
        decision: GovernanceDecision,
        turn: int = 0,
        *,
        stream: bool = False,
        stream_completed: bool = True,
        stream_abort_reason: str | None = None,
        stream_truncated: bool = False,
        stream_dropped_chars: int = 0,
        pef_context: str | None = None,
        pre_llm: bool = False,
        pef_snapshot: dict | None = None,
    ) -> None:
        """Log to audit backend (flat JSONL or AFL-JSONL-1) + produce AttestedOutput."""
        # Consume log slice once — it is cleared after the first read.
        log_slice = consume_log_slice()

        if self._backend == "jsonl":
            self._log_jsonl(
                decision,
                turn=turn,
                stream=stream,
                stream_completed=stream_completed,
                stream_abort_reason=stream_abort_reason,
                stream_truncated=stream_truncated,
                stream_dropped_chars=stream_dropped_chars,
                pef_context=pef_context,
                pre_llm=pre_llm,
                pef_snapshot=pef_snapshot,
                log_slice=log_slice,
            )
            return

        # ── AFL-JSONL-1 ledger path ────────────────────────────────────────────
        if self._ledger is None and self._secret_key is None:
            return

        # Build payload
        payload = {
            "event_type": "governance_decision",
            "turn": turn,
            "escalation_level": decision.escalation_level,
            "policy": decision.policy,
            "action": decision.action.name,
            "flags": [
                {"type": f.flag_type.name, "severity": f.severity, "claim": f.claim}
                for f in decision.flags
            ],
            "rationale": decision.rationale,
            "attempt": decision.attempt,
            **({"original_response": decision.original_response} if decision.original_response else {}),
            **({"governed_response": decision.governed_response} if decision.governed_response else {}),
            **({"governance_note": decision.governance_note} if decision.governance_note else {}),
        }
        from aurora_lens.context import auth_label_var
        auth_label = auth_label_var.get(None)
        payload["consumer_label"] = auth_label  # Always present; null when auth disabled
        if auth_label is not None:
            payload["auth_label"] = auth_label

        if stream:
            payload["stream"] = True
            payload["stream_completed"] = stream_completed
            if not stream_completed and stream_abort_reason:
                payload["stream_abort_reason"] = stream_abort_reason
            if stream_completed:
                if stream_truncated:
                    payload["stream_truncated"] = True
                    payload["stream_dropped_chars"] = stream_dropped_chars

        if log_slice is not None:
            payload["log_slice_present"] = True
            payload.update(log_slice)
        else:
            payload["log_slice_present"] = False

        # Generate CID for this decision event
        fp = PEFFingerprint(
            kind=decision.action.name,
            head=(decision.rationale or "")[:32],
            rationale=f"turn:{turn}:flags:{len(decision.flags)}",
        )
        cid = self._cid_provider.cid_for_pef(fp)
        decision.cid = cid

        # Canonical forensic_event for non-ADMIT (response + audit)
        if decision.action not in (InterventionAction.PASS, InterventionAction.SOFT_CORRECT):
            ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
            from aurora_lens.context import trace_id_var
            trace_id = trace_id_var.get(None) or (self._trace_id or "")
            decision.forensic_event = build_forensic_event(
                decision,
                pre_llm=pre_llm,
                pef_snapshot=pef_snapshot,
                trace_id=trace_id,
                timestamp=ts,
                audit_id=cid,
            )
            payload["forensic_event"] = decision.forensic_event
            if pef_snapshot is not None:
                payload["pef_snapshot"] = pef_snapshot

        # Append to forensic ledger (hash-chained, tamper-evident)
        # ext.pef: SHA-256 of payload for PEF continuity chain.
        if self._ledger is not None:
            pef_hash = hashlib.sha256(
                json.dumps(payload, sort_keys=True, ensure_ascii=False).encode()
            ).hexdigest()
            self._ledger.append(
                op=decision.action.name,
                payload_data=payload,
                cid=cid,
                scope={
                    "domain": "governance",
                    "subdomain": "aurora-lens",
                    "authority": "policy",
                },
                ext={"pef": pef_hash},
            )

        # Produce AttestedOutput (HMAC-signed)
        if self._secret_key is not None:
            content = decision.corrected_response or decision.original_response or ""
            from aurora_lens.context import session_id_var as _sid2_var, trace_id_var as _tid2_var
            _ts_attest = datetime.datetime.now(datetime.timezone.utc).isoformat()
            attested = AttestedOutput(
                content=content,
                policy_cid=f"policy:{decision.policy}",
                rationale_code=decision.action.name,
                decision=decision.action.name,
                event_cid=cid,
                signature="",  # will be replaced by signing
                meta={
                    "turn": turn,
                    "flags": len(decision.flags),
                    "attempt": decision.attempt,
                    "timestamp": _ts_attest,
                    "trace_id": _tid2_var.get(None) or (self._trace_id or ""),
                    "session_id": _sid2_var.get(None),
                    "run_id": self._run_id,
                },
            )
            self._last_attestation = sign_attested_output(attested, self._secret_key)

    def _log_jsonl(
        self,
        decision: GovernanceDecision,
        turn: int = 0,
        *,
        stream: bool = False,
        stream_completed: bool = True,
        stream_abort_reason: str | None = None,
        stream_truncated: bool = False,
        stream_dropped_chars: int = 0,
        pef_context: str | None = None,
        pre_llm: bool = False,
        pef_snapshot: dict | None = None,
        log_slice: dict | None = None,
    ) -> None:
        """Write flat JSONL audit entry (BuiltinBridge-compatible format)."""
        if not self._audit_path_str and self._secret_key is None:
            return

        from aurora_lens.context import (
            auth_label_var,
            auth_policy_var,
            session_id_var,
            trace_id_var,
            request_hash_var,
        )

        ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
        trace_id = trace_id_var.get(None) or ""
        auth_label = auth_label_var.get(None)
        policy_profile = auth_policy_var.get(None) or self._default_policy
        failed_constraints = [f.flag_type.name for f in decision.flags] if decision.flags else []
        pef_ctx = pef_context or ""
        state_hash = hashlib.sha256(pef_ctx.encode()).hexdigest()[:32] if pef_ctx else ""
        request_hash = request_hash_var.get(None)

        entry: dict[str, Any] = {
            "schema_version": 2,
            "run_id": self._run_id,
            "trace_id": trace_id,
            "timestamp": ts,
            "session_id": session_id_var.get(None),
            "turn": turn,
            "tenant_label": auth_label,
            "consumer_label": auth_label,
            "policy_profile": policy_profile,
            "outcome": decision.action.name,
            "failed_constraints": failed_constraints,
            "original_response": decision.original_response,
            "governed_response": decision.governed_response,
            "state_hash": state_hash,
        }
        if request_hash:
            entry["request_hash"] = request_hash
        if decision.rationale:
            entry["rationale"] = decision.rationale
        if decision.governance_note:
            entry["governance_note"] = decision.governance_note

        # Build forensic_event for non-PASS/non-SOFT_CORRECT outcomes
        if decision.action not in (InterventionAction.PASS, InterventionAction.SOFT_CORRECT):
            fe = build_forensic_event(
                decision,
                pre_llm=pre_llm,
                pef_snapshot=pef_snapshot,
                trace_id=trace_id,
                timestamp=ts,
            )
            decision.forensic_event = fe
            entry["forensic_event"] = fe
            if pef_snapshot is not None:
                entry["pef_snapshot"] = pef_snapshot

        if stream:
            entry["stream"] = True
            entry["stream_completed"] = stream_completed
            if not stream_completed and stream_abort_reason:
                entry["stream_abort_reason"] = stream_abort_reason
            if stream_completed and stream_truncated:
                entry["stream_truncated"] = True
                entry["stream_dropped_chars"] = stream_dropped_chars

        if log_slice is not None:
            entry.update(log_slice)

        if decision.unexpected_unclassified_termination:
            entry["unexpected_unclassified_termination"] = True
            entry["fallback_reason"] = decision.fallback_reason

        # Generate CID
        fp = PEFFingerprint(
            kind=decision.action.name,
            head=(decision.rationale or "")[:32],
            rationale=f"turn:{turn}:flags:{len(decision.flags)}",
        )
        cid = self._cid_provider.cid_for_pef(fp)
        decision.cid = cid

        if self._audit_path_str:
            new_cid = append_audit_entry(
                self._audit_path_str,
                entry,
                signing_key=self._secret_key,
                prev_cid=self._prev_cid,
            )
            if new_cid:
                decision.cid = new_cid
                self._prev_cid = new_cid

        # Produce AttestedOutput (HMAC-signed)
        if self._secret_key is not None:
            content = decision.corrected_response or decision.original_response or ""
            from aurora_lens.context import session_id_var as _sid_var, trace_id_var as _tid_var
            attested = AttestedOutput(
                content=content,
                policy_cid=f"policy:{policy_profile}",
                rationale_code=decision.action.name,
                decision=decision.action.name,
                event_cid=cid,
                signature="",
                meta={
                    "turn": turn,
                    "flags": len(decision.flags),
                    "attempt": decision.attempt,
                    "timestamp": ts,
                    "trace_id": trace_id_var.get(None) or "",
                    "session_id": _sid_var.get(None),
                    "run_id": self._run_id,
                },
            )
            self._last_attestation = sign_attested_output(attested, self._secret_key)

    # ── Public utilities ──────────────────────────────────────────

    @property
    def last_attestation(self) -> AttestedOutput | None:
        """The most recent AttestedOutput (None if signing not configured)."""
        return self._last_attestation

    @property
    def kernel_stats(self) -> dict:
        """Stub — GovernanceKernel is not used. Returns minimal stats dict."""
        return {"turn_index": 0, "status": "not_used"}

    def verify_ledger(self) -> bool:
        """Verify the forensic ledger hash chain integrity.

        Returns True if chain is valid (or no ledger configured).
        """
        if self._ledger is None:
            return True
        return self._ledger.verify()

    def verify_attestation(
        self,
        attested: AttestedOutput | None = None,
    ) -> bool:
        """Verify an AttestedOutput signature.

        If no attestation passed, verifies the last one produced.
        Returns False if no secret key or no attestation available.
        """
        if self._secret_key is None:
            return False
        target = attested or self._last_attestation
        if target is None:
            return False
        return verify_governed_output(target, self._secret_key)

    @property
    def ledger_stats(self) -> dict | None:
        """Return ledger statistics (None if no ledger configured)."""
        if self._ledger is None:
            return None
        return self._ledger.get_stats()

    # ── Internal ──────────────────────────────────────────────────

    def _build_rationale(self, flags: list[Flag], action: InterventionAction) -> str:
        if not flags:
            return "No verification flags"

        flag_types = set(f.flag_type.name for f in flags)
        severities = set(f.severity for f in flags)
        worst = "error" if "error" in severities else "warning"

        return (
            f"{action.name}: {len(flags)} flag(s) "
            f"[{', '.join(sorted(flag_types))}] "
            f"(worst severity: {worst})"
        )

    def _build_governance_note(self, flags: list[Flag]) -> str:
        parts = []
        for flag in flags:
            parts.append(f"{flag.flag_type.name}: {flag.claim} — {flag.evidence}")
        return "; ".join(parts)
