"""Trace data models — the core schema for Litmus recordings."""

from __future__ import annotations

import uuid
from datetime import datetime, timezone
from enum import Enum
from typing import Any

from pydantic import BaseModel, Field


def _trace_id() -> str:
    return f"lt-{uuid.uuid4().hex[:12]}"


def _step_id() -> str:
    return f"s-{uuid.uuid4().hex[:8]}"


def _now() -> datetime:
    return datetime.now(timezone.utc)


class StepType(str, Enum):
    LLM_CALL = "llm_call"
    TOOL_CALL = "tool_call"


class LLMRequest(BaseModel):
    """Captured LLM API request."""

    provider: str  # "anthropic", "openai", etc.
    model: str | None = None
    method: str  # HTTP method
    path: str  # e.g. "/v1/messages"
    headers: dict[str, str] = Field(default_factory=dict)
    body: dict[str, Any] | None = None


class LLMResponse(BaseModel):
    """Captured LLM API response."""

    status_code: int
    headers: dict[str, str] = Field(default_factory=dict)
    body: dict[str, Any] | None = None
    # For streaming responses: list of raw SSE chunks
    stream_chunks: list[str] | None = None
    is_stream: bool = False


class LLMCall(BaseModel):
    """A single LLM API call (request + response)."""

    step_id: str = Field(default_factory=_step_id)
    step_type: StepType = StepType.LLM_CALL
    timestamp: datetime = Field(default_factory=_now)
    request: LLMRequest
    response: LLMResponse | None = None
    latency_ms: float = 0
    error: str | None = None


class ToolRequest(BaseModel):
    """Captured tool/API call request."""

    method: str
    url: str
    headers: dict[str, str] = Field(default_factory=dict)
    body: Any | None = None


class ToolResponse(BaseModel):
    """Captured tool/API call response."""

    status_code: int
    headers: dict[str, str] = Field(default_factory=dict)
    body: Any | None = None


class ToolCall(BaseModel):
    """A single tool/external API call."""

    step_id: str = Field(default_factory=_step_id)
    step_type: StepType = StepType.TOOL_CALL
    timestamp: datetime = Field(default_factory=_now)
    request: ToolRequest
    response: ToolResponse | None = None
    latency_ms: float = 0
    error: str | None = None


Step = LLMCall | ToolCall


def _collect_metadata() -> dict[str, str]:
    """Auto-detect environment info."""
    import platform
    import sys

    meta: dict[str, str] = {
        "python_version": sys.version.split()[0],
        "platform": platform.platform(),
        "arch": platform.machine(),
    }

    # Detect installed SDK versions
    for pkg in ("anthropic", "openai", "mistral", "cohere", "groq"):
        try:
            mod = __import__(pkg)
            ver = getattr(mod, "__version__", getattr(mod, "VERSION", None))
            if ver:
                meta[f"{pkg}_sdk"] = str(ver)
        except ImportError:
            pass

    return meta


class TraceMetadata(BaseModel):
    """Metadata about the trace environment — auto-populated on creation."""

    litmus_version: str = "0.1.0"
    python_version: str | None = None
    platform: str | None = None
    arch: str | None = None
    sdk_versions: dict[str, str] = Field(default_factory=dict)
    environment: str = "development"
    extra: dict[str, Any] = Field(default_factory=dict)

    @classmethod
    def auto(cls) -> TraceMetadata:
        """Create metadata with auto-detected environment info."""
        info = _collect_metadata()
        sdk_versions = {k: v for k, v in info.items() if k.endswith("_sdk")}
        return cls(
            python_version=info.get("python_version"),
            platform=info.get("platform"),
            arch=info.get("arch"),
            sdk_versions=sdk_versions,
        )


class TraceStatus(str, Enum):
    RECORDING = "recording"
    COMPLETED = "completed"
    FAILED = "failed"


class Trace(BaseModel):
    """A complete recorded agent execution."""

    trace_id: str = Field(default_factory=_trace_id)
    agent_id: str | None = None
    started_at: datetime = Field(default_factory=_now)
    completed_at: datetime | None = None
    status: TraceStatus = TraceStatus.RECORDING
    steps: list[LLMCall | ToolCall] = Field(default_factory=list)
    metadata: TraceMetadata = Field(default_factory=TraceMetadata)

    def add_step(self, step: LLMCall | ToolCall) -> None:
        self.steps.append(step)

    def complete(self) -> None:
        self.completed_at = _now()
        self.status = TraceStatus.COMPLETED

    def fail(self, error: str | None = None) -> None:
        self.completed_at = _now()
        self.status = TraceStatus.FAILED

    def duration_ms(self) -> float | None:
        if self.completed_at is None:
            return None
        return (self.completed_at - self.started_at).total_seconds() * 1000

    def llm_calls(self) -> list[LLMCall]:
        return [s for s in self.steps if isinstance(s, LLMCall)]

    def tool_calls(self) -> list[ToolCall]:
        return [s for s in self.steps if isinstance(s, ToolCall)]

    def save(self, path: str, compact: bool = False) -> None:
        """Save trace to a JSON file.

        Args:
            compact: Strip request bodies (keep model name), tool definitions,
                     and system prompts to reduce file size. Keeps only data
                     needed for replay.
        """
        from pathlib import Path

        if compact:
            trace = self._compacted()
            data = trace.model_dump_json(indent=2)
        else:
            data = self.model_dump_json(indent=2)

        p = Path(path)
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(data)

    def _compacted(self) -> Trace:
        """Return a copy with request bodies stripped for smaller files."""
        compact = self.model_copy(deep=True)
        for step in compact.steps:
            if isinstance(step, LLMCall) and step.request.body:
                body = step.request.body
                # Keep only model name and stream flag from request
                step.request.body = {
                    k: v for k, v in body.items()
                    if k in ("model", "stream")
                }
            # Strip headers from both request and response
            step.request.headers = {}
            if isinstance(step, LLMCall) and step.response:
                step.response.headers = {}
        return compact

    @classmethod
    def load(cls, path: str) -> Trace:
        """Load a trace from a JSON file.

        Raises:
            FileNotFoundError: If the file doesn't exist.
            ValueError: If the JSON is invalid or doesn't match the schema.
        """
        from pathlib import Path

        p = Path(path)
        if not p.exists():
            raise FileNotFoundError(f"Trace file not found: {path}")

        try:
            return cls.model_validate_json(p.read_text())
        except Exception as e:
            raise ValueError(f"Failed to load trace {path}: {e}") from e
