"""Append-only grounded trace ledger for Gate 4."""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from .trace import Episode, utc_now

LEDGER_SCHEMA_VERSION = 1


def canonical_json(data: object) -> str:
    return json.dumps(data, sort_keys=True, separators=(",", ":"), ensure_ascii=True)


def stable_hash(data: object) -> str:
    return hashlib.sha256(canonical_json(data).encode("utf-8")).hexdigest()


@dataclass(slots=True)
class TraceLedgerRecord:
    trace_id: str
    schema_version: int
    agent_visible: dict[str, Any]
    evaluator_metadata: dict[str, Any]
    bytecode_ops: list[str]
    tool_calls: list[dict[str, Any]]
    files_read: list[str]
    files_patched: list[dict[str, Any]]
    verifier_commands: list[list[str]]
    verifier_outputs: list[dict[str, Any]]
    success: bool
    reused_skill_id: str | None = None
    fallback_used: bool = False
    quarantined_skill_id: str | None = None
    created_at: str = field(default_factory=utc_now)
    content_hash: str = ""

    @classmethod
    def from_episode(cls, episode: Episode) -> "TraceLedgerRecord":
        metadata = episode.metadata
        evaluator_metadata = dict(metadata.get("evaluator_metadata") or {})
        tool_calls = [dict(item) for item in metadata.get("tool_calls", [])]
        verifier_outputs = [dict(item) for item in metadata.get("verifier_outputs", [])]
        files_read = [
            str(item)
            for item in metadata.get("files_inspected", [])
            if str(item) != "." and str(item).endswith(".py")
        ]
        files_patched = [dict(item) for item in metadata.get("patches_applied", [])]
        verifier_commands = [
            list(call.get("args", {}).get("command", []))
            for call in tool_calls
            if call.get("operation") == "run_command"
        ]
        agent_visible = {
            "task_description": episode.task,
            "step_summaries": [step.result_summary for step in episode.steps],
            "visible_tool_summaries": [
                {
                    "operation": call.get("operation"),
                    "success": call.get("success"),
                    "summary": call.get("summary"),
                    "stdout_tail": call.get("stdout_tail", ""),
                    "stderr_tail": call.get("stderr_tail", ""),
                }
                for call in tool_calls
            ],
        }
        record = cls(
            trace_id="",
            schema_version=LEDGER_SCHEMA_VERSION,
            agent_visible=agent_visible,
            evaluator_metadata=evaluator_metadata,
            bytecode_ops=[step.instruction.op.value for step in episode.steps],
            tool_calls=tool_calls,
            files_read=files_read,
            files_patched=files_patched,
            verifier_commands=verifier_commands,
            verifier_outputs=verifier_outputs,
            success=episode.outcome == "success",
            reused_skill_id=_metadata_string(metadata, "reused_skill_id"),
            fallback_used=bool(metadata.get("fallback_used", False)),
            quarantined_skill_id=_metadata_string(metadata, "quarantined_skill_id"),
            created_at=episode.created_at,
        )
        record.trace_id = "trace_" + stable_hash(record._hash_payload())[:24]
        record.content_hash = stable_hash(record._hash_payload())
        return record

    def _hash_payload(self) -> dict[str, Any]:
        return {
            "schema_version": self.schema_version,
            "agent_visible": self.agent_visible,
            "evaluator_metadata": self.evaluator_metadata,
            "bytecode_ops": self.bytecode_ops,
            "tool_calls": self.tool_calls,
            "files_read": self.files_read,
            "files_patched": self.files_patched,
            "verifier_commands": self.verifier_commands,
            "verifier_outputs": self.verifier_outputs,
            "success": self.success,
            "reused_skill_id": self.reused_skill_id,
            "fallback_used": self.fallback_used,
            "quarantined_skill_id": self.quarantined_skill_id,
            "created_at": self.created_at,
        }

    def verify_hash(self) -> bool:
        return self.content_hash == stable_hash(self._hash_payload())

    def to_dict(self) -> dict[str, Any]:
        return {
            **self._hash_payload(),
            "trace_id": self.trace_id,
            "content_hash": self.content_hash,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "TraceLedgerRecord":
        return cls(
            trace_id=str(data["trace_id"]),
            schema_version=int(data.get("schema_version", LEDGER_SCHEMA_VERSION)),
            agent_visible=dict(data.get("agent_visible") or {}),
            evaluator_metadata=dict(data.get("evaluator_metadata") or {}),
            bytecode_ops=[str(item) for item in data.get("bytecode_ops", [])],
            tool_calls=[dict(item) for item in data.get("tool_calls", [])],
            files_read=[str(item) for item in data.get("files_read", [])],
            files_patched=[dict(item) for item in data.get("files_patched", [])],
            verifier_commands=[list(item) for item in data.get("verifier_commands", [])],
            verifier_outputs=[dict(item) for item in data.get("verifier_outputs", [])],
            success=bool(data.get("success", False)),
            reused_skill_id=str(data["reused_skill_id"]) if data.get("reused_skill_id") is not None else None,
            fallback_used=bool(data.get("fallback_used", False)),
            quarantined_skill_id=str(data["quarantined_skill_id"]) if data.get("quarantined_skill_id") is not None else None,
            created_at=str(data.get("created_at") or utc_now()),
            content_hash=str(data.get("content_hash", "")),
        )


class TraceLedger:
    def __init__(self, state_dir: str | Path, file_name: str = "trace_ledger.jsonl") -> None:
        if Path(file_name).is_absolute() or ".." in Path(file_name).parts:
            raise ValueError("ledger file name must stay inside the state directory")
        self.state_dir = Path(state_dir)
        self.state_dir.mkdir(parents=True, exist_ok=True)
        self.path = (self.state_dir / file_name).resolve()
        if not self.path.is_relative_to(self.state_dir.resolve()):
            raise ValueError("ledger path escapes state directory")

    def append(self, record: TraceLedgerRecord) -> None:
        if not record.verify_hash():
            raise ValueError("trace ledger record hash does not match content")
        with self.path.open("a", encoding="utf-8") as handle:
            handle.write(canonical_json(record.to_dict()) + "\n")

    def read_all(self) -> list[TraceLedgerRecord]:
        if not self.path.exists():
            return []
        records: list[TraceLedgerRecord] = []
        for line in self.path.read_text(encoding="utf-8").splitlines():
            if line.strip():
                records.append(TraceLedgerRecord.from_dict(json.loads(line)))
        return records

    def verify_hashes(self) -> bool:
        return all(record.verify_hash() for record in self.read_all())


def _metadata_string(metadata: dict[str, Any], key: str) -> str | None:
    value = metadata.get(key)
    return str(value) if value is not None else None
