"""Storage base classes for trace backends.

TraceReader — read-only interface (used by LangfuseStore and CLI read paths).
StoreBase   — read + write (used by LocalStore, SQLiteStore, S3Store, PostgresStore).

Evaluations are handled by BanditoClient (bandito/api.py), not by storage backends.
"""

from __future__ import annotations

import re
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import Any, Iterator

from ..models import Trace
from .normalize import normalize_trace
from .summary import summarize_trace


def sanitize_project_name(project: str) -> str:
    """Sanitize project name to prevent path traversal.

    - Removes path traversal patterns (.., /, \\)
    - Removes control characters
    - Collapses multiple dashes
    - Returns 'unknown' if result is empty
    """
    sanitized = project.replace("..", "").replace("/", "-").replace("\\", "-")
    sanitized = "".join(c for c in sanitized if ord(c) >= 32)
    sanitized = re.sub(r'-+', '-', sanitized).strip("-")
    return sanitized or "unknown"


@dataclass
class TraceStats:
    total_traces: int = 0
    total_cost: float = 0.0
    avg_cost: float = 0.0
    avg_latency: float = 0.0
    avg_tokens: float = 0.0
    span_kinds: dict[str, int] = field(default_factory=dict)
    daily_counts: list[int] = field(default_factory=list)  # last 7 days


class TraceReader(ABC):
    """Read-only interface for trace storage backends.

    Use this type when code only needs to read traces (CLI commands,
    analysis surfaces, retrieval layer). LangfuseStore implements this
    directly since it cannot write traces.
    """

    @abstractmethod
    def _read_trace(self, trace_id: str) -> dict[str, Any] | None:
        """Load a single trace by ID. Returns None if not found.

        Backends implement this. Consumers call get_trace() which normalizes.
        """
        ...

    def get_trace(self, trace_id: str) -> dict[str, Any] | None:
        """Load a single trace by ID, normalized to canonical shape."""
        raw = self._read_trace(trace_id)
        if raw is None:
            return None
        return normalize_trace(raw)

    @abstractmethod
    def iter_traces(self, project: str | None = None) -> Iterator[dict[str, Any]]:
        """Iterate over traces, optionally filtered by project.

        Backends should implement native project filtering (path-based,
        SQL WHERE, etc.) for efficiency.
        """
        ...

    # --- Validation ---

    def validate(self) -> str | None:
        """Test that the storage backend is reachable and configured correctly.

        Returns None on success, or an error message string on failure.
        """
        return None  # Default: no validation (local always works)

    # --- Derived methods (implemented once, work for all backends) ---

    def list_projects(self) -> list[str]:
        projects = set()
        for trace in self.iter_traces():
            if "project" in trace:
                projects.add(trace["project"])
        return sorted(projects)

    def list_traces(
        self,
        project: str | None = None,
        tag: str | None = None,
        limit: int | None = None,
        offset: int = 0,
    ) -> list[dict[str, Any]]:
        traces = []
        for raw in self.iter_traces(project=project):
            trace = normalize_trace(raw)
            if tag is not None and tag not in trace.get("tags", []):
                continue
            traces.append(trace)
        traces.sort(key=lambda t: t.get("started_at", ""), reverse=True)
        if offset:
            traces = traces[offset:]
        if limit is not None and limit > 0:
            traces = traces[:limit]
        return traces

    def list_traces_filtered(
        self,
        project: str | None = None,
        tag: str | None = None,
        since: datetime | None = None,
        limit: int | None = None,
        offset: int = 0,
    ) -> list[dict[str, Any]]:
        traces = []
        for raw in self.iter_traces(project=project):
            trace = normalize_trace(raw)
            if tag is not None and tag not in trace.get("tags", []):
                continue
            if since is not None:
                started = trace.get("started_at", "")
                if started:
                    try:
                        ts = datetime.fromisoformat(started.replace("Z", "+00:00"))
                        if ts < since:
                            continue
                    except (ValueError, TypeError):
                        pass
            traces.append(trace)
        traces.sort(key=lambda t: t.get("started_at", ""), reverse=True)
        if offset:
            traces = traces[offset:]
        if limit is not None and limit > 0:
            traces = traces[:limit]
        return traces

    def list_trace_summaries(
        self,
        project: str | None = None,
        tag: str | None = None,
        since: datetime | None = None,
        limit: int | None = None,
        offset: int = 0,
    ) -> list[dict[str, Any]]:
        """List trace summaries — lightweight metadata, no span content.

        Default: derives summaries from full traces. Backends can override
        for performance (e.g., Langfuse returns summaries from trace.list()
        without fetching observations).
        """
        summaries = []
        for raw in self.iter_traces(project=project):
            trace = normalize_trace(raw)
            if tag is not None and tag not in trace.get("tags", []):
                continue
            if since is not None:
                started = trace.get("started_at", "")
                if started:
                    try:
                        ts = datetime.fromisoformat(started.replace("Z", "+00:00"))
                        if ts < since:
                            continue
                    except (ValueError, TypeError):
                        pass
            summaries.append(summarize_trace(trace))
        summaries.sort(key=lambda s: s.get("started_at", ""), reverse=True)
        if offset:
            summaries = summaries[offset:]
        if limit is not None and limit > 0:
            summaries = summaries[:limit]
        return summaries

    def count_traces(self, project: str | None = None) -> int:
        """Count traces efficiently without loading full data."""
        count = 0
        for _ in self.iter_traces(project=project):
            count += 1
        return count

    def get_stats(self, project: str) -> TraceStats:
        """Compute trace-only stats (cost, latency, tokens, span kinds)."""
        traces = self.list_traces(project)
        stats = TraceStats()
        stats.total_traces = len(traces)

        total_cost = 0.0
        total_latency = 0.0
        total_tokens = 0
        daily: dict[str, int] = {}

        for trace in traces:
            for span in trace.get("spans", []):
                kind = span.get("kind", "unknown")
                stats.span_kinds[kind] = stats.span_kinds.get(kind, 0) + 1
                if span.get("input_tokens"):
                    total_tokens += span["input_tokens"]
                if span.get("output_tokens"):
                    total_tokens += span["output_tokens"]
                if span.get("cost"):
                    total_cost += span["cost"]

            started = trace.get("started_at", "")
            ended = trace.get("ended_at", "")
            if started and ended:
                try:
                    s = datetime.fromisoformat(started.replace("Z", "+00:00"))
                    e = datetime.fromisoformat(ended.replace("Z", "+00:00"))
                    total_latency += (e - s).total_seconds()
                except (ValueError, TypeError):
                    pass

            if started:
                try:
                    dt = datetime.fromisoformat(started.replace("Z", "+00:00"))
                    day = dt.strftime("%Y-%m-%d")
                    daily[day] = daily.get(day, 0) + 1
                except (ValueError, TypeError):
                    pass

        if traces:
            stats.total_cost = total_cost
            stats.avg_cost = total_cost / len(traces) if total_cost else 0.0
            stats.avg_latency = total_latency / len(traces)
            stats.avg_tokens = total_tokens / len(traces) if total_tokens else 0.0

        today = datetime.now(timezone.utc).date()
        stats.daily_counts = [
            daily.get((today - timedelta(days=6 - i)).isoformat(), 0)
            for i in range(7)
        ]

        return stats


class StoreBase(TraceReader):
    """Read-write base for trace storage backends.

    Extends TraceReader with write capabilities. Use this for backends
    that can persist traces (Local, SQLite, S3, PostgreSQL).
    """

    @abstractmethod
    def write_trace(self, trace: Trace) -> None:
        """Persist a trace."""
        ...

    def write(self, obj: Trace) -> None:
        """Write a trace."""
        if isinstance(obj, Trace):
            self.write_trace(obj)
        else:
            raise TypeError(f"Expected Trace, got {type(obj).__name__}")
