"""Pure snapshot loading logic, shared by CLI and MCP server.

No click/rich/display dependencies — raises standard exceptions on error.
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from ._debug_session import normalize_snapshot_view
from .compact_keys import expand_keys, is_compact_snapshot
from .output import get_latest_snapshot


def _load_snapshot_file_raw(path: Path) -> dict[str, Any]:
    """Load a snapshot file as raw expanded dict (no session normalization)."""
    if not path.exists():
        raise FileNotFoundError(f"File not found: {path}")

    if path.suffix == ".toon":
        try:
            from .toon_encoder import decode_snapshot_toon

            data = decode_snapshot_toon(path.read_text("utf-8"))
        except ImportError:
            raise
        except Exception as e:
            raise ValueError(f"Failed to decode TOON file {path}: {e}") from e
    else:
        data = json.loads(path.read_text("utf-8"))

    if isinstance(data, dict) and is_compact_snapshot(data):
        expanded = expand_keys(data)
        if isinstance(expanded, dict):
            expanded.pop("__llmdebug_compact__", None)
            return expanded
    if not isinstance(data, dict):
        raise ValueError(f"Expected dict in snapshot file {path}, got {type(data).__name__}")
    return data


def load_snapshot_from_file(path: Path) -> dict[str, Any]:
    """Load and auto-expand a single snapshot file.

    Raises:
        FileNotFoundError: If path doesn't exist.
        ImportError: If TOON format without toons package.
    """
    return normalize_snapshot_view(_load_snapshot_file_raw(path))


def load_snapshot_from_file_raw(path: Path) -> dict[str, Any]:
    """Load and auto-expand a single snapshot file without normalization."""
    return _load_snapshot_file_raw(path)


def _validate_snapshot_path(path: Path, out_dir: str) -> None:
    """Ensure path is within the snapshot directory to prevent path traversal.

    Raises:
        ValueError: If path is outside the allowed directory.
    """
    try:
        resolved = path.resolve()
        allowed = Path(out_dir).resolve()
        resolved.relative_to(allowed)
    except ValueError:
        raise ValueError(f"Path {path} is outside snapshot directory {out_dir}") from None


def load_snapshot(path: str | None, out_dir: str, *, _trust_path: bool = False) -> dict[str, Any]:
    """Load from explicit path or fall back to latest.

    Args:
        path: Explicit path to snapshot file, or None for latest.
        out_dir: Snapshot directory.
        _trust_path: If False (default), validate that path is within out_dir.
            Set True only for trusted internal callers.

    Raises:
        FileNotFoundError: If no snapshots found.
        ValueError: If path is outside the snapshot directory.
    """
    if path is not None:
        p = Path(path)
        if not _trust_path:
            _validate_snapshot_path(p, out_dir)
        return load_snapshot_from_file(p)
    snapshot = get_latest_snapshot(out_dir)
    if snapshot is None:
        raise FileNotFoundError(f"No snapshots found in {out_dir}")
    return snapshot


def load_snapshot_raw(
    path: str | None, out_dir: str, *, _trust_path: bool = False
) -> dict[str, Any]:
    """Load a raw snapshot payload without DebugSession normalization."""
    if path is not None:
        p = Path(path)
        if not _trust_path:
            _validate_snapshot_path(p, out_dir)
        return load_snapshot_from_file_raw(p)
    snapshot = get_latest_snapshot(out_dir, normalize=False)
    if snapshot is None:
        raise FileNotFoundError(f"No snapshots found in {out_dir}")
    return snapshot


def find_all_snapshots(out_dir: str) -> list[dict[str, Any]]:
    """Scan output dir, return snapshot metadata sorted newest-first.

    Returns [] if dir doesn't exist. Silently skips corrupted files.
    """
    out_path = Path(out_dir)
    if not out_path.is_dir():
        return []

    files = sorted(
        (
            p
            for p in out_path.iterdir()
            if p.is_file()
            and p.suffix in {".json", ".toon"}
            and not p.name.startswith("latest")
            and not p.name.endswith(".tmp")
        ),
        key=lambda p: p.stat().st_mtime,
        reverse=True,
    )

    entries: list[dict[str, Any]] = []
    skipped = 0
    for p in files:
        try:
            data = load_snapshot_from_file(p)
            exc = data.get("exception", {})
            frames = data.get("frames") or [{}]
            fr0 = frames[0] if frames else {}
            entries.append(
                {
                    "path": p,
                    "mtime": p.stat().st_mtime,
                    "name": data.get("name", "?"),
                    "exc_type": exc.get("type", "?"),
                    "exc_message": (exc.get("message") or "")[:80],
                    "file": fr0.get("file_rel", fr0.get("file", "?")),
                    "line": fr0.get("line", "?"),
                }
            )
        except Exception:
            skipped += 1
            continue
    if skipped > 0:
        import sys

        sys.stderr.write(
            f"llmdebug: warning: skipped {skipped} corrupted snapshot(s) in {out_dir}\n"
        )
    return entries


def resolve_snapshot_ref(
    ref: str | None,
    out_dir: str,
    all_snapshots: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
    """Resolve a snapshot reference to a loaded snapshot dict.

    Handles:
    - None / "latest" → most recent snapshot
    - "previous" → second most recent
    - "#N" → Nth snapshot from list (1-based)
    - file path → load directly

    Raises:
        FileNotFoundError: If the referenced snapshot doesn't exist.
        ValueError: If the reference is out of range.
    """
    if ref is None or ref == "latest":
        return load_snapshot(None, out_dir)

    if ref == "previous":
        snapshots = all_snapshots if all_snapshots is not None else find_all_snapshots(out_dir)
        if len(snapshots) < 2:
            raise FileNotFoundError(
                f"Need at least 2 snapshots to use 'previous', "
                f"but only {len(snapshots)} found in {out_dir}"
            )
        return load_snapshot_from_file(snapshots[1]["path"])

    if ref.startswith("#"):
        try:
            n = int(ref[1:])
        except ValueError:
            raise ValueError(f"Invalid snapshot reference: {ref!r}") from None
        snapshots = all_snapshots if all_snapshots is not None else find_all_snapshots(out_dir)
        if n < 1 or n > len(snapshots):
            raise ValueError(
                f"Snapshot #{n} not found. Only {len(snapshots)} available in {out_dir}"
            )
        return load_snapshot_from_file(snapshots[n - 1]["path"])

    # Treat as file path
    return load_snapshot(ref, out_dir)
