"""Native observability storage and exporter for Logos ADK."""
from __future__ import annotations

from collections import Counter, defaultdict, deque
from dataclasses import asdict, dataclass, field
from datetime import UTC, datetime, timedelta
import os
import json
from threading import RLock
from typing import Any, Protocol
from uuid import uuid4

from logos_adk.observe import Exporter, Metric, Span
from logos_adk.postgres_utils import import_psycopg, resolve_database_url


@dataclass
class LogEntry:
    """Structured log entry retained in the native observability store."""

    timestamp: str
    level: str
    message: str
    source: str
    agent_name: str | None = None
    session_id: str | None = None
    trace_id: str | None = None
    request_id: str | None = None
    attributes: dict[str, Any] = field(default_factory=dict)


@dataclass
class AlertRule:
    """Simple threshold rule evaluated against recent telemetry."""

    name: str
    severity: str
    metric_name: str
    threshold: float
    window_seconds: int = 300
    comparison: str = "gt"
    description: str | None = None


@dataclass
class AlertEvent:
    """Raised alert event."""

    id: str
    rule_name: str
    severity: str
    message: str
    triggered_at: str
    value: float
    attributes: dict[str, Any] = field(default_factory=dict)


@dataclass
class RetentionPolicy:
    """Retention policy for a single observability signal table."""

    ttl_days: int
    max_rows: int


def _default_alert_rules() -> list[AlertRule]:
    return [
        AlertRule(
            name="high_request_latency",
            severity="warning",
            metric_name="adk_request_latency_ms",
            threshold=2000.0,
            description="Average request latency is above 2 seconds",
        ),
        AlertRule(
            name="error_spike",
            severity="critical",
            metric_name="adk_error_total",
            threshold=3.0,
            description="Multiple errors were observed in the last 5 minutes",
        ),
        AlertRule(
            name="queue_backlog",
            severity="warning",
            metric_name="adk_queue_backlog",
            threshold=10.0,
            description="Task queue backlog is above the safe threshold",
        ),
        AlertRule(
            name="provider_timeout_spike",
            severity="critical",
            metric_name="adk_provider_timeout_total",
            threshold=2.0,
            description="Provider timeouts were observed in the last 5 minutes",
        ),
        AlertRule(
            name="circuit_breaker_opened",
            severity="critical",
            metric_name="adk_circuit_breaker_open_total",
            threshold=1.0,
            description="A provider circuit breaker opened recently",
        ),
        AlertRule(
            name="state_backend_failures",
            severity="critical",
            metric_name="adk_state_backend_failure_total",
            threshold=1.0,
            description="State backend failures were observed in the last 5 minutes",
        ),
        AlertRule(
            name="reindex_failure_spike",
            severity="critical",
            metric_name="adk_reindex_failures_total",
            threshold=0.5,
            description="A retrieval reindex request failed recently",
        ),
        AlertRule(
            name="high_reindex_latency",
            severity="warning",
            metric_name="adk_reindex_duration_ms",
            threshold=5000.0,
            description="Average reindex worker latency is above 5 seconds",
        ),
        AlertRule(
            name="continuous_eval_regression",
            severity="critical",
            metric_name="adk_continuous_eval_regression_total",
            threshold=0.5,
            description="A continuous eval job breached a regression threshold recently",
        ),
        AlertRule(
            name="self_learning_auto_rollback",
            severity="critical",
            metric_name="adk_self_learning_auto_rollback_total",
            threshold=0.5,
            description="A self-learning deployment was auto-rolled back recently",
        ),
        AlertRule(
            name="self_learning_experiment_disabled",
            severity="warning",
            metric_name="adk_self_learning_experiment_disabled_total",
            threshold=0.5,
            description="A self-learning prompt experiment was auto-disabled recently",
        ),
    ]


class ObservabilityStore(Protocol):
    rules: list[AlertRule]

    def reset(self) -> None: ...
    def record_span(self, span: Span) -> None: ...
    def record_metric(self, metric: Metric) -> None: ...
    def log(
        self,
        *,
        level: str,
        message: str,
        source: str,
        agent_name: str | None = None,
        session_id: str | None = None,
        trace_id: str | None = None,
        request_id: str | None = None,
        attributes: dict[str, Any] | None = None,
    ) -> None: ...
    def latest_spans(self, limit: int = 100) -> list[dict[str, Any]]: ...
    def latest_metrics(self, limit: int = 200) -> list[dict[str, Any]]: ...
    def latest_logs(self, limit: int = 200) -> list[dict[str, Any]]: ...
    def latest_alerts(self, limit: int = 100) -> list[dict[str, Any]]: ...
    def query_spans(
        self,
        *,
        limit: int = 100,
        trace_id: str | None = None,
        agent_name: str | None = None,
        status: str | None = None,
        attributes_filter: dict[str, str] | None = None,
        started_after: datetime | None = None,
        ended_before: datetime | None = None,
    ) -> list[dict[str, Any]]: ...
    def query_metrics(
        self,
        *,
        limit: int = 200,
        name: str | None = None,
        attributes_filter: dict[str, str] | None = None,
        recorded_after: datetime | None = None,
        recorded_before: datetime | None = None,
    ) -> list[dict[str, Any]]: ...
    def query_logs(
        self,
        *,
        limit: int = 200,
        level: str | None = None,
        trace_id: str | None = None,
        source: str | None = None,
        attributes_filter: dict[str, str] | None = None,
        logged_after: datetime | None = None,
        logged_before: datetime | None = None,
    ) -> list[dict[str, Any]]: ...
    def query_alerts(
        self,
        *,
        limit: int = 100,
        severity: str | None = None,
        rule_name: str | None = None,
        attributes_filter: dict[str, str] | None = None,
        triggered_after: datetime | None = None,
        triggered_before: datetime | None = None,
    ) -> list[dict[str, Any]]: ...
    def summary(self) -> dict[str, Any]: ...
    def prometheus_text(self) -> str: ...
    def grafana_dashboard(self) -> dict[str, Any]: ...


class _ObservabilityMixin:
    rules: list[AlertRule]

    def log(
        self,
        *,
        level: str,
        message: str,
        source: str,
        agent_name: str | None = None,
        session_id: str | None = None,
        trace_id: str | None = None,
        request_id: str | None = None,
        attributes: dict[str, Any] | None = None,
    ) -> None:
        raise NotImplementedError

    def _append_metric(self, metric: Metric) -> None:
        raise NotImplementedError

    def _append_alert(self, alert: AlertEvent) -> None:
        raise NotImplementedError

    def _recent_alert_exists(self, rule_name: str, now: datetime) -> bool:
        raise NotImplementedError

    def _metric_recorded_at(self, metric: Metric) -> datetime:
        raise NotImplementedError

    def _summary_spans(self) -> list[Span]:
        raise NotImplementedError

    def _summary_metrics(self) -> list[Metric]:
        raise NotImplementedError

    def _summary_logs(self) -> list[LogEntry]:
        raise NotImplementedError

    def _summary_alerts(self) -> list[AlertEvent]:
        raise NotImplementedError

    def _metrics_for_alerts(self) -> list[Metric]:
        raise NotImplementedError

    def _record_derived_telemetry_for_span(self, span: Span) -> None:
        if span.status == "error":
            exception_type = str(span.attributes.get("exception_type", ""))
            self.log(
                level="error",
                message=f"Span {span.name} failed",
                source="trace",
                agent_name=span.agent_name,
                session_id=span.session_id,
                trace_id=span.trace_id,
                attributes=span.attributes,
            )
            self._append_metric(
                Metric(
                    name="adk_error_total",
                    value=1.0,
                    unit="count",
                    attributes={
                        "agent_name": span.agent_name or "unknown",
                        "span_name": span.name,
                        "status": span.status,
                    },
                )
            )
            if exception_type == "ProviderTimeoutError":
                self._append_metric(
                    Metric(
                        name="adk_provider_timeout_total",
                        value=1.0,
                        unit="count",
                        attributes={
                            "agent_name": span.agent_name or "unknown",
                            "span_name": span.name,
                        },
                    )
                )
            if exception_type == "CircuitBreakerOpenError":
                self._append_metric(
                    Metric(
                        name="adk_circuit_breaker_open_total",
                        value=1.0,
                        unit="count",
                        attributes={
                            "agent_name": span.agent_name or "unknown",
                            "span_name": span.name,
                        },
                    )
                )
            if exception_type == "StateBackendError":
                self._append_metric(
                    Metric(
                        name="adk_state_backend_failure_total",
                        value=1.0,
                        unit="count",
                        attributes={
                            "agent_name": span.agent_name or "unknown",
                            "span_name": span.name,
                        },
                    )
                )
        if span.name in {"Agent.run", "Agent.stream"}:
            self._append_metric(
                Metric(
                    name="adk_request_latency_ms",
                    value=span.duration_ms,
                    unit="ms",
                    attributes={
                        "agent_name": span.agent_name or "unknown",
                        "status": span.status,
                        "mode": str(span.attributes.get("mode", "unknown")),
                    },
                )
            )
            self._append_metric(
                Metric(
                    name="adk_request_total",
                    value=1.0,
                    unit="count",
                    attributes={
                        "agent_name": span.agent_name or "unknown",
                        "status": span.status,
                    },
                )
            )

    def summary(self) -> dict[str, Any]:
        spans = self._summary_spans()
        metrics = self._summary_metrics()
        logs = self._summary_logs()
        alerts = self._summary_alerts()

        now = datetime.now(UTC)
        recent_spans = [
            span for span in spans if span.ended_at >= now - timedelta(minutes=15)
        ]
        recent_metrics = metrics[-300:]
        recent_logs = logs[-100:]
        agent_status_counts: dict[str, Counter[str]] = defaultdict(Counter)
        trace_waterfall: dict[str, list[dict[str, Any]]] = defaultdict(list)
        for span in recent_spans:
            agent_status_counts[span.agent_name or "unknown"][span.status] += 1
            trace_waterfall[span.trace_id].append(
                {
                    "name": span.name,
                    "span_id": span.span_id,
                    "parent_span_id": span.parent_span_id,
                    "duration_ms": round(span.duration_ms, 2),
                    "status": span.status,
                    "started_at": span.started_at.isoformat(),
                    "agent_name": span.agent_name,
                }
            )

        latency_values = [
            metric.value for metric in recent_metrics if metric.name == "adk_request_latency_ms"
        ]
        error_total = sum(
            metric.value for metric in recent_metrics if metric.name == "adk_error_total"
        )
        backlog = 0.0
        provider_timeouts = 0.0
        circuit_breaker_open_total = 0.0
        state_backend_failures = 0.0
        reindex_requests_total = 0.0
        reindex_failures_total = 0.0
        reindex_coalesced_total = 0.0
        reindex_latency_values: list[float] = []
        routing_win_rate_values: list[float] = []
        routing_budget_pressure = 0.0
        routing_canary_selected_total = 0.0
        routing_canary_outcome_delta = 0.0
        for metric in reversed(recent_metrics):
            if metric.name == "adk_queue_backlog":
                backlog = metric.value
                break
        for metric in reversed(recent_metrics):
            if metric.name == "adk_model_routing_budget_pressure":
                routing_budget_pressure = metric.value
                break
        for metric in reversed(recent_metrics):
            if metric.name == "adk_model_routing_canary_outcome_delta":
                routing_canary_outcome_delta = metric.value
                break
        provider_timeouts = sum(
            metric.value for metric in recent_metrics if metric.name == "adk_provider_timeout_total"
        )
        circuit_breaker_open_total = sum(
            metric.value
            for metric in recent_metrics
            if metric.name == "adk_circuit_breaker_open_total"
        )
        state_backend_failures = sum(
            metric.value
            for metric in recent_metrics
            if metric.name == "adk_state_backend_failure_total"
        )
        reindex_requests_total = sum(
            metric.value for metric in recent_metrics if metric.name == "adk_reindex_requests_total"
        )
        reindex_failures_total = sum(
            metric.value for metric in recent_metrics if metric.name == "adk_reindex_failures_total"
        )
        reindex_coalesced_total = sum(
            metric.value for metric in recent_metrics if metric.name == "adk_reindex_coalesced_total"
        )
        reindex_latency_values = [
            metric.value for metric in recent_metrics if metric.name == "adk_reindex_duration_ms"
        ]
        routing_win_rate_values = [
            metric.value for metric in recent_metrics if metric.name == "adk_model_routing_win_rate"
        ]
        routing_canary_selected_total = sum(
            metric.value
            for metric in recent_metrics
            if metric.name == "adk_model_routing_canary_selected_total"
        )

        requests_total = sum(
            metric.value for metric in recent_metrics if metric.name == "adk_request_total"
        )
        avg_latency = sum(latency_values) / len(latency_values) if latency_values else 0.0
        p95_latency = (
            sorted(latency_values)[max(0, int(len(latency_values) * 0.95) - 1)]
            if latency_values
            else 0.0
        )
        avg_reindex_latency = (
            sum(reindex_latency_values) / len(reindex_latency_values)
            if reindex_latency_values
            else 0.0
        )
        p95_reindex_latency = (
            sorted(reindex_latency_values)[max(0, int(len(reindex_latency_values) * 0.95) - 1)]
            if reindex_latency_values
            else 0.0
        )
        avg_routing_win_rate = (
            sum(routing_win_rate_values) / len(routing_win_rate_values)
            if routing_win_rate_values
            else 0.0
        )

        metrics_timeseries: dict[str, list[dict[str, Any]]] = defaultdict(list)
        for metric in recent_metrics:
            metrics_timeseries[metric.name].append(
                {
                    "value": metric.value,
                    "unit": metric.unit,
                    "attributes": metric.attributes,
                    "timestamp": metric.attributes.get("_recorded_at", datetime.now(UTC).isoformat()),
                }
            )

        return {
            "totals": {
                "spans": len(spans),
                "metrics": len(metrics),
                "logs": len(logs),
                "alerts": len(alerts),
                "requests_total": requests_total,
                "errors_total": error_total,
                "avg_latency_ms": round(avg_latency, 2),
                "p95_latency_ms": round(p95_latency, 2),
                "queue_backlog": backlog,
                "provider_timeouts_total": provider_timeouts,
                "circuit_breaker_open_total": circuit_breaker_open_total,
                "state_backend_failures_total": state_backend_failures,
                "reindex_requests_total": reindex_requests_total,
                "reindex_failures_total": reindex_failures_total,
                "reindex_coalesced_total": reindex_coalesced_total,
                "avg_reindex_latency_ms": round(avg_reindex_latency, 2),
                "p95_reindex_latency_ms": round(p95_reindex_latency, 2),
                "avg_model_routing_win_rate": round(avg_routing_win_rate, 4),
                "model_routing_budget_pressure": round(routing_budget_pressure, 6),
                "model_routing_canary_selected_total": routing_canary_selected_total,
                "model_routing_canary_outcome_delta": round(routing_canary_outcome_delta, 6),
            },
            "agents": {
                agent: {
                    "ok": counts.get("ok", 0),
                    "error": counts.get("error", 0),
                    "other": sum(v for k, v in counts.items() if k not in {"ok", "error"}),
                }
                for agent, counts in sorted(agent_status_counts.items())
            },
            "traces": [
                {"trace_id": trace_id, "spans": sorted(trace, key=lambda item: item["started_at"])}
                for trace_id, trace in list(trace_waterfall.items())[-20:]
            ],
            "metrics_timeseries": dict(metrics_timeseries),
            "recent_logs": [asdict(log) for log in recent_logs[::-1]],
            "recent_alerts": [asdict(alert) for alert in alerts][-20:][::-1],
            "rules": [asdict(rule) for rule in self.rules],
        }

    def prometheus_text(self) -> str:
        summary = self.summary()
        totals = summary["totals"]
        lines = [
            "# HELP logos_adk_requests_total Total completed requests",
            "# TYPE logos_adk_requests_total counter",
            f"logos_adk_requests_total {totals['requests_total']}",
            "# HELP logos_adk_errors_total Total observed errors",
            "# TYPE logos_adk_errors_total counter",
            f"logos_adk_errors_total {totals['errors_total']}",
            "# HELP logos_adk_request_latency_avg_ms Average request latency",
            "# TYPE logos_adk_request_latency_avg_ms gauge",
            f"logos_adk_request_latency_avg_ms {totals['avg_latency_ms']}",
            "# HELP logos_adk_request_latency_p95_ms P95 request latency",
            "# TYPE logos_adk_request_latency_p95_ms gauge",
            f"logos_adk_request_latency_p95_ms {totals['p95_latency_ms']}",
            "# HELP logos_adk_queue_backlog Current queued task backlog",
            "# TYPE logos_adk_queue_backlog gauge",
            f"logos_adk_queue_backlog {totals['queue_backlog']}",
            "# HELP logos_adk_provider_timeouts_total Total provider timeouts",
            "# TYPE logos_adk_provider_timeouts_total counter",
            f"logos_adk_provider_timeouts_total {totals['provider_timeouts_total']}",
            "# HELP logos_adk_circuit_breaker_open_total Total circuit breaker openings",
            "# TYPE logos_adk_circuit_breaker_open_total counter",
            f"logos_adk_circuit_breaker_open_total {totals['circuit_breaker_open_total']}",
            "# HELP logos_adk_state_backend_failures_total Total state backend failures",
            "# TYPE logos_adk_state_backend_failures_total counter",
            f"logos_adk_state_backend_failures_total {totals['state_backend_failures_total']}",
            "# HELP logos_adk_reindex_requests_total Total reindex requests",
            "# TYPE logos_adk_reindex_requests_total counter",
            f"logos_adk_reindex_requests_total {totals['reindex_requests_total']}",
            "# HELP logos_adk_reindex_failures_total Total failed reindex requests",
            "# TYPE logos_adk_reindex_failures_total counter",
            f"logos_adk_reindex_failures_total {totals['reindex_failures_total']}",
            "# HELP logos_adk_reindex_coalesced_total Total coalesced reindex requests",
            "# TYPE logos_adk_reindex_coalesced_total counter",
            f"logos_adk_reindex_coalesced_total {totals['reindex_coalesced_total']}",
            "# HELP logos_adk_reindex_duration_avg_ms Average reindex worker latency",
            "# TYPE logos_adk_reindex_duration_avg_ms gauge",
            f"logos_adk_reindex_duration_avg_ms {totals['avg_reindex_latency_ms']}",
            "# HELP logos_adk_reindex_duration_p95_ms P95 reindex worker latency",
            "# TYPE logos_adk_reindex_duration_p95_ms gauge",
            f"logos_adk_reindex_duration_p95_ms {totals['p95_reindex_latency_ms']}",
            "# HELP logos_adk_model_routing_win_rate Average model routing win rate",
            "# TYPE logos_adk_model_routing_win_rate gauge",
            f"logos_adk_model_routing_win_rate {totals['avg_model_routing_win_rate']}",
            "# HELP logos_adk_model_routing_budget_pressure Current routing budget pressure ratio",
            "# TYPE logos_adk_model_routing_budget_pressure gauge",
            f"logos_adk_model_routing_budget_pressure {totals['model_routing_budget_pressure']}",
            "# HELP logos_adk_model_routing_canary_selected_total Total canary selections",
            "# TYPE logos_adk_model_routing_canary_selected_total counter",
            f"logos_adk_model_routing_canary_selected_total {totals['model_routing_canary_selected_total']}",
            "# HELP logos_adk_model_routing_canary_outcome_delta Current canary quality delta",
            "# TYPE logos_adk_model_routing_canary_outcome_delta gauge",
            f"logos_adk_model_routing_canary_outcome_delta {totals['model_routing_canary_outcome_delta']}",
        ]
        for agent, counts in summary["agents"].items():
            lines.append(f'logos_adk_agent_spans_total{{agent="{agent}",status="ok"}} {counts["ok"]}')
            lines.append(f'logos_adk_agent_spans_total{{agent="{agent}",status="error"}} {counts["error"]}')
        return "\n".join(lines) + "\n"

    def grafana_dashboard(self) -> dict[str, Any]:
        return {
            "title": "Logos ADK Observability",
            "panels": [
                {"type": "stat", "title": "Requests Total", "target": "logos_adk_requests_total"},
                {"type": "stat", "title": "Errors Total", "target": "logos_adk_errors_total"},
                {"type": "timeseries", "title": "Latency P95", "target": "logos_adk_request_latency_p95_ms"},
                {"type": "timeseries", "title": "Queue Backlog", "target": "logos_adk_queue_backlog"},
                {
                    "type": "timeseries",
                    "title": "Provider Timeouts",
                    "target": "logos_adk_provider_timeouts_total",
                },
                {
                    "type": "timeseries",
                    "title": "Circuit Breaker Opens",
                    "target": "logos_adk_circuit_breaker_open_total",
                },
                {
                    "type": "timeseries",
                    "title": "State Backend Failures",
                    "target": "logos_adk_state_backend_failures_total",
                },
                {
                    "type": "timeseries",
                    "title": "Reindex Failures",
                    "target": "logos_adk_reindex_failures_total",
                },
                {
                    "type": "timeseries",
                    "title": "Reindex Coalesced Requests",
                    "target": "logos_adk_reindex_coalesced_total",
                },
                {
                    "type": "timeseries",
                    "title": "Reindex Worker Latency P95",
                    "target": "logos_adk_reindex_duration_p95_ms",
                },
                {
                    "type": "timeseries",
                    "title": "Model Routing Win Rate",
                    "target": "logos_adk_model_routing_win_rate",
                },
                {
                    "type": "timeseries",
                    "title": "Model Routing Budget Pressure",
                    "target": "logos_adk_model_routing_budget_pressure",
                },
                {
                    "type": "timeseries",
                    "title": "Canary Outcome Delta",
                    "target": "logos_adk_model_routing_canary_outcome_delta",
                },
            ],
            "datasource": {"type": "prometheus", "uid": "logos-adk"},
        }

    def _evaluate_alerts(self) -> None:
        now = datetime.now(UTC)
        metrics = self._metrics_for_alerts()
        for rule in self.rules:
            recent_values = [
                metric.value
                for metric in metrics
                if metric.name == rule.metric_name
                and self._metric_recorded_at(metric) >= now - timedelta(seconds=rule.window_seconds)
            ]
            if not recent_values:
                continue
            if rule.metric_name == "adk_request_latency_ms":
                observed_value = sum(recent_values) / len(recent_values)
            elif rule.metric_name == "adk_error_total":
                observed_value = sum(recent_values)
            elif rule.metric_name == "adk_reindex_failures_total":
                observed_value = sum(recent_values)
            elif rule.metric_name == "adk_reindex_duration_ms":
                observed_value = sum(recent_values) / len(recent_values)
            else:
                observed_value = recent_values[-1]
            triggered = observed_value > rule.threshold if rule.comparison == "gt" else observed_value < rule.threshold
            if not triggered or self._recent_alert_exists(rule.name, now):
                continue
            self._append_alert(
                AlertEvent(
                    id=str(uuid4()),
                    rule_name=rule.name,
                    severity=rule.severity,
                    message=rule.description or rule.name,
                    triggered_at=now.isoformat(),
                    value=observed_value,
                    attributes={
                        "metric_name": rule.metric_name,
                        "window_seconds": rule.window_seconds,
                    },
                )
            )


class NativeObservabilityStore(_ObservabilityMixin):
    """In-memory retention and query layer for local observability."""

    def __init__(
        self,
        *,
        max_spans: int = 2000,
        max_metrics: int = 4000,
        max_logs: int = 4000,
        max_alerts: int = 500,
    ) -> None:
        self.max_spans = max_spans
        self.max_metrics = max_metrics
        self.max_logs = max_logs
        self.max_alerts = max_alerts
        self._lock = RLock()
        self.spans: deque[Span] = deque(maxlen=max_spans)
        self.metrics: deque[Metric] = deque(maxlen=max_metrics)
        self.logs: deque[LogEntry] = deque(maxlen=max_logs)
        self.alerts: deque[AlertEvent] = deque(maxlen=max_alerts)
        self.rules = _default_alert_rules()
        self._last_alert_by_rule: dict[str, datetime] = {}

    def reset(self) -> None:
        with self._lock:
            self.spans.clear()
            self.metrics.clear()
            self.logs.clear()
            self.alerts.clear()
            self._last_alert_by_rule.clear()

    def record_span(self, span: Span) -> None:
        with self._lock:
            self.spans.append(span)
        self._record_derived_telemetry_for_span(span)
        self._evaluate_alerts()

    def record_metric(self, metric: Metric) -> None:
        self._append_metric(metric)
        self._evaluate_alerts()

    def log(
        self,
        *,
        level: str,
        message: str,
        source: str,
        agent_name: str | None = None,
        session_id: str | None = None,
        trace_id: str | None = None,
        request_id: str | None = None,
        attributes: dict[str, Any] | None = None,
    ) -> None:
        with self._lock:
            self.logs.append(
                LogEntry(
                    timestamp=datetime.now(UTC).isoformat(),
                    level=level,
                    message=message,
                    source=source,
                    agent_name=agent_name,
                    session_id=session_id,
                    trace_id=trace_id,
                    request_id=request_id,
                    attributes=attributes or {},
                )
            )

    def latest_spans(self, limit: int = 100) -> list[dict[str, Any]]:
        with self._lock:
            spans = list(self.spans)[-limit:]
        return [asdict(span) for span in reversed(spans)]

    def latest_metrics(self, limit: int = 200) -> list[dict[str, Any]]:
        with self._lock:
            metrics = list(self.metrics)[-limit:]
        return [asdict(metric) for metric in reversed(metrics)]

    def latest_logs(self, limit: int = 200) -> list[dict[str, Any]]:
        with self._lock:
            logs = list(self.logs)[-limit:]
        return [asdict(log) for log in reversed(logs)]

    def latest_alerts(self, limit: int = 100) -> list[dict[str, Any]]:
        with self._lock:
            alerts = list(self.alerts)[-limit:]
        return [asdict(alert) for alert in reversed(alerts)]

    def query_spans(
        self,
        *,
        limit: int = 100,
        trace_id: str | None = None,
        agent_name: str | None = None,
        status: str | None = None,
        attributes_filter: dict[str, str] | None = None,
        started_after: datetime | None = None,
        ended_before: datetime | None = None,
    ) -> list[dict[str, Any]]:
        spans = self._summary_spans()
        if trace_id is not None:
            spans = [span for span in spans if span.trace_id == trace_id]
        if agent_name is not None:
            spans = [span for span in spans if span.agent_name == agent_name]
        if status is not None:
            spans = [span for span in spans if span.status == status]
        if started_after is not None:
            spans = [span for span in spans if span.started_at >= started_after]
        if ended_before is not None:
            spans = [span for span in spans if span.ended_at <= ended_before]
        if attributes_filter:
            spans = [
                span for span in spans
                if all(str(span.attributes.get(key)) == value for key, value in attributes_filter.items())
            ]
        return [asdict(span) for span in reversed(spans[-limit:])]

    def query_metrics(
        self,
        *,
        limit: int = 200,
        name: str | None = None,
        attributes_filter: dict[str, str] | None = None,
        recorded_after: datetime | None = None,
        recorded_before: datetime | None = None,
    ) -> list[dict[str, Any]]:
        metrics = self._summary_metrics()
        if name is not None:
            metrics = [metric for metric in metrics if metric.name == name]
        if recorded_after is not None:
            metrics = [metric for metric in metrics if self._metric_recorded_at(metric) >= recorded_after]
        if recorded_before is not None:
            metrics = [metric for metric in metrics if self._metric_recorded_at(metric) <= recorded_before]
        if attributes_filter:
            metrics = [
                metric for metric in metrics
                if all(str(metric.attributes.get(key)) == value for key, value in attributes_filter.items())
            ]
        return [asdict(metric) for metric in reversed(metrics[-limit:])]

    def query_logs(
        self,
        *,
        limit: int = 200,
        level: str | None = None,
        trace_id: str | None = None,
        source: str | None = None,
        attributes_filter: dict[str, str] | None = None,
        logged_after: datetime | None = None,
        logged_before: datetime | None = None,
    ) -> list[dict[str, Any]]:
        logs = self._summary_logs()
        if level is not None:
            logs = [log for log in logs if log.level == level]
        if trace_id is not None:
            logs = [log for log in logs if log.trace_id == trace_id]
        if source is not None:
            logs = [log for log in logs if log.source == source]
        if logged_after is not None:
            logs = [log for log in logs if datetime.fromisoformat(log.timestamp) >= logged_after]
        if logged_before is not None:
            logs = [log for log in logs if datetime.fromisoformat(log.timestamp) <= logged_before]
        if attributes_filter:
            logs = [
                log for log in logs
                if all(str(log.attributes.get(key)) == value for key, value in attributes_filter.items())
            ]
        return [asdict(log) for log in reversed(logs[-limit:])]

    def query_alerts(
        self,
        *,
        limit: int = 100,
        severity: str | None = None,
        rule_name: str | None = None,
        attributes_filter: dict[str, str] | None = None,
        triggered_after: datetime | None = None,
        triggered_before: datetime | None = None,
    ) -> list[dict[str, Any]]:
        alerts = self._summary_alerts()
        if severity is not None:
            alerts = [alert for alert in alerts if alert.severity == severity]
        if rule_name is not None:
            alerts = [alert for alert in alerts if alert.rule_name == rule_name]
        if triggered_after is not None:
            alerts = [alert for alert in alerts if datetime.fromisoformat(alert.triggered_at) >= triggered_after]
        if triggered_before is not None:
            alerts = [alert for alert in alerts if datetime.fromisoformat(alert.triggered_at) <= triggered_before]
        if attributes_filter:
            alerts = [
                alert for alert in alerts
                if all(str(alert.attributes.get(key)) == value for key, value in attributes_filter.items())
            ]
        return [asdict(alert) for alert in reversed(alerts[-limit:])]

    def _append_metric(self, metric: Metric) -> None:
        if "_recorded_at" not in metric.attributes:
            metric.attributes["_recorded_at"] = datetime.now(UTC).isoformat()
        with self._lock:
            self.metrics.append(metric)

    def _append_alert(self, alert: AlertEvent) -> None:
        with self._lock:
            self._last_alert_by_rule[alert.rule_name] = datetime.fromisoformat(alert.triggered_at)
            self.alerts.append(alert)

    def _recent_alert_exists(self, rule_name: str, now: datetime) -> bool:
        last = self._last_alert_by_rule.get(rule_name)
        return bool(last and (now - last).total_seconds() < 60)

    def _metric_recorded_at(self, metric: Metric) -> datetime:
        raw = metric.attributes.get("_recorded_at")
        if isinstance(raw, str):
            return datetime.fromisoformat(raw)
        return datetime.now(UTC)

    def _summary_spans(self) -> list[Span]:
        with self._lock:
            return list(self.spans)

    def _summary_metrics(self) -> list[Metric]:
        with self._lock:
            return list(self.metrics)

    def _summary_logs(self) -> list[LogEntry]:
        with self._lock:
            return list(self.logs)

    def _summary_alerts(self) -> list[AlertEvent]:
        with self._lock:
            return list(self.alerts)

    def _metrics_for_alerts(self) -> list[Metric]:
        return self._summary_metrics()


class PostgreSQLObservabilityStore(_ObservabilityMixin):
    """PostgreSQL-backed observability persistence."""

    def __init__(
        self,
        dsn: str,
        *,
        schema: str = "public",
        table_prefix: str = "adk_observability",
        retention: dict[str, RetentionPolicy] | None = None,
        cleanup_interval_seconds: int = 300,
    ) -> None:
        self.dsn = dsn
        self.schema = schema
        self.table_prefix = table_prefix
        self.retention = retention or _default_retention_policies()
        self.cleanup_interval_seconds = cleanup_interval_seconds
        self.rules = _default_alert_rules()
        self._last_cleanup_at: datetime | None = None
        self._ensure_schema()

    def _connect(self) -> Any:
        psycopg = import_psycopg()
        return psycopg.connect(self.dsn)

    def _full_table(self, suffix: str) -> str:
        return f"{self.schema}.{self.table_prefix}_{suffix}"

    def _ensure_schema(self) -> None:
        with self._connect() as connection:
            with connection.cursor() as cursor:
                cursor.execute(f"CREATE SCHEMA IF NOT EXISTS {self.schema}")
                cursor.execute(
                    f"""
                    CREATE TABLE IF NOT EXISTS {self._full_table('spans')} (
                        trace_id TEXT NOT NULL,
                        span_id TEXT PRIMARY KEY,
                        parent_span_id TEXT,
                        name TEXT NOT NULL,
                        started_at TIMESTAMPTZ NOT NULL,
                        ended_at TIMESTAMPTZ NOT NULL,
                        duration_ms DOUBLE PRECISION NOT NULL,
                        agent_name TEXT,
                        session_id TEXT,
                        status TEXT NOT NULL,
                        attributes JSONB NOT NULL DEFAULT '{{}}'::jsonb
                    )
                    """
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_spans_ended_at "
                    f"ON {self._full_table('spans')} (ended_at DESC)"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_spans_trace_id_started_at "
                    f"ON {self._full_table('spans')} (trace_id, started_at ASC)"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_spans_agent_status_ended_at "
                    f"ON {self._full_table('spans')} (agent_name, status, ended_at DESC)"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_spans_attributes_gin "
                    f"ON {self._full_table('spans')} USING GIN (attributes)"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_spans_tenant_id "
                    f"ON {self._full_table('spans')} ((attributes->>'tenant_id'))"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_spans_tool_name "
                    f"ON {self._full_table('spans')} ((attributes->>'tool_name'))"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_spans_error_partial "
                    f"ON {self._full_table('spans')} (ended_at DESC) WHERE status = 'error'"
                )
                cursor.execute(
                    f"""
                    CREATE TABLE IF NOT EXISTS {self._full_table('metrics')} (
                        id BIGSERIAL PRIMARY KEY,
                        name TEXT NOT NULL,
                        value DOUBLE PRECISION NOT NULL,
                        unit TEXT,
                        attributes JSONB NOT NULL DEFAULT '{{}}'::jsonb,
                        recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
                    )
                    """
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_metrics_recorded_at "
                    f"ON {self._full_table('metrics')} (recorded_at DESC)"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_metrics_name_recorded_at "
                    f"ON {self._full_table('metrics')} (name, recorded_at DESC)"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_metrics_attributes_gin "
                    f"ON {self._full_table('metrics')} USING GIN (attributes)"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_metrics_tenant_id "
                    f"ON {self._full_table('metrics')} ((attributes->>'tenant_id'))"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_metrics_tool_name "
                    f"ON {self._full_table('metrics')} ((attributes->>'tool_name'))"
                )
                cursor.execute(
                    f"""
                    CREATE TABLE IF NOT EXISTS {self._full_table('logs')} (
                        id BIGSERIAL PRIMARY KEY,
                        timestamp TIMESTAMPTZ NOT NULL,
                        level TEXT NOT NULL,
                        message TEXT NOT NULL,
                        source TEXT NOT NULL,
                        agent_name TEXT,
                        session_id TEXT,
                        trace_id TEXT,
                        request_id TEXT,
                        attributes JSONB NOT NULL DEFAULT '{{}}'::jsonb
                    )
                    """
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_logs_timestamp "
                    f"ON {self._full_table('logs')} (timestamp DESC)"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_logs_level_timestamp "
                    f"ON {self._full_table('logs')} (level, timestamp DESC)"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_logs_trace_id_timestamp "
                    f"ON {self._full_table('logs')} (trace_id, timestamp DESC)"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_logs_attributes_gin "
                    f"ON {self._full_table('logs')} USING GIN (attributes)"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_logs_tenant_id "
                    f"ON {self._full_table('logs')} ((attributes->>'tenant_id'))"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_logs_tool_name "
                    f"ON {self._full_table('logs')} ((attributes->>'tool_name'))"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_logs_error_partial "
                    f"ON {self._full_table('logs')} (timestamp DESC) WHERE level = 'error'"
                )
                cursor.execute(
                    f"""
                    CREATE TABLE IF NOT EXISTS {self._full_table('alerts')} (
                        id TEXT PRIMARY KEY,
                        rule_name TEXT NOT NULL,
                        severity TEXT NOT NULL,
                        message TEXT NOT NULL,
                        triggered_at TIMESTAMPTZ NOT NULL,
                        value DOUBLE PRECISION NOT NULL,
                        attributes JSONB NOT NULL DEFAULT '{{}}'::jsonb
                    )
                    """
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_alerts_triggered_at "
                    f"ON {self._full_table('alerts')} (triggered_at DESC)"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_alerts_rule_triggered_at "
                    f"ON {self._full_table('alerts')} (rule_name, triggered_at DESC)"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_alerts_attributes_gin "
                    f"ON {self._full_table('alerts')} USING GIN (attributes)"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_alerts_tenant_id "
                    f"ON {self._full_table('alerts')} ((attributes->>'tenant_id'))"
                )
                cursor.execute(
                    f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_alerts_tool_name "
                    f"ON {self._full_table('alerts')} ((attributes->>'tool_name'))"
                )
            connection.commit()

    def reset(self) -> None:
        with self._connect() as connection:
            with connection.cursor() as cursor:
                for suffix in ("spans", "metrics", "logs", "alerts"):
                    cursor.execute(f"TRUNCATE TABLE {self._full_table(suffix)}")
            connection.commit()
        self._last_cleanup_at = None

    def record_span(self, span: Span) -> None:
        with self._connect() as connection:
            with connection.cursor() as cursor:
                cursor.execute(
                    f"""
                    INSERT INTO {self._full_table('spans')}
                    (trace_id, span_id, parent_span_id, name, started_at, ended_at, duration_ms,
                     agent_name, session_id, status, attributes)
                    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb)
                    ON CONFLICT (span_id) DO UPDATE SET
                        trace_id = EXCLUDED.trace_id,
                        parent_span_id = EXCLUDED.parent_span_id,
                        name = EXCLUDED.name,
                        started_at = EXCLUDED.started_at,
                        ended_at = EXCLUDED.ended_at,
                        duration_ms = EXCLUDED.duration_ms,
                        agent_name = EXCLUDED.agent_name,
                        session_id = EXCLUDED.session_id,
                        status = EXCLUDED.status,
                        attributes = EXCLUDED.attributes
                    """,
                    (
                        span.trace_id,
                        span.span_id,
                        span.parent_span_id,
                        span.name,
                        span.started_at,
                        span.ended_at,
                        span.duration_ms,
                        span.agent_name,
                        span.session_id,
                        span.status,
                        json.dumps(span.attributes),
                    ),
                )
            connection.commit()
        self._maybe_cleanup()
        self._record_derived_telemetry_for_span(span)
        self._evaluate_alerts()

    def record_metric(self, metric: Metric) -> None:
        self._append_metric(metric)
        self._evaluate_alerts()

    def log(
        self,
        *,
        level: str,
        message: str,
        source: str,
        agent_name: str | None = None,
        session_id: str | None = None,
        trace_id: str | None = None,
        request_id: str | None = None,
        attributes: dict[str, Any] | None = None,
    ) -> None:
        with self._connect() as connection:
            with connection.cursor() as cursor:
                cursor.execute(
                    f"""
                    INSERT INTO {self._full_table('logs')}
                    (timestamp, level, message, source, agent_name, session_id, trace_id, request_id, attributes)
                    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb)
                    """,
                    (
                        datetime.now(UTC),
                        level,
                        message,
                        source,
                        agent_name,
                        session_id,
                        trace_id,
                        request_id,
                        json.dumps(attributes or {}),
                    ),
                )
            connection.commit()
        self._maybe_cleanup()

    def latest_spans(self, limit: int = 100) -> list[dict[str, Any]]:
        return [asdict(span) for span in reversed(self._load_spans(limit=limit))]

    def latest_metrics(self, limit: int = 200) -> list[dict[str, Any]]:
        return [asdict(metric) for metric in reversed(self._load_metrics(limit=limit))]

    def latest_logs(self, limit: int = 200) -> list[dict[str, Any]]:
        return [asdict(log) for log in reversed(self._load_logs(limit=limit))]

    def latest_alerts(self, limit: int = 100) -> list[dict[str, Any]]:
        return [asdict(alert) for alert in reversed(self._load_alerts(limit=limit))]

    def query_spans(
        self,
        *,
        limit: int = 100,
        trace_id: str | None = None,
        agent_name: str | None = None,
        status: str | None = None,
        attributes_filter: dict[str, str] | None = None,
        started_after: datetime | None = None,
        ended_before: datetime | None = None,
    ) -> list[dict[str, Any]]:
        rows = self._query_rows(
            table=self._full_table("spans"),
            select_columns=(
                "trace_id, span_id, parent_span_id, name, started_at, ended_at, duration_ms, "
                "agent_name, session_id, status, attributes"
            ),
            order_by="ended_at DESC",
            limit=limit,
            scalar_filters={
                "trace_id": trace_id,
                "agent_name": agent_name,
                "status": status,
            },
            time_filters=(
                ("started_at >= %s", started_after),
                ("ended_at <= %s", ended_before),
            ),
            attributes_filter=attributes_filter,
        )
        return [
            asdict(
                Span(
                    trace_id=row[0],
                    span_id=row[1],
                    parent_span_id=row[2],
                    name=row[3],
                    started_at=row[4],
                    ended_at=row[5],
                    duration_ms=row[6],
                    agent_name=row[7],
                    session_id=row[8],
                    status=row[9],
                    attributes=row[10] if isinstance(row[10], dict) else json.loads(row[10] or "{}"),
                )
            )
            for row in rows
        ]

    def query_metrics(
        self,
        *,
        limit: int = 200,
        name: str | None = None,
        attributes_filter: dict[str, str] | None = None,
        recorded_after: datetime | None = None,
        recorded_before: datetime | None = None,
    ) -> list[dict[str, Any]]:
        rows = self._query_rows(
            table=self._full_table("metrics"),
            select_columns="name, value, unit, attributes, recorded_at",
            order_by="recorded_at DESC",
            limit=limit,
            scalar_filters={"name": name},
            time_filters=(
                ("recorded_at >= %s", recorded_after),
                ("recorded_at <= %s", recorded_before),
            ),
            attributes_filter=attributes_filter,
        )
        return [
            asdict(
                Metric(
                    name=row[0],
                    value=row[1],
                    unit=row[2],
                    attributes=(
                        row[3] if isinstance(row[3], dict) else json.loads(row[3] or "{}")
                    ) | {"_recorded_at": row[4].isoformat()},
                )
            )
            for row in rows
        ]

    def query_logs(
        self,
        *,
        limit: int = 200,
        level: str | None = None,
        trace_id: str | None = None,
        source: str | None = None,
        attributes_filter: dict[str, str] | None = None,
        logged_after: datetime | None = None,
        logged_before: datetime | None = None,
    ) -> list[dict[str, Any]]:
        rows = self._query_rows(
            table=self._full_table("logs"),
            select_columns=(
                "timestamp, level, message, source, agent_name, session_id, trace_id, request_id, attributes"
            ),
            order_by="timestamp DESC",
            limit=limit,
            scalar_filters={
                "level": level,
                "trace_id": trace_id,
                "source": source,
            },
            time_filters=(
                ("timestamp >= %s", logged_after),
                ("timestamp <= %s", logged_before),
            ),
            attributes_filter=attributes_filter,
        )
        return [
            asdict(
                LogEntry(
                    timestamp=row[0].isoformat(),
                    level=row[1],
                    message=row[2],
                    source=row[3],
                    agent_name=row[4],
                    session_id=row[5],
                    trace_id=row[6],
                    request_id=row[7],
                    attributes=row[8] if isinstance(row[8], dict) else json.loads(row[8] or "{}"),
                )
            )
            for row in rows
        ]

    def query_alerts(
        self,
        *,
        limit: int = 100,
        severity: str | None = None,
        rule_name: str | None = None,
        attributes_filter: dict[str, str] | None = None,
        triggered_after: datetime | None = None,
        triggered_before: datetime | None = None,
    ) -> list[dict[str, Any]]:
        rows = self._query_rows(
            table=self._full_table("alerts"),
            select_columns="id, rule_name, severity, message, triggered_at, value, attributes",
            order_by="triggered_at DESC",
            limit=limit,
            scalar_filters={
                "severity": severity,
                "rule_name": rule_name,
            },
            time_filters=(
                ("triggered_at >= %s", triggered_after),
                ("triggered_at <= %s", triggered_before),
            ),
            attributes_filter=attributes_filter,
        )
        return [
            asdict(
                AlertEvent(
                    id=row[0],
                    rule_name=row[1],
                    severity=row[2],
                    message=row[3],
                    triggered_at=row[4].isoformat(),
                    value=row[5],
                    attributes=row[6] if isinstance(row[6], dict) else json.loads(row[6] or "{}"),
                )
            )
            for row in rows
        ]

    def _append_metric(self, metric: Metric) -> None:
        attributes = dict(metric.attributes)
        attributes.setdefault("_recorded_at", datetime.now(UTC).isoformat())
        with self._connect() as connection:
            with connection.cursor() as cursor:
                cursor.execute(
                    f"""
                    INSERT INTO {self._full_table('metrics')}
                    (name, value, unit, attributes, recorded_at)
                    VALUES (%s, %s, %s, %s::jsonb, %s)
                    """,
                    (
                        metric.name,
                        metric.value,
                        metric.unit,
                        json.dumps(attributes),
                        datetime.fromisoformat(attributes["_recorded_at"]),
                    ),
                )
            connection.commit()
        self._maybe_cleanup()

    def _append_alert(self, alert: AlertEvent) -> None:
        with self._connect() as connection:
            with connection.cursor() as cursor:
                cursor.execute(
                    f"""
                    INSERT INTO {self._full_table('alerts')}
                    (id, rule_name, severity, message, triggered_at, value, attributes)
                    VALUES (%s, %s, %s, %s, %s, %s, %s::jsonb)
                    ON CONFLICT (id) DO NOTHING
                    """,
                    (
                        alert.id,
                        alert.rule_name,
                        alert.severity,
                        alert.message,
                        datetime.fromisoformat(alert.triggered_at),
                        alert.value,
                        json.dumps(alert.attributes),
                    ),
                )
            connection.commit()
        self._maybe_cleanup()

    def cleanup(self) -> None:
        """Apply retention policies to all observability tables."""
        now = datetime.now(UTC)
        with self._connect() as connection:
            with connection.cursor() as cursor:
                self._cleanup_signal(
                    cursor,
                    table=self._full_table("spans"),
                    timestamp_column="ended_at",
                    primary_key="span_id",
                    policy=self.retention["spans"],
                )
                self._cleanup_signal(
                    cursor,
                    table=self._full_table("metrics"),
                    timestamp_column="recorded_at",
                    primary_key="id",
                    policy=self.retention["metrics"],
                )
                self._cleanup_signal(
                    cursor,
                    table=self._full_table("logs"),
                    timestamp_column="timestamp",
                    primary_key="id",
                    policy=self.retention["logs"],
                )
                self._cleanup_signal(
                    cursor,
                    table=self._full_table("alerts"),
                    timestamp_column="triggered_at",
                    primary_key="id",
                    policy=self.retention["alerts"],
                )
            connection.commit()
        self._last_cleanup_at = now

    def _maybe_cleanup(self) -> None:
        now = datetime.now(UTC)
        if self.cleanup_interval_seconds <= 0:
            self.cleanup()
            return
        if self._last_cleanup_at is None:
            self.cleanup()
            return
        if (now - self._last_cleanup_at).total_seconds() >= self.cleanup_interval_seconds:
            self.cleanup()

    def _cleanup_signal(
        self,
        cursor: Any,
        *,
        table: str,
        timestamp_column: str,
        primary_key: str,
        policy: RetentionPolicy,
    ) -> None:
        cutoff = datetime.now(UTC) - timedelta(days=policy.ttl_days)
        cursor.execute(
            f"DELETE FROM {table} WHERE {timestamp_column} < %s",
            (cutoff,),
        )
        cursor.execute(
            f"""
            DELETE FROM {table}
            WHERE {primary_key} IN (
                SELECT {primary_key}
                FROM {table}
                ORDER BY {timestamp_column} DESC
                OFFSET %s
            )
            """,
            (policy.max_rows,),
        )

    def _recent_alert_exists(self, rule_name: str, now: datetime) -> bool:
        with self._connect() as connection:
            with connection.cursor() as cursor:
                cursor.execute(
                    f"""
                    SELECT triggered_at
                    FROM {self._full_table('alerts')}
                    WHERE rule_name = %s
                    ORDER BY triggered_at DESC
                    LIMIT 1
                    """,
                    (rule_name,),
                )
                row = cursor.fetchone()
        if row is None:
            return False
        triggered_at = row[0]
        return (now - triggered_at).total_seconds() < 60

    def _metric_recorded_at(self, metric: Metric) -> datetime:
        raw = metric.attributes.get("_recorded_at")
        if isinstance(raw, str):
            return datetime.fromisoformat(raw)
        return datetime.now(UTC)

    def _summary_spans(self) -> list[Span]:
        return self._load_spans()

    def _summary_metrics(self) -> list[Metric]:
        return self._load_metrics()

    def _summary_logs(self) -> list[LogEntry]:
        return self._load_logs()

    def _summary_alerts(self) -> list[AlertEvent]:
        return self._load_alerts()

    def _metrics_for_alerts(self) -> list[Metric]:
        return self._load_metrics(limit=500)

    def _load_spans(self, *, limit: int | None = None) -> list[Span]:
        query = (
            f"SELECT trace_id, span_id, parent_span_id, name, started_at, ended_at, duration_ms, "
            f"agent_name, session_id, status, attributes "
            f"FROM {self._full_table('spans')} ORDER BY ended_at ASC"
        )
        params: list[Any] = []
        if limit is not None:
            query += " LIMIT %s"
            params.append(limit)
        with self._connect() as connection:
            with connection.cursor() as cursor:
                cursor.execute(query, params)
                rows = cursor.fetchall()
        return [
            Span(
                trace_id=row[0],
                span_id=row[1],
                parent_span_id=row[2],
                name=row[3],
                started_at=row[4],
                ended_at=row[5],
                duration_ms=row[6],
                agent_name=row[7],
                session_id=row[8],
                status=row[9],
                attributes=row[10] if isinstance(row[10], dict) else json.loads(row[10] or "{}"),
            )
            for row in rows
        ]

    def _load_metrics(self, *, limit: int | None = None) -> list[Metric]:
        query = (
            f"SELECT name, value, unit, attributes, recorded_at "
            f"FROM {self._full_table('metrics')} ORDER BY recorded_at ASC"
        )
        params: list[Any] = []
        if limit is not None:
            query += " LIMIT %s"
            params.append(limit)
        with self._connect() as connection:
            with connection.cursor() as cursor:
                cursor.execute(query, params)
                rows = cursor.fetchall()
        metrics: list[Metric] = []
        for row in rows:
            attributes = row[3] if isinstance(row[3], dict) else json.loads(row[3] or "{}")
            attributes.setdefault("_recorded_at", row[4].isoformat())
            metrics.append(Metric(name=row[0], value=row[1], unit=row[2], attributes=attributes))
        return metrics

    def _load_logs(self, *, limit: int | None = None) -> list[LogEntry]:
        query = (
            f"SELECT timestamp, level, message, source, agent_name, session_id, trace_id, request_id, attributes "
            f"FROM {self._full_table('logs')} ORDER BY timestamp ASC"
        )
        params: list[Any] = []
        if limit is not None:
            query += " LIMIT %s"
            params.append(limit)
        with self._connect() as connection:
            with connection.cursor() as cursor:
                cursor.execute(query, params)
                rows = cursor.fetchall()
        return [
            LogEntry(
                timestamp=row[0].isoformat(),
                level=row[1],
                message=row[2],
                source=row[3],
                agent_name=row[4],
                session_id=row[5],
                trace_id=row[6],
                request_id=row[7],
                attributes=row[8] if isinstance(row[8], dict) else json.loads(row[8] or "{}"),
            )
            for row in rows
        ]

    def _load_alerts(self, *, limit: int | None = None) -> list[AlertEvent]:
        query = (
            f"SELECT id, rule_name, severity, message, triggered_at, value, attributes "
            f"FROM {self._full_table('alerts')} ORDER BY triggered_at ASC"
        )
        params: list[Any] = []
        if limit is not None:
            query += " LIMIT %s"
            params.append(limit)
        with self._connect() as connection:
            with connection.cursor() as cursor:
                cursor.execute(query, params)
                rows = cursor.fetchall()
        return [
            AlertEvent(
                id=row[0],
                rule_name=row[1],
                severity=row[2],
                message=row[3],
                triggered_at=row[4].isoformat(),
                value=row[5],
                attributes=row[6] if isinstance(row[6], dict) else json.loads(row[6] or "{}"),
            )
            for row in rows
        ]

    def _query_rows(
        self,
        *,
        table: str,
        select_columns: str,
        order_by: str,
        limit: int,
        scalar_filters: dict[str, Any] | None = None,
        time_filters: tuple[tuple[str, Any | None], ...] = (),
        attributes_filter: dict[str, str] | None = None,
    ) -> list[Any]:
        clauses: list[str] = []
        params: list[Any] = []

        for column, value in (scalar_filters or {}).items():
            if value is None:
                continue
            clauses.append(f"{column} = %s")
            params.append(value)

        for clause, value in time_filters:
            if value is None:
                continue
            clauses.append(clause)
            params.append(value)

        for key, value in (attributes_filter or {}).items():
            clauses.append("attributes @> %s::jsonb")
            params.append(json.dumps({key: value}))

        where_sql = f" WHERE {' AND '.join(clauses)}" if clauses else ""
        params.append(limit)

        query = (
            f"SELECT {select_columns} FROM {table}{where_sql} "
            f"ORDER BY {order_by} LIMIT %s"
        )
        with self._connect() as connection:
            with connection.cursor() as cursor:
                cursor.execute(query, params)
                return cursor.fetchall()


class NativeObservabilityExporter(Exporter):
    """Exporter that writes spans and metrics into the native observability store."""

    def __init__(self, store: ObservabilityStore | None = None) -> None:
        self.store = store or get_observability_store()

    async def on_span(self, span: Span) -> None:
        self.store.record_span(span)

    async def on_metric(self, metric: Metric) -> None:
        self.store.record_metric(metric)

    async def flush(self) -> None:
        return


_store: ObservabilityStore | None = None
_exporter: NativeObservabilityExporter | None = None


def _build_store() -> ObservabilityStore:
    dsn = resolve_database_url(
        env_vars=(
            "LOGOS_ADK_OBSERVABILITY_DSN",
            "LOGOS_ADK_DATABASE_URL",
            "DATABASE_URL",
        )
    )
    if dsn:
        return PostgreSQLObservabilityStore(
            dsn,
            retention=_retention_from_env(),
            cleanup_interval_seconds=int(os.getenv("LOGOS_ADK_OBSERVABILITY_CLEANUP_INTERVAL_SECONDS", "300")),
        )
    return NativeObservabilityStore()


def _default_retention_policies() -> dict[str, RetentionPolicy]:
    return {
        "spans": RetentionPolicy(ttl_days=7, max_rows=1_000_000),
        "metrics": RetentionPolicy(ttl_days=30, max_rows=2_000_000),
        "logs": RetentionPolicy(ttl_days=14, max_rows=1_000_000),
        "alerts": RetentionPolicy(ttl_days=30, max_rows=250_000),
    }


def _retention_from_env() -> dict[str, RetentionPolicy]:
    defaults = _default_retention_policies()
    env_mapping = {
        "spans": ("LOGOS_ADK_OBSERVABILITY_SPANS_TTL_DAYS", "LOGOS_ADK_OBSERVABILITY_SPANS_MAX_ROWS"),
        "metrics": ("LOGOS_ADK_OBSERVABILITY_METRICS_TTL_DAYS", "LOGOS_ADK_OBSERVABILITY_METRICS_MAX_ROWS"),
        "logs": ("LOGOS_ADK_OBSERVABILITY_LOGS_TTL_DAYS", "LOGOS_ADK_OBSERVABILITY_LOGS_MAX_ROWS"),
        "alerts": ("LOGOS_ADK_OBSERVABILITY_ALERTS_TTL_DAYS", "LOGOS_ADK_OBSERVABILITY_ALERTS_MAX_ROWS"),
    }
    resolved: dict[str, RetentionPolicy] = {}
    for signal, (ttl_env, max_rows_env) in env_mapping.items():
        default = defaults[signal]
        resolved[signal] = RetentionPolicy(
            ttl_days=int(os.getenv(ttl_env, str(default.ttl_days))),
            max_rows=int(os.getenv(max_rows_env, str(default.max_rows))),
        )
    return resolved


def reset_observability_store() -> ObservabilityStore:
    global _store, _exporter
    _store = _build_store()
    _exporter = NativeObservabilityExporter(_store)
    return _store


def get_observability_store() -> ObservabilityStore:
    global _store, _exporter
    if _store is None or _exporter is None:
        reset_observability_store()
    assert _store is not None
    return _store


def get_native_exporter() -> NativeObservabilityExporter:
    global _exporter
    if _exporter is None:
        reset_observability_store()
    assert _exporter is not None
    return _exporter
