"""Evidence Pack builder.

Creates structured evidence packs containing run manifests, plan graphs,
events, spans, policy decisions, budget receipts, eval results, and
content-addressed artifacts.
"""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

from abi_control_core.sdk.artifacts import ArtifactStore


class EvidenceValidationError(Exception):
    """Raised when evidence pack validation fails.

    Attributes:
        errors: List of validation error messages.
        warnings: List of validation warning messages.
    """

    def __init__(self, errors: list[str], warnings: list[str] | None = None) -> None:
        self.errors = errors
        self.warnings = warnings or []
        error_summary = "; ".join(errors[:3])
        if len(errors) > 3:
            error_summary += f" ... and {len(errors) - 3} more"
        super().__init__(
            f"Evidence pack validation failed ({len(errors)} error(s)): {error_summary}"
        )


@dataclass
class ValidationResult:
    """Result of evidence pack validation.

    Attributes:
        valid: True if pack passes all validation checks.
        errors: List of validation error messages.
        warnings: List of validation warning messages (non-blocking).
    """

    valid: bool
    errors: list[str]
    warnings: list[str]


def _sha256_file(path: Path) -> str:
    """Compute SHA-256 hex digest of a file."""
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()


def _write_json(path: Path, data: Any) -> None:
    """Write JSON with consistent formatting."""
    path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")


def _write_ndjson(path: Path, records: list[dict[str, Any]]) -> None:
    """Write newline-delimited JSON."""
    lines = [json.dumps(r, ensure_ascii=False) for r in records]
    path.write_text("\n".join(lines) + "\n" if lines else "", encoding="utf-8")


def validate_evidence_pack(pack_path: Path) -> ValidationResult:
    """Validate a finalized evidence pack.

    Performs comprehensive validation including:
    - Manifest existence and valid JSON format
    - All referenced files exist on disk
    - SHA-256 hashes match file contents
    - Required components are present (run manifest + at least one gate receipt)

    Args:
        pack_path: Path to the evidence pack directory.

    Returns:
        ValidationResult with validation status, errors, and warnings.
    """
    errors: list[str] = []
    warnings: list[str] = []

    # Check that pack directory exists
    if not pack_path.exists():
        errors.append(f"Evidence pack directory does not exist: {pack_path}")
        return ValidationResult(valid=False, errors=errors, warnings=warnings)

    if not pack_path.is_dir():
        errors.append(f"Evidence pack path is not a directory: {pack_path}")
        return ValidationResult(valid=False, errors=errors, warnings=warnings)

    # Check manifest.json exists
    manifest_path = pack_path / "evidence_pack_manifest.json"
    if not manifest_path.exists():
        errors.append("Missing evidence_pack_manifest.json")
        return ValidationResult(valid=False, errors=errors, warnings=warnings)

    # Load and parse manifest
    try:
        manifest_data = json.loads(manifest_path.read_text(encoding="utf-8"))
    except json.JSONDecodeError as e:
        errors.append(f"Invalid JSON in evidence_pack_manifest.json: {e}")
        return ValidationResult(valid=False, errors=errors, warnings=warnings)
    except Exception as e:
        errors.append(f"Failed to read evidence_pack_manifest.json: {e}")
        return ValidationResult(valid=False, errors=errors, warnings=warnings)

    # Extract components and hashes from manifest
    components = manifest_data.get("components", {})
    hashes = manifest_data.get("hashes", {})

    # Check required components
    if "run_manifest" not in components:
        errors.append("Missing required component: run_manifest")

    # Check that at least one gate receipt exists (either budget_receipts or eval_results)
    has_gate_receipt = any(
        comp in components for comp in ["budget_receipts", "eval_results"]
    )
    if not has_gate_receipt:
        warnings.append(
            "No gate receipt found (expected budget_receipts or eval_results)"
        )

    # Verify all files referenced in hashes exist
    for file_path_str, expected_hash in hashes.items():
        file_path = pack_path / file_path_str
        if not file_path.exists():
            errors.append(f"Referenced file missing: {file_path_str}")
            continue

        # Recompute hash and compare
        try:
            actual_hash = f"sha256:{_sha256_file(file_path)}"
            if actual_hash != expected_hash:
                errors.append(
                    f"Hash mismatch for {file_path_str}: "
                    f"expected {expected_hash}, got {actual_hash}"
                )
        except Exception as e:
            errors.append(f"Failed to compute hash for {file_path_str}: {e}")

    # Verify all component files exist
    for component_name, component_file in components.items():
        component_path = pack_path / component_file
        if not component_path.exists():
            errors.append(f"Component file missing: {component_file} ({component_name})")

    # Verify hashes.json exists
    hashes_path = pack_path / "hashes.json"
    if not hashes_path.exists():
        errors.append("Missing hashes.json")

    # Check artifacts directory if artifact_digests are present
    artifact_digests = manifest_data.get("artifact_digests", [])
    if artifact_digests:
        artifacts_dir = pack_path / "artifacts"
        if not artifacts_dir.exists():
            errors.append("Manifest lists artifacts but artifacts directory is missing")
        else:
            # Verify each artifact exists
            for digest_str in artifact_digests:
                # Strip "sha256:" prefix if present
                digest = digest_str.replace("sha256:", "")
                artifact_path = artifacts_dir / digest
                if not artifact_path.exists():
                    errors.append(f"Artifact missing: {digest}")

    valid = len(errors) == 0
    return ValidationResult(valid=valid, errors=errors, warnings=warnings)


def validate_before_promote(pack_path: Path) -> ValidationResult:
    """Pre-promotion validation hook for evidence packs.

    This function should be called before promoting an evidence pack to ensure
    it meets all quality requirements.

    Args:
        pack_path: Path to the evidence pack directory.

    Returns:
        ValidationResult with validation status, errors, and warnings.

    Raises:
        EvidenceValidationError: If validation fails (valid=False).
    """
    result = validate_evidence_pack(pack_path)
    if not result.valid:
        raise EvidenceValidationError(result.errors, result.warnings)
    return result


class EvidencePackBuilder:
    """Builds an evidence pack directory for a given run.

    Usage::

        builder = EvidencePackBuilder(base_dir=Path("evidence_packs"), run_id="abcd1234")
        builder.set_manifest(run_manifest_dict)
        builder.set_plan_graph(plan_graph_dict)
        builder.add_events([event1, event2])
        builder.add_spans([span1, span2])
        builder.add_policy_decisions([decision1])
        builder.set_budget_receipts(receipts_dict)
        builder.set_eval_results(eval_results_dict)
        builder.add_artifact(b"file contents")
        summary = builder.finalize()
    """

    def __init__(self, base_dir: Path, run_id: str) -> None:
        self.run_id = run_id
        self.pack_dir = base_dir / run_id
        self.pack_dir.mkdir(parents=True, exist_ok=True)

        self._cas = ArtifactStore(self.pack_dir / "artifacts")
        self._components: dict[str, str] = {}
        self._artifact_digests: list[str] = []

    def set_manifest(self, manifest: dict[str, Any]) -> None:
        """Write the run manifest."""
        path = self.pack_dir / "manifest.json"
        _write_json(path, manifest)
        self._components["run_manifest"] = "manifest.json"

    def set_plan_graph(self, plan_graph: dict[str, Any]) -> None:
        """Write the plan graph."""
        path = self.pack_dir / "plan_graph.json"
        _write_json(path, plan_graph)
        self._components["plan_graph"] = "plan_graph.json"

    def add_events(self, events: list[dict[str, Any]]) -> None:
        """Write events as NDJSON."""
        path = self.pack_dir / "events.ndjson"
        _write_ndjson(path, events)
        self._components["events"] = "events.ndjson"

    def add_spans(self, spans: list[dict[str, Any]]) -> None:
        """Write spans as NDJSON."""
        path = self.pack_dir / "spans.ndjson"
        _write_ndjson(path, spans)
        self._components["spans"] = "spans.ndjson"

    def add_policy_decisions(self, decisions: list[dict[str, Any]]) -> None:
        """Write policy decisions as NDJSON."""
        path = self.pack_dir / "policy_decisions.ndjson"
        _write_ndjson(path, decisions)
        self._components["policy_decisions"] = "policy_decisions.ndjson"

    def set_budget_receipts(self, receipts: dict[str, Any]) -> None:
        """Write budget receipts."""
        path = self.pack_dir / "budget_receipts.json"
        _write_json(path, receipts)
        self._components["budget_receipts"] = "budget_receipts.json"

    def set_eval_results(self, results: dict[str, Any]) -> None:
        """Write eval results."""
        path = self.pack_dir / "eval_results.json"
        _write_json(path, results)
        self._components["eval_results"] = "eval_results.json"

    def add_artifact(self, data: bytes) -> str:
        """Store an artifact in the CAS and return its digest."""
        digest, _ = self._cas.put_bytes(data)
        self._artifact_digests.append(f"sha256:{digest}")
        return digest

    def add_artifact_file(self, source: Path) -> str:
        """Store a file artifact in the CAS and return its digest."""
        digest, _ = self._cas.put_file(source)
        self._artifact_digests.append(f"sha256:{digest}")
        return digest

    def finalize(self) -> dict[str, Any]:
        """Finalize the evidence pack: compute hashes, write manifest, and validate.

        Returns:
            Summary dict with pack_dir, run_id, file_hashes, artifact_digests, components.

        Raises:
            EvidenceValidationError: If the finalized pack fails validation.
        """
        # Compute hashes for all written files (excluding the hashes file itself
        # and the evidence pack manifest)
        file_hashes: dict[str, str] = {}
        for path in sorted(self.pack_dir.rglob("*")):
            if path.is_file() and path.name not in ("hashes.json", "evidence_pack_manifest.json"):
                rel = path.relative_to(self.pack_dir).as_posix()
                file_hashes[rel] = f"sha256:{_sha256_file(path)}"

        # Write hashes.json
        hashes_path = self.pack_dir / "hashes.json"
        _write_json(hashes_path, file_hashes)

        # Write evidence pack manifest
        ep_manifest = {
            "schema_version": "evidence-pack-manifest/1.0",
            "run_id": self.run_id,
            "created_at": datetime.now(UTC).isoformat(),
            "canonical_fields": [
                "/components",
                "/hashes",
                "/artifact_digests",
                "/summary/verdict",
            ],
            "metadata_fields": [
                "/created_at",
                "/run_id",
                "/summary/total_files",
                "/summary/total_artifacts",
            ],
            "components": self._components,
            "hashes": file_hashes,
            "artifact_digests": self._artifact_digests,
            "summary": {
                "total_files": len(file_hashes),
                "total_artifacts": len(self._artifact_digests),
            },
        }
        _write_json(self.pack_dir / "evidence_pack_manifest.json", ep_manifest)

        # Validate the finalized pack
        validation_result = validate_evidence_pack(self.pack_dir)
        if not validation_result.valid:
            raise EvidenceValidationError(validation_result.errors, validation_result.warnings)

        return {
            "pack_dir": str(self.pack_dir),
            "run_id": self.run_id,
            "file_hashes": file_hashes,
            "artifact_digests": self._artifact_digests,
            "components": self._components,
        }
