"""Datadog metrics + structured log shipping.

All public calls are fire-and-forget — hot paths enqueue a dict and return
immediately (O(1), no I/O). Three background daemon threads handle the actual
network I/O at their own pace.

Usage:
    from ai_interview.metrics import metrics

    # One-time init inside run_daemon()
    metrics.start(config)
    logging.getLogger().addHandler(metrics.make_log_handler())

    # Fire-and-forget anywhere
    metrics.record("screenshot_capture", duration_ms=45, ok=True)

    # Context manager — auto-captures wall-clock duration
    with metrics.timer("claude_query", provider="claude") as t:
        t.set(chars=len(response))

    # On SIGTERM
    metrics.stop()
"""

from __future__ import annotations

import json
import logging
import os
import queue
import socket
import threading
import time
from contextlib import contextmanager
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Generator

if TYPE_CHECKING:
    from ai_interview.config import Config

logger = logging.getLogger(__name__)

_SENTINEL = object()
_DD_LOGS_INTAKE = "https://http-intake.logs.datadoghq.com/api/v2/logs"
_FLUSH_INTERVAL_S = 10      # metrics batch flush
_LOG_FLUSH_INTERVAL_S = 5   # log batch flush
_LOG_BATCH_MAX = 100        # flush logs early if batch reaches this size
_SYSTEM_SAMPLE_INTERVAL_S = 10


# ---------------------------------------------------------------------------
# Timer context manager
# ---------------------------------------------------------------------------

class _TimerCtx:
    """Returned by metrics.timer(). Records duration on __exit__."""

    def __init__(self, collector: "MetricsCollector", event: str, tags: dict) -> None:
        self._collector = collector
        self._event = event
        self._tags = tags
        self._t0: float = 0.0
        self._extra: dict = {}

    def set(self, **kwargs: Any) -> None:
        """Attach extra fields to the final record (call mid-flight)."""
        self._extra.update(kwargs)

    def __enter__(self) -> "_TimerCtx":
        self._t0 = time.monotonic()
        return self

    def __exit__(self, *_) -> None:
        duration_ms = int((time.monotonic() - self._t0) * 1000)
        self._collector.record(self._event, duration_ms=duration_ms,
                               **self._tags, **self._extra)


# ---------------------------------------------------------------------------
# Datadog log handler
# ---------------------------------------------------------------------------

class DatadogLogHandler(logging.Handler):
    """Enqueues log records for async batch shipping to DD Logs intake.

    Only WARNING+ records are forwarded (avoids flooding DD with debug output).
    All enqueues are non-blocking — if the queue is full the record is dropped
    silently rather than blocking the caller.
    """

    def __init__(self, log_queue: queue.SimpleQueue, enabled: bool) -> None:
        super().__init__(level=logging.WARNING)
        self._q = log_queue
        self._enabled = enabled
        self._hostname = socket.gethostname()

    def emit(self, record: logging.LogRecord) -> None:
        if not self._enabled:
            return
        try:
            self._q.put_nowait({
                "ddsource": "python",
                "service": "ai-interview-assistant",
                "hostname": self._hostname,
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "level": record.levelname,
                "logger": record.name,
                "message": self.format(record),
            })
        except Exception:
            pass  # never raise from a log handler


# ---------------------------------------------------------------------------
# Main collector
# ---------------------------------------------------------------------------

class MetricsCollector:
    """Singleton metrics collector. All public methods are no-ops until start()."""

    def __init__(self) -> None:
        self._metric_q: queue.SimpleQueue = queue.SimpleQueue()
        self._log_q: queue.SimpleQueue = queue.SimpleQueue()
        self._enabled = False
        self._api_key = ""
        self._app_key = ""
        self._threads: list[threading.Thread] = []
        self._stop_event = threading.Event()

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    def start(self, config: "Config") -> None:
        """Initialise DD and start background threads. No-op if no DD key."""
        api_key = getattr(config, "dd_api_key", "") or ""
        app_key = getattr(config, "dd_app_key", "") or ""
        if not api_key:
            return

        try:
            import datadog
            datadog.initialize(api_key=api_key, app_key=app_key)
        except ImportError:
            logger.warning("datadog package not installed — metrics disabled. "
                           "Run: pip install datadog")
            return
        except Exception as exc:
            logger.warning("Datadog init failed: %s — metrics disabled", exc)
            return

        self._api_key = api_key
        self._app_key = app_key
        self._enabled = True
        self._stop_event.clear()

        # Thread 1 — metrics flusher
        t1 = threading.Thread(target=self._metrics_flush_loop, daemon=True,
                              name="dd-metrics-flusher")
        # Thread 2 — log flusher
        t2 = threading.Thread(target=self._log_flush_loop, daemon=True,
                              name="dd-log-flusher")
        # Thread 3 — psutil sampler
        t3 = threading.Thread(target=self._system_sampler_loop, daemon=True,
                              name="dd-system-sampler")

        self._threads = [t1, t2, t3]
        for t in self._threads:
            t.start()

        logger.info("Datadog metrics enabled — flushing every %ds", _FLUSH_INTERVAL_S)

    def stop(self) -> None:
        """Flush remaining queue and stop all background threads."""
        if not self._enabled:
            return
        self._stop_event.set()
        # Unblock queue.get() calls with sentinels
        self._metric_q.put(_SENTINEL)
        self._log_q.put(_SENTINEL)
        for t in self._threads:
            t.join(timeout=3.0)
        self._enabled = False

    # ------------------------------------------------------------------
    # Public API — fire-and-forget
    # ------------------------------------------------------------------

    def record(self, event: str, val: float = 1, **kwargs: Any) -> None:
        """Enqueue one metric point. Returns immediately — no I/O."""
        if not self._enabled:
            return
        try:
            self._metric_q.put_nowait({
                "event": event,
                "val": val,
                "ts": time.time(),
                "tags": kwargs,
            })
        except Exception:
            pass

    def timer(self, event: str, **tags: Any) -> _TimerCtx:
        """Return a context manager that records wall-clock duration on exit."""
        return _TimerCtx(self, event, tags)

    def make_log_handler(self) -> DatadogLogHandler:
        """Return a logging.Handler that ships WARNING+ logs to DD Logs."""
        return DatadogLogHandler(self._log_q, self._enabled)

    # ------------------------------------------------------------------
    # Background threads
    # ------------------------------------------------------------------

    def _metrics_flush_loop(self) -> None:
        """Drain metric queue every 10s and batch-send to Datadog."""
        try:
            import datadog.api as dd_api
        except ImportError:
            return

        while not self._stop_event.is_set():
            # Sleep in small increments so stop_event is checked promptly
            for _ in range(_FLUSH_INTERVAL_S * 10):
                if self._stop_event.is_set():
                    break
                time.sleep(0.1)
            self._flush_metrics(dd_api)

        # Final flush on stop
        self._flush_metrics(dd_api)

    def _flush_metrics(self, dd_api: Any) -> None:
        items: list[dict] = []
        while True:
            try:
                item = self._metric_q.get_nowait()
            except queue.Empty:
                break
            if item is _SENTINEL:
                break
            items.append(item)

        if not items:
            return

        # Build DD metric payload
        # Latency/size metrics → distribution (gets p50/p95/p99 in DD)
        # Gauge/count metrics handled by the "type" key in tags (default: distribution)
        _GAUGE_EVENTS = {
            "system.process_cpu_pct", "system.process_rss_mb",
            "system.host_cpu_pct", "system.host_mem_pct",
        }
        _COUNT_EVENTS = {"hotkey", "deepgram_final", "deepgram_reconnect"}

        series = []
        for item in items:
            event = item["event"]
            metric_name = f"ai_interview.{event}"
            tags = [f"{k}:{v}" for k, v in item["tags"].items()]

            if event in _GAUGE_EVENTS:
                metric_type = "gauge"
            elif event in _COUNT_EVENTS:
                metric_type = "count"
            else:
                metric_type = "distribution"

            series.append({
                "metric": metric_name,
                "points": [[item["ts"], item["val"]]],
                "type": metric_type,
                "tags": tags,
            })

        try:
            dd_api.Metric.send(series)
        except Exception as exc:
            logger.debug("DD metrics flush error: %s", exc)

    def _log_flush_loop(self) -> None:
        """Batch log records every 5s (or 100 records) and POST to DD Logs."""
        batch: list[dict] = []
        last_flush = time.monotonic()

        while not self._stop_event.is_set():
            # Try to get a record with short timeout
            try:
                record = self._log_q.get(timeout=0.5)
                if record is _SENTINEL:
                    break
                batch.append(record)
            except queue.Empty:
                pass

            now = time.monotonic()
            if batch and (
                len(batch) >= _LOG_BATCH_MAX
                or (now - last_flush) >= _LOG_FLUSH_INTERVAL_S
            ):
                self._flush_logs(batch)
                batch = []
                last_flush = now

        # Final flush
        if batch:
            self._flush_logs(batch)

    def _flush_logs(self, batch: list[dict]) -> None:
        try:
            import httpx
            httpx.post(
                _DD_LOGS_INTAKE,
                headers={
                    "DD-API-KEY": self._api_key,
                    "Content-Type": "application/json",
                },
                content=json.dumps(batch),
                timeout=10,
            )
        except Exception as exc:
            logger.debug("DD logs flush error: %s", exc)

    def _system_sampler_loop(self) -> None:
        """Sample process + host CPU/memory every 10s and enqueue as gauges."""
        try:
            import psutil
        except ImportError:
            logger.debug("psutil not installed — system metrics disabled. "
                         "Run: pip install psutil")
            return

        proc = psutil.Process(os.getpid())
        # First call initialises the interval baseline (returns 0.0)
        proc.cpu_percent(interval=None)
        psutil.cpu_percent(interval=None)

        while not self._stop_event.is_set():
            for _ in range(_SYSTEM_SAMPLE_INTERVAL_S * 10):
                if self._stop_event.is_set():
                    return
                time.sleep(0.1)

            try:
                self.record("system.process_cpu_pct",
                            val=proc.cpu_percent(interval=None))
                self.record("system.process_rss_mb",
                            val=proc.memory_info().rss / (1024 ** 2))
                self.record("system.host_cpu_pct",
                            val=psutil.cpu_percent(interval=None))
                self.record("system.host_mem_pct",
                            val=psutil.virtual_memory().percent)
            except Exception as exc:
                logger.debug("System metrics sample error: %s", exc)


# ---------------------------------------------------------------------------
# Module-level singleton — import from anywhere
# ---------------------------------------------------------------------------

metrics = MetricsCollector()
