"""Evidence-Vault — zentraler Beweis-Bereich pro Agent-Run.

User-Vorgabe 2026-06-08: pro Agent-Run brauchen wir einen Bereich um
Playwright-Videos (mp4), Screenshots (png) und CSV-Exporte abzulegen,
damit DSB/Anwalt eine portable Beweiskette hat.

Layout:
  artifacts/agent_runs/{run_id}/
    manifest.json                  ← Index + SHA256 pro Asset
    screenshots/
      {slot}_{name}.png
    videos/
      {slot}_walk.mp4
    csv/
      cookies_{slot}.csv
    findings/
      {slot}_output.json           ← AgentOutput
    raw/
      {slot}_doc.html              ← Original-Source

Pro Run-ID: kann mehrere "Slots" enthalten (z.B. 5 URLs).

manifest.json:
  {
    "run_id": "...",
    "agent_id": "impressum",
    "agent_version": "2.0",
    "started_at": "2026-06-08T...",
    "assets": [
      {"slot": "url1", "kind": "video", "path": "videos/url1_walk.mp4",
       "sha256": "...", "size_bytes": 12345, "mime": "video/mp4"}
    ]
  }
"""

from __future__ import annotations

import hashlib
import json
import logging
import mimetypes
import os
import shutil
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)


def _vault_root() -> Path:
    """Resolved at call time so tests can monkeypatch the env var.
    Default to /tmp so the appuser can always write (container-friendly)."""
    return Path(os.environ.get(
        "EVIDENCE_VAULT_ROOT", "/tmp/breakpilot/agent_runs",
    ))


class EvidenceVault:
    """Pro Agent-Run eine Instanz. Schreibt alle Assets unter
    VAULT_ROOT/{run_id}/."""

    def __init__(
        self,
        agent_id: str,
        agent_version: str,
        run_id: str | None = None,
    ) -> None:
        self.run_id = run_id or uuid.uuid4().hex[:16]
        self.agent_id = agent_id
        self.agent_version = agent_version
        self.root = _vault_root() / self.run_id
        self.root.mkdir(parents=True, exist_ok=True)
        for sub in ("screenshots", "videos", "csv",
                     "findings", "raw"):
            (self.root / sub).mkdir(exist_ok=True)
        self.manifest_path = self.root / "manifest.json"
        self._manifest: dict[str, Any] = self._load_or_init()
        # Persist immediately so the file exists even before any
        # put_*() call — important for ../runs-listing.
        if not self.manifest_path.exists():
            self._save_manifest()

    def _load_or_init(self) -> dict[str, Any]:
        if self.manifest_path.exists():
            try:
                return json.loads(self.manifest_path.read_text())
            except Exception as e:
                logger.warning("manifest load fail, reset: %s", e)
        return {
            "run_id": self.run_id,
            "agent_id": self.agent_id,
            "agent_version": self.agent_version,
            "started_at": datetime.now(timezone.utc).isoformat(),
            "assets": [],
        }

    def _save_manifest(self) -> None:
        self.manifest_path.write_text(
            json.dumps(self._manifest, indent=2, default=str),
        )

    def put_bytes(
        self,
        kind: str,
        slot: str,
        filename: str,
        data: bytes,
        mime: str | None = None,
    ) -> str:
        """Schreibt Bytes in einen Vault-Slot.

        kind: 'screenshot' | 'video' | 'csv' | 'finding' | 'raw'
        slot: identifier (z.B. URL-Index "url1")
        Returns: relative path inside the vault.
        """
        subdir = self._subdir_for(kind)
        safe_name = self._safe_filename(filename)
        rel = f"{subdir}/{slot}_{safe_name}"
        target = self.root / rel
        target.write_bytes(data)
        sha = hashlib.sha256(data).hexdigest()
        if not mime:
            mime, _ = mimetypes.guess_type(safe_name)
            mime = mime or "application/octet-stream"
        self._manifest["assets"].append({
            "slot": slot,
            "kind": kind,
            "path": rel,
            "sha256": sha,
            "size_bytes": len(data),
            "mime": mime,
            "stored_at": datetime.now(timezone.utc).isoformat(),
        })
        self._save_manifest()
        return rel

    def put_file(
        self,
        kind: str,
        slot: str,
        source_path: str,
        target_filename: str | None = None,
    ) -> str:
        """Copy a file (e.g. Playwright video) into the vault."""
        src = Path(source_path)
        if not src.is_file():
            raise FileNotFoundError(source_path)
        target_filename = target_filename or src.name
        data = src.read_bytes()
        return self.put_bytes(
            kind, slot, target_filename, data,
            mime=mimetypes.guess_type(src.name)[0],
        )

    def put_json(
        self,
        kind: str,
        slot: str,
        filename: str,
        payload: Any,
    ) -> str:
        data = json.dumps(payload, indent=2, default=str).encode("utf-8")
        return self.put_bytes(kind, slot, filename, data,
                                mime="application/json")

    def assets_for_slot(self, slot: str) -> list[dict]:
        return [a for a in self._manifest["assets"] if a["slot"] == slot]

    def list_assets(self) -> list[dict]:
        return list(self._manifest["assets"])

    def asset_path(self, relative_path: str) -> Path | None:
        """Resolve a manifest-relative path to absolute. Returns None
        if outside vault (defense against path traversal)."""
        p = (self.root / relative_path).resolve()
        try:
            p.relative_to(self.root.resolve())
        except ValueError:
            return None
        if not p.exists():
            return None
        return p

    def finalize(self) -> dict[str, Any]:
        """Marks the run as finished and returns the manifest snapshot."""
        self._manifest["finished_at"] = datetime.now(
            timezone.utc).isoformat()
        self._save_manifest()
        return dict(self._manifest)

    def url(self) -> str:
        """API-URL um die Assets zu listen (für Frontend)."""
        return f"/api/v1/specialist-agent/run/{self.run_id}/artifacts"

    @staticmethod
    def _subdir_for(kind: str) -> str:
        return {
            "screenshot": "screenshots",
            "video": "videos",
            "csv": "csv",
            "finding": "findings",
            "raw": "raw",
        }.get(kind, "raw")

    @staticmethod
    def _safe_filename(name: str) -> str:
        # Strip directory prefixes first so "../../etc/passwd" → "passwd"
        base = os.path.basename(name.replace("\\", "/"))
        keep = "._-"
        cleaned = "".join(
            c if (c.isalnum() or c in keep) else "_" for c in base
        )
        # Collapse parent-dir markers that survived in unusual encodings
        cleaned = cleaned.replace("..", "_")
        return cleaned[:120] or "unnamed"


def open_vault(
    agent_id: str,
    agent_version: str,
    run_id: str | None = None,
) -> EvidenceVault:
    return EvidenceVault(agent_id, agent_version, run_id)


def list_runs(limit: int = 50) -> list[dict]:
    """Lists recent run-manifests."""
    root = _vault_root()
    if not root.exists():
        return []
    runs: list[tuple[float, dict]] = []
    for p in root.iterdir():
        if not p.is_dir():
            continue
        m = p / "manifest.json"
        if not m.is_file():
            continue
        try:
            data = json.loads(m.read_text())
            runs.append((m.stat().st_mtime, data))
        except Exception:
            continue
    runs.sort(key=lambda x: x[0], reverse=True)
    return [r[1] for r in runs[:limit]]


def delete_run(run_id: str) -> bool:
    """Tombstone: löscht den ganzen Run-Ordner. Für DSGVO-DSR Art. 17."""
    p = _vault_root() / run_id
    if not p.exists():
        return False
    shutil.rmtree(p)
    return True
