"""ForensicLedger — AFL-JSONL-1 hash-chained tamper-evident audit log.

Original implementation from aurora-stack (libs/audit/ledger.py).
No external dependencies — pure stdlib.

canonicalize_jcs() is RFC 8785 compliant for the AFL value domain
(which forbids floats). Python's json.dumps with sort_keys=True and
separators=(',',':') is JCS-compliant for this domain.
"""

from __future__ import annotations

import datetime
import hashlib
import hmac as _hmac
import json
from pathlib import Path
from typing import Any


def _canonicalize_jcs(obj: Any) -> bytes:
    """Minimal JCS (RFC 8785) for the AFL value domain (no floats)."""
    return json.dumps(
        obj,
        sort_keys=True,
        separators=(",", ":"),
        ensure_ascii=False,
    ).encode("utf-8")


class ForensicLedger:
    """AFL-JSONL-1 forensic envelope log. One line = one event. Hash-chained."""

    def __init__(self, log_path: str | Path, trace_id: str | None = None,
                 secret_key: bytes | None = None):
        self.log_path = Path(log_path)
        self.trace_id = trace_id or f"trace:{self.log_path.name}"
        self.last_hash = "h:null"
        self.last_cid: str | None = None
        self.root_hash: str | None = None
        self.seq = 0
        self._secret_key = secret_key

        self.log_path.parent.mkdir(parents=True, exist_ok=True)

        if self.log_path.exists() and self.log_path.stat().st_size > 0:
            self._resume()

    def _resume(self) -> None:
        try:
            with open(self.log_path, encoding="utf-8") as f:
                lines = f.readlines()
            if not lines:
                return
            first = json.loads(lines[0])
            self.root_hash = first.get("root")
            last = json.loads(lines[-1])
            self.last_hash = last.get("hash", "h:null")
            self.last_cid = last.get("cid")
            self.seq = (last.get("seq") or 0) + 1
        except Exception:
            pass

    def _timestamp(self) -> str:
        return datetime.datetime.now(datetime.timezone.utc).isoformat()

    def append(
        self,
        op: str,
        payload_data: dict[str, Any],
        cid: str,
        scope: dict[str, str] | None = None,
        ext: dict[str, Any] | None = None,
        override_time: str | None = None,
    ) -> str:
        """Create and append one forensic envelope. Returns the new hash."""
        envelope: dict[str, Any] = {
            "v": 1,
            "kind": "aurora.event",
            "time": override_time or self._timestamp(),
            "seq": self.seq,
            "trace": self.trace_id,
            "prev": self.last_hash,
            "root": self.root_hash if self.root_hash else "PENDING",
            "cid": cid,
            "op": op,
            "scope": scope or {"domain": "governance", "subdomain": "aurora-lens", "authority": "policy"},
            "payload": {"pv": 1, "data": payload_data},
            "canon": "c14n:jcs",
            "ext": ext or {},
        }

        if self.seq == 0:
            envelope["root"] = "h:null"
            h = self._hash(envelope)
            envelope["root"] = h
            envelope["hash"] = h
            self.root_hash = h
        else:
            envelope["root"] = self.root_hash
            h = self._hash(envelope)
            envelope["hash"] = h

        # sig: HMAC-SHA256 over the entry's hash field when a key is available;
        # null otherwise. The hash chain is the primary tamper-detection mechanism;
        # sig provides an additional authentication layer when a secret_key is set.
        if self._secret_key is not None:
            sig_bytes = _hmac.new(
                self._secret_key,
                h.encode("utf-8"),
                "sha256",
            ).hexdigest()
            envelope["sig"] = {"alg": "hmac-sha256", "value": sig_bytes}
        else:
            envelope["sig"] = None

        with open(self.log_path, "a", encoding="utf-8") as f:
            f.write(json.dumps(envelope, separators=(",", ":"), ensure_ascii=False) + "\n")

        self.last_hash = h
        self.last_cid = cid
        self.seq += 1
        return h

    def _hash(self, envelope: dict[str, Any]) -> str:
        target = {k: v for k, v in envelope.items() if k not in ("hash", "sig", "root")}
        digest = hashlib.sha256(_canonicalize_jcs(target)).hexdigest()
        return f"h:sha256:{digest}"

    def verify(self) -> bool:
        """Verify hash chain integrity. Returns True if valid or no entries."""
        if not self.log_path.exists():
            return True
        try:
            lines = [ln for ln in self.log_path.read_text(encoding="utf-8").splitlines() if ln.strip()]
        except OSError:
            return False
        prev_hash = "h:null"
        for ln in lines:
            try:
                entry = json.loads(ln)
            except json.JSONDecodeError:
                return False
            stored = entry.get("hash", "")
            if entry.get("prev") != prev_hash and entry.get("seq", 0) != 0:
                return False
            target = {k: v for k, v in entry.items() if k not in ("hash", "sig", "root")}
            computed = f"h:sha256:{hashlib.sha256(_canonicalize_jcs(target)).hexdigest()}"
            if not (stored == computed):
                return False
            prev_hash = stored
        return True

    def get_stats(self) -> dict[str, Any]:
        return {
            "seq": self.seq,
            "log_path": str(self.log_path),
            "trace_id": self.trace_id,
            "last_hash": self.last_hash,
        }
