"""
Crash/error telemetry — capture, store, and report errors for production monitoring.

Non-blocking: errors are written to disk and never thrown back to the caller.
No network calls: all data stays local. Opt-in remote reporting via webhook.

Usage:
  from src.telemetry.tracker import capture_exception, get_error_report

  try:
      ...
  except Exception:
      capture_exception()  # logs to file, doesn't re-raise

  # Or as a context manager:
  with error_context("pipeline.execute"):
      ...
"""
from __future__ import annotations

import json
import os
import sys
import threading
import traceback
from contextlib import contextmanager
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

TELEMETRY_DIR = Path(__file__).resolve().parent.parent.parent / "data" / "telemetry"
ERRORS_FILE = TELEMETRY_DIR / "errors.ndjson"
CRASHES_FILE = TELEMETRY_DIR / "crashes.ndjson"
METRICS_FILE = TELEMETRY_DIR / "metrics.ndjson"

MAX_ERRORS = int(os.environ.get("GAT_TELEMETRY_MAX_ERRORS", "10000"))
MAX_CRASHES = int(os.environ.get("GAT_TELEMETRY_MAX_CRASHES", "1000"))
MAX_METRICS = int(os.environ.get("GAT_TELEMETRY_MAX_METRICS", "50000"))

_write_lock = threading.Lock()


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


def _append_jsonl(path: Path, entry: dict[str, Any], max_lines: int) -> None:
    with _write_lock:
        path.parent.mkdir(parents=True, exist_ok=True)
        lines = []
        if path.exists():
            try:
                lines = path.read_text(encoding="utf-8").splitlines()
            except Exception:
                pass
        lines.append(json.dumps(entry, ensure_ascii=False))
        if len(lines) > max_lines:
            lines = lines[-max_lines:]
        path.write_text("\n".join(lines) + "\n", encoding="utf-8")


# ─── Error Capture ─────────────────────────────────────────────────

def capture_exception(
    error: Exception | None = None,
    context: str = "",
    metadata: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """
    Capture an exception for telemetry. Does NOT re-raise.

    Call this in except blocks to record the error without disrupting execution.
    If error is None, uses sys.exc_info() to get the current exception.
    """
    if error is None:
        exc_type, exc_value, exc_tb = sys.exc_info()
        if exc_value is None:
            return {"ok": False, "reason": "no active exception"}
        error = exc_value
    else:
        exc_tb = error.__traceback__

    tb_lines = traceback.format_exception(type(error), error, exc_tb) if exc_tb else [str(error)]

    entry = {
        "timestamp": _now(),
        "type": type(error).__name__,
        "message": str(error)[:500],
        "context": context,
        "traceback": "".join(tb_lines)[-2000:],  # last 2000 chars
        "metadata": metadata or {},
    }

    _append_jsonl(ERRORS_FILE, entry, MAX_ERRORS)

    # Also write to stderr for immediate visibility
    print(f"[TELEMETRY] {entry['type']}: {entry['message'][:100]}", file=sys.stderr)

    return {"ok": True, "recorded": entry["type"]}


def capture_crash(context: str = "", metadata: dict[str, Any] | None = None) -> dict[str, Any]:
    """
    Record a crash (process-level failure). More detailed than capture_exception.
    """
    exc_type, exc_value, exc_tb = sys.exc_info()
    tb_lines = traceback.format_exception(exc_type, exc_value, exc_tb) if exc_tb else ["unknown"]

    entry = {
        "timestamp": _now(),
        "type": str(exc_type.__name__) if exc_type else "Unknown",
        "message": str(exc_value)[:500] if exc_value else "No error details",
        "context": context,
        "traceback": "".join(tb_lines),
        "metadata": metadata or {},
        "python_version": sys.version,
        "platform": sys.platform,
    }

    _append_jsonl(CRASHES_FILE, entry, MAX_CRASHES)
    return {"ok": True, "recorded": entry["type"]}


# ─── Context Manager ───────────────────────────────────────────────

@contextmanager
def error_context(context: str, capture_as_crash: bool = False):
    """
    Context manager that captures any exception and continues.

    with error_context("pipeline.execute"):
        risky_operation()
    """
    try:
        yield
    except Exception as e:
        if capture_as_crash:
            capture_crash(context)
        else:
            capture_exception(e, context)


# ─── Metrics Tracking ──────────────────────────────────────────────

def track_metric(name: str, value: float, tags: dict[str, str] | None = None) -> None:
    """Record a numeric metric for trending."""
    entry = {
        "timestamp": _now(),
        "metric": name,
        "value": value,
        "tags": tags or {},
    }
    _append_jsonl(METRICS_FILE, entry, MAX_METRICS)


# ─── Reporting ─────────────────────────────────────────────────────

def get_error_report(limit: int = 50, error_type: str = "") -> list[dict[str, Any]]:
    """Get recent errors for dashboard display."""
    if not ERRORS_FILE.exists():
        return []
    try:
        errors = []
        for line in ERRORS_FILE.read_text(encoding="utf-8").splitlines():
            if not line.strip():
                continue
            try:
                e = json.loads(line)
                if error_type and e.get("type", "") != error_type:
                    continue
                errors.append(e)
            except json.JSONDecodeError:
                continue
        return errors[-limit:]
    except Exception:
        return []


def get_error_stats() -> dict[str, Any]:
    """Get error statistics: counts by type, recent frequency."""
    errors = get_error_report(limit=1000)
    if not errors:
        return {"total": 0, "by_type": {}, "recent_24h": 0}

    by_type: dict[str, int] = {}
    recent = 0
    cutoff = datetime.now(timezone.utc).isoformat()[:10]  # today

    for e in errors:
        t = e.get("type", "Unknown")
        by_type[t] = by_type.get(t, 0) + 1
        if e.get("timestamp", "")[:10] == cutoff:
            recent += 1

    return {"total": len(errors), "by_type": by_type, "recent_24h": recent}
