"""In-memory service dependency graph for otelctx."""

from __future__ import annotations

import math
import threading
from dataclasses import dataclass, field
from datetime import UTC, datetime


@dataclass
class ServiceEdge:
    source: str
    target: str
    call_count: int = 0
    error_count: int = 0
    total_latency_ms: float = 0.0
    _latencies: list[float] = field(default_factory=list, repr=False)

    @property
    def error_rate(self) -> float:
        if self.call_count == 0:
            return 0.0
        return self.error_count / self.call_count

    @property
    def avg_latency_ms(self) -> float:
        if self.call_count == 0:
            return 0.0
        return self.total_latency_ms / self.call_count

    def to_dict(self) -> dict:
        return {
            "source": self.source,
            "target": self.target,
            "call_count": self.call_count,
            "error_count": self.error_count,
            "error_rate": round(self.error_rate, 4),
            "avg_latency_ms": round(self.avg_latency_ms, 3),
        }


@dataclass
class ServiceNode:
    name: str
    span_count: int = 0
    error_count: int = 0
    _latencies: list[float] = field(default_factory=list, repr=False)

    @property
    def health_score(self) -> float:
        """Health score 0-1. 1 = healthy, 0 = critical."""
        if self.span_count == 0:
            return 1.0
        error_rate = self.error_count / self.span_count
        return max(0.0, 1.0 - (error_rate * 2.0))

    @property
    def status(self) -> str:
        score = self.health_score
        if score >= 0.9:
            return "healthy"
        if score >= 0.5:
            return "degraded"
        return "critical"

    @property
    def p95_latency_ms(self) -> float:
        if not self._latencies:
            return 0.0
        sorted_latencies = sorted(self._latencies)
        idx = int(math.ceil(0.95 * len(sorted_latencies))) - 1
        return sorted_latencies[max(0, idx)]

    def to_dict(self) -> dict:
        return {
            "name": self.name,
            "span_count": self.span_count,
            "error_count": self.error_count,
            "health_score": round(self.health_score, 4),
            "status": self.status,
            "p95_latency_ms": round(self.p95_latency_ms, 3),
        }


class ServiceGraph:
    """In-memory service dependency graph, rebuilt from span data."""

    def __init__(self) -> None:
        self._lock = threading.RLock()
        self._nodes: dict[str, ServiceNode] = {}
        self._edges: dict[tuple[str, str], ServiceEdge] = {}
        self._last_rebuilt: datetime | None = None

    def rebuild(self, repository) -> None:
        """Rebuild from all spans in repository."""
        from otelctx.db.models import Span, Trace
        from sqlalchemy import select
        from common.db import session_scope

        nodes: dict[str, ServiceNode] = {}
        edges: dict[tuple[str, str], ServiceEdge] = {}
        span_by_id: dict[str, dict] = {}

        with session_scope(repository.session_factory) as session:
            spans = list(session.execute(
                select(Span, Trace.trace_id)
                .join(Trace, Span.trace_id == Trace.id)
                .order_by(Span.id)
            ))

            # Build node map
            for span, trace_id in spans:
                svc = span.service_name or "unknown"
                if svc not in nodes:
                    nodes[svc] = ServiceNode(name=svc)
                node = nodes[svc]
                node.span_count += 1
                if span.status_code == "ERROR":
                    node.error_count += 1
                if span.duration_ms is not None and span.duration_ms > 0:
                    node._latencies.append(span.duration_ms)

                span_by_id[span.span_id] = {
                    "service": svc,
                    "parent_span_id": span.parent_span_id,
                    "status_code": span.status_code,
                    "duration_ms": span.duration_ms or 0.0,
                }

            # Build edges from parent->child span service relationships
            for span_id, span_data in span_by_id.items():
                parent_id = span_data["parent_span_id"]
                if parent_id and parent_id in span_by_id:
                    parent_svc = span_by_id[parent_id]["service"]
                    child_svc = span_data["service"]
                    if parent_svc != child_svc:
                        edge_key = (parent_svc, child_svc)
                        if edge_key not in edges:
                            edges[edge_key] = ServiceEdge(source=parent_svc, target=child_svc)
                        edge = edges[edge_key]
                        edge.call_count += 1
                        edge.total_latency_ms += span_data["duration_ms"]
                        if span_data["status_code"] == "ERROR":
                            edge.error_count += 1

        with self._lock:
            self._nodes = nodes
            self._edges = edges
            self._last_rebuilt = datetime.now(UTC)

    def get_graph(self) -> dict:
        with self._lock:
            return {
                "nodes": [n.to_dict() for n in self._nodes.values()],
                "edges": [e.to_dict() for e in self._edges.values()],
                "last_rebuilt": self._last_rebuilt.isoformat() if self._last_rebuilt else None,
                "service_count": len(self._nodes),
                "edge_count": len(self._edges),
            }

    def get_service_health(self, service_name: str) -> dict | None:
        with self._lock:
            node = self._nodes.get(service_name)
            if not node:
                return None
            downstream_edges = [e.to_dict() for k, e in self._edges.items() if k[0] == service_name]
            upstream_edges = [e.to_dict() for k, e in self._edges.items() if k[1] == service_name]
            return {
                **node.to_dict(),
                "downstream": downstream_edges,
                "upstream": upstream_edges,
            }

    def start_auto_rebuild(self, repository, interval_seconds: float = 30.0) -> "_AutoRebuildThread":
        """Start a background thread that rebuilds the graph every *interval_seconds*."""
        t = _AutoRebuildThread(self, repository, interval_seconds)
        t.start()
        return t


class _AutoRebuildThread:
    """Background daemon thread that periodically rebuilds a ServiceGraph."""

    def __init__(self, graph: ServiceGraph, repository, interval_seconds: float) -> None:
        self._graph = graph
        self._repository = repository
        self._interval = interval_seconds
        self._stop_event = threading.Event()
        self._thread = threading.Thread(
            target=self._run, daemon=True, name="graph-auto-rebuild"
        )

    def start(self) -> None:
        self._thread.start()

    def stop(self, timeout: float = 5.0) -> None:
        self._stop_event.set()
        self._thread.join(timeout=timeout)

    def _run(self) -> None:
        while not self._stop_event.wait(timeout=self._interval):
            try:
                self._graph.rebuild(self._repository)
            except Exception:  # noqa: BLE001
                pass
