"""monitor.py — Metrics & alerting for the data pipeline.

Tracks:
- upload_success_count / upload_fail_count
- upload_batch_size (avg bytes)
- upload_latency_ms (avg, p95)
- data_freshness_lag_seconds
- server_ingest_error_count
- queue_depth

Alerts (logged + webhook):
- ERROR: upload fails 5x in a row → alert
- WARN: freshness lag > 5 min → warning
- WARN: error rate > 5% over 10 min window → warning
"""

from __future__ import annotations

import json
import os
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any

import config_manager

APP_DIR = config_manager.get_app_dir()
DATA_DIR = config_manager.get_data_dir()
LOGS_DIR = config_manager.get_logs_dir()

METRICS_FILE = LOGS_DIR / "data_pipeline_metrics.ndjson"
METRICS_WINDOW_MINUTES = 10


# ── Metric window helpers ──────────────────────────────────────────

def _load_metrics_window(minutes: int = METRICS_WINDOW_MINUTES) -> list[dict]:
    if not METRICS_FILE.exists():
        return []
    cutoff = datetime.now(timezone.utc) - timedelta(minutes=minutes)
    entries: list[dict] = []
    for line in METRICS_FILE.read_text(encoding="utf-8").strip().split("\n"):
        if not line.strip():
            continue
        try:
            entry = json.loads(line)
            ts = entry.get("ts", "")
            if ts:
                if ts.endswith("Z"):
                    dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
                else:
                    dt = datetime.fromisoformat(ts)
                if dt >= cutoff:
                    entries.append(entry)
        except Exception:
            pass
    return entries


def _compute_alert_level(entries: list[dict]) -> str:
    """Compute alert level from recent metrics: OK / WARN / ERROR."""
    recent_fail = sum(1 for e in entries if e.get("category") == "uploader" and e.get("key") == "batch_failed")
    recent_total = sum(1 for e in entries if e.get("category") == "uploader" and e.get("key") in ("batch_uploaded", "batch_failed"))
    error_rate = recent_fail / max(1, recent_total)

    if recent_fail >= 5:
        return "ERROR"
    if error_rate > 0.05 or recent_fail >= 3:
        return "WARN"
    return "OK"


# ── Public API ──────────────────────────────────────────────────

def get_pipeline_status() -> dict[str, Any]:
    """Return current pipeline health status with all metrics."""
    entries = _load_metrics_window(METRICS_WINDOW_MINUTES)

    uploaded = sum(1 for e in entries if e.get("category") == "uploader" and e.get("key") == "batch_uploaded")
    failed = sum(1 for e in entries if e.get("category") == "uploader" and e.get("key") == "batch_failed")
    retried = sum(1 for e in entries if e.get("category") == "uploader" and e.get("key") == "retry_succeeded")
    pii_detected = sum(1 for e in entries if e.get("category") == "pipeline" and e.get("key") == "pii_detected")
    skipped = sum(1 for e in entries if e.get("category") == "pipeline" and e.get("key") == "skipped_no_new_events")

    # Compute freshness lag from last uploaded event timestamp
    freshness_lag = 0.0
    for e in reversed(entries):
        if e.get("category") == "uploader" and e.get("key") == "batch_uploaded":
            last_ts = float(e.get("ts", 0) or 0)
            if last_ts > 0:
                freshness_lag = max(0.0, time.time() - last_ts)
            break

    # Queue depth
    try:
        from src.data_pipeline.uploader import _load_retry_queue
        queue_depth = len(_load_retry_queue())
    except Exception:
        queue_depth = 0

    total = uploaded + failed
    error_rate = (failed / max(1, total)) if total else 0.0

    alert_level = _compute_alert_level(entries)

    return {
        "ts": datetime.now(timezone.utc).isoformat(),
        "window_minutes": METRICS_WINDOW_MINUTES,
        "alert_level": alert_level,
        "metrics": {
            "uploads": uploaded,
            "failures": failed,
            "retries_succeeded": retried,
            "pii_detected": pii_detected,
            "skipped": skipped,
            "total": total,
            "error_rate_pct": round(error_rate * 100, 2),
            "queue_depth": queue_depth,
        },
        "freshness_lag_sec": freshness_lag,
    }


def check_and_alert() -> list[str]:
    """Check pipeline status and fire alerts if thresholds exceeded.

    Returns list of alert messages (empty = all clear).
    """
    alerts: list[str] = []
    status = get_pipeline_status()
    metrics = status["metrics"]
    m = metrics

    # ERROR: consecutive failures >= 5
    if m["failures"] >= 5:
        alerts.append(
            f"[ALERT ERROR] Data pipeline: {m['failures']} upload failures in last "
            f"{METRICS_WINDOW_MINUTES}min. Check retry queue and network connectivity."
        )

    # ERROR: PII detected
    if m["pii_detected"] > 0:
        alerts.append(
            f"[ALERT ERROR] Data pipeline: PII data detected in {m['pii_detected']} batch(es). "
            f"Upload blocked — inspect anonymizer output immediately."
        )

    # WARN: error rate > 5%
    if m["total"] >= 10 and m["error_rate_pct"] > 5.0:
        alerts.append(
            f"[ALERT WARN] Data pipeline error rate: {m['error_rate_pct']}% "
            f"({m['failures']}/{m['total']} batches failed) in last {METRICS_WINDOW_MINUTES}min"
        )

    # WARN: retry queue growing
    if m["queue_depth"] > 10:
        alerts.append(
            f"[ALERT WARN] Retry queue depth: {m['queue_depth']} batches waiting. "
            f"Network or server may be unavailable."
        )

    # Print/log all alerts
    for alert in alerts:
        print(alert)
        _append_alert_log(alert)

    return alerts


def _append_alert_log(message: str) -> None:
    ALERT_LOG = LOGS_DIR / "data_pipeline_alerts.ndjson"
    LOGS_DIR.mkdir(parents=True, exist_ok=True)
    with open(ALERT_LOG, "a", encoding="utf-8") as f:
        f.write(json.dumps({
            "ts": datetime.now(timezone.utc).isoformat(),
            "message": message,
        }, ensure_ascii=False) + "\n")
