"""latency_monitor.py — End-to-end latency monitoring and SLA enforcement.

Monitors the full pipeline:
  Tick ingestion → Feature computation → Signal generation → LLM verification → Order execution

SLA thresholds:
  - Direction change recognition: ≤500ms (configurable, X=500ms)
  - P99 latency: ≤X * 1.20 (120% of threshold)
  - Trend accuracy: ≥Y% (configurable, default 70%)
  - False positive rate: ≤Z% (default 5%)

Reports:
  - Real-time: P50/P95/P99 latency per pipeline stage
  - Daily: Accuracy, false positive rate, regime-wise breakdown
  - Per-event: Any delay ≥X ms logged for review

Usage:
  from src.latency_monitor import LatencyMonitor, get_monitor, report_delay_event

  monitor = get_monitor()
  monitor.start()

  # In signal pipeline:
  monitor.record_stage("tick_ingestion", start_ts, end_ts)
  monitor.record_stage("feature_compute", start_ts, end_ts)
  monitor.record_stage("llm_verify", start_ts, end_ts)
  monitor.record_stage("execution", start_ts, end_ts)
"""

from __future__ import annotations

import json
import os
import threading
import time
from collections import deque
from dataclasses import dataclass, field
from datetime import datetime, 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()

# ── Configurable SLA thresholds ───────────────────────────────

DIRECTION_CHANGE_SLA_MS = float(os.environ.get("LATENCY_SLA_MS", "500"))
P99_MULTIPLIER = 1.20
ACCURACY_THRESHOLD_PCT = float(os.environ.get("ACCURACY_THRESHOLD_PCT", "70"))
FALSE_POSITIVE_RATE_MAX = float(os.environ.get("FALSE_POSITIVE_RATE_MAX", "5"))
ALERT_COOLDOWN_SEC = 60  # Don't spam alerts


# ── Data structures ─────────────────────────────────────────────

@dataclass
class StageMetric:
    """Single pipeline stage latency record."""
    pipeline_cycle_id: str = ""
    stage: str = ""
    start_ts: float = 0.0
    end_ts: float = 0.0
    duration_ms: float = 0.0
    tick_ts: float = 0.0  # When the triggering tick arrived
    signal_generated: bool = False
    signal_verdict: str = ""  # GO / HOLD / VETO
    signal_side: str = ""
    confidence: float = 0.0
    regime: str = "UNKNOWN"
    is_anomaly: bool = False


@dataclass
class PipelineReport:
    """End-to-end pipeline report for one cycle."""
    cycle_id: str
    ts: str
    ts_unix: float

    # Stage timings
    tick_ingestion_ms: float = 0.0
    feature_compute_ms: float = 0.0
    signal_gen_ms: float = 0.0
    llm_verify_ms: float = 0.0
    risk_calibrate_ms: float = 0.0
    execution_ms: float = 0.0

    # Totals
    total_ms: float = 0.0
    sla_passed: bool = False
    p99_threshold_ms: float = 0.0

    # Signal info
    signal_generated: bool = False
    signal_verdict: str = ""
    signal_side: str = ""
    confidence: float = 0.0
    regime: str = "UNKNOWN"

    # Anomaly detection
    spread_anomaly: bool = False
    circuit_open: bool = False
    price_gap: bool = False


# ── Rolling window metrics ──────────────────────────────────────

METRICS_WINDOW_SIZE = 1000  # Keep last 1000 pipeline cycles
DAILY_REPORT_FILE = LOGS_DIR / "daily_latency_report.ndjson"
DELAY_EVENT_FILE = LOGS_DIR / "delay_events.ndjson"
PIPELINE_METRICS_FILE = LOGS_DIR / "pipeline_latency_metrics.ndjson"


class LatencyMonitor:
    """E2E latency monitoring with SLA enforcement and daily reporting."""

    def __init__(
        self,
        sla_ms: float = DIRECTION_CHANGE_SLA_MS,
        p99_multiplier: float = P99_MULTIPLIER,
    ) -> None:
        self.sla_ms = sla_ms
        self.p99_multiplier = p99_multiplier
        self.p99_threshold_ms = round(sla_ms * p99_multiplier, 1)

        self._lock = threading.Lock()
        self._cycles: deque[PipelineReport] = deque(maxlen=METRICS_WINDOW_SIZE)
        self._current_cycle: dict[str, Any] = {}
        self._cycle_count = 0

        # Per-stage rolling metrics
        self._stage_metrics: dict[str, list[float]] = {
            stage: [] for stage in [
                "tick_ingestion", "feature_compute", "signal_gen",
                "llm_verify", "risk_calibrate", "execution"
            ]
        }

        # Alert state
        self._last_alert_ts = 0.0
        self._alert_count_today = 0

        # Daily accuracy tracking
        self._signal_results: deque[dict] = deque(maxlen=200)  # signal → outcome
        self._daily_reset_hour = 0  # Reset daily stats at midnight

        # Running state
        self._running = False
        self._thread: threading.Thread | None = None

    def start(self) -> None:
        """Start the background monitoring thread."""
        if self._running:
            return
        self._running = True
        self._thread = threading.Thread(target=self._background_loop, daemon=True, name="LatencyMonitor")
        self._thread.start()

    def stop(self) -> None:
        """Stop the monitoring thread."""
        self._running = False
        if self._thread:
            self._thread.join(timeout=2.0)
            self._thread = None

    def _background_loop(self) -> None:
        """Background loop: produce daily reports and check for anomalies."""
        while self._running:
            now = datetime.now(timezone.utc)
            # Check for midnight reset
            if now.hour == 0 and self._daily_reset_hour != now.hour:
                self._produce_daily_report()
                self._daily_reset_hour = now.hour

            # Check P99 SLA every minute
            if int(now.timestamp()) % 60 < 1:
                self._check_p99_sla()

            time.sleep(1)

    def begin_pipeline_cycle(self) -> str:
        """Mark the start of a new pipeline cycle. Returns cycle_id."""
        cycle_id = f"pc_{int(time.time() * 1000)}"
        with self._lock:
            self._current_cycle = {
                "cycle_id": cycle_id,
                "ts": datetime.now(timezone.utc).isoformat(),
                "ts_unix": time.time(),
                "stages": {},
                "signal_generated": False,
            }
        return cycle_id

    def record_stage(self, stage: str, start_ts: float, end_ts: float) -> None:
        """Record a pipeline stage completion."""
        duration_ms = (end_ts - start_ts) * 1000
        cycle_id = ""

        with self._lock:
            if self._current_cycle:
                cycle_id = self._current_cycle.get("cycle_id", "")
                self._current_cycle["stages"][stage] = duration_ms

            # Update rolling stage metrics
            if stage in self._stage_metrics:
                self._stage_metrics[stage].append(duration_ms)
                if len(self._stage_metrics[stage]) > METRICS_WINDOW_SIZE:
                    self._stage_metrics[stage].pop(0)

        # Log to metrics file
        self._append_metric({
            "cycle_id": cycle_id,
            "stage": stage,
            "duration_ms": round(duration_ms, 2),
            "ts": datetime.now(timezone.utc).isoformat(),
        })

        # Check if over budget
        budget = 50.0 if stage == "feature_compute" else self.sla_ms
        if duration_ms > budget:
            self._log_delay_event(cycle_id, stage, duration_ms, budget)

    def complete_pipeline_cycle(
        self,
        cycle_id: str,
        signal_generated: bool = False,
        signal_verdict: str = "",
        signal_side: str = "",
        confidence: float = 0.0,
        regime: str = "UNKNOWN",
        spread_anomaly: bool = False,
        circuit_open: bool = False,
        price_gap: bool = False,
    ) -> PipelineReport:
        """Mark the completion of a pipeline cycle and compute report."""
        now = time.time()
        ts_iso = datetime.fromtimestamp(now, tz=timezone.utc).isoformat()

        with self._lock:
            stages = self._current_cycle.get("stages", {}) if self._current_cycle.get("cycle_id") == cycle_id else {}
            self._current_cycle = {}

        total_ms = sum(stages.values()) if stages else 0.0

        report = PipelineReport(
            cycle_id=cycle_id,
            ts=ts_iso,
            ts_unix=now,
            tick_ingestion_ms=round(stages.get("tick_ingestion", 0.0), 2),
            feature_compute_ms=round(stages.get("feature_compute", 0.0), 2),
            signal_gen_ms=round(stages.get("signal_gen", 0.0), 2),
            llm_verify_ms=round(stages.get("llm_verify", 0.0), 2),
            risk_calibrate_ms=round(stages.get("risk_calibrate", 0.0), 2),
            execution_ms=round(stages.get("execution", 0.0), 2),
            total_ms=round(total_ms, 2),
            sla_passed=total_ms <= self.sla_ms,
            p99_threshold_ms=self.p99_threshold_ms,
            signal_generated=signal_generated,
            signal_verdict=signal_verdict,
            signal_side=signal_side,
            confidence=confidence,
            regime=regime,
            spread_anomaly=spread_anomaly,
            circuit_open=circuit_open,
            price_gap=price_gap,
        )

        with self._lock:
            self._cycles.append(report)
            self._cycle_count += 1

        return report

    def record_signal_outcome(self, cycle_id: str, verdict: str, outcome: str) -> None:
        """Record the execution outcome of a signal (for accuracy tracking)."""
        with self._lock:
            self._signal_results.append({
                "cycle_id": cycle_id,
                "verdict": verdict,
                "outcome": outcome,  # FILLED / REJECTED / SKIPPED
                "ts": datetime.now(timezone.utc).isoformat(),
            })

    def _check_p99_sla(self) -> None:
        """Check P99 latency and fire alerts if exceeded."""
        latencies = [c.total_ms for c in self._cycles if c.total_ms > 0]
        if len(latencies) < 10:
            return

        sorted_lat = sorted(latencies)
        n = len(sorted_lat)
        p99 = sorted_lat[int(n * 0.99)]

        if p99 > self.p99_threshold_ms:
            now = time.time()
            if now - self._last_alert_ts > ALERT_COOLDOWN_SEC:
                self._fire_sla_alert(p99)
                self._last_alert_ts = now

    def _fire_sla_alert(self, p99_latency: float) -> None:
        """Fire SLA violation alert."""
        self._alert_count_today += 1
        print(f"[ALERT SLA] P99 latency {p99_latency:.1f}ms exceeds threshold "
              f"{self.p99_threshold_ms:.1f}ms (SLA: {self.sla_ms:.0f}ms)")
        self._append_daily_alert({
            "type": "SLA_VIOLATION",
            "p99_latency_ms": round(p99_latency, 1),
            "threshold_ms": self.p99_threshold_ms,
            "sla_ms": self.sla_ms,
            "ts": datetime.now(timezone.utc).isoformat(),
            "alert_count_today": self._alert_count_today,
        })

    def _log_delay_event(self, cycle_id: str, stage: str, duration_ms: float, budget_ms: float) -> None:
        """Log a delay event for post-session review."""
        event = {
            "cycle_id": cycle_id,
            "stage": stage,
            "duration_ms": round(duration_ms, 2),
            "budget_ms": round(budget_ms, 2),
            "excess_ms": round(duration_ms - budget_ms, 2),
            "ts": datetime.now(timezone.utc).isoformat(),
        }
        try:
            LOGS_DIR.mkdir(parents=True, exist_ok=True)
            with open(DELAY_EVENT_FILE, "a", encoding="utf-8") as f:
                f.write(json.dumps(event, ensure_ascii=False) + "\n")
        except Exception:
            pass

    def _append_metric(self, metric: dict[str, Any]) -> None:
        try:
            LOGS_DIR.mkdir(parents=True, exist_ok=True)
            with open(PIPELINE_METRICS_FILE, "a", encoding="utf-8") as f:
                f.write(json.dumps(metric, ensure_ascii=False) + "\n")
        except Exception:
            pass

    def _append_daily_alert(self, alert: dict[str, Any]) -> None:
        try:
            LOGS_DIR.mkdir(parents=True, exist_ok=True)
            with open(DAILY_REPORT_FILE, "a", encoding="utf-8") as f:
                f.write(json.dumps(alert, ensure_ascii=False) + "\n")
        except Exception:
            pass

    def _produce_daily_report(self) -> None:
        """Produce daily latency and accuracy report."""
        with self._lock:
            cycles = list(self._cycles)

        if not cycles:
            return

        latencies = [c.total_ms for c in cycles if c.total_ms > 0]
        if not latencies:
            return

        sorted_lat = sorted(latencies)
        n = len(sorted_lat)

        # Per-stage stats
        stage_stats = {}
        for stage in self._stage_metrics:
            if self._stage_metrics[stage]:
                s = sorted(self._stage_metrics[stage])
                cnt = len(s)
                stage_stats[stage] = {
                    "avg_ms": round(sum(s) / cnt, 2),
                    "p50_ms": round(s[cnt // 2], 2),
                    "p95_ms": round(s[int(cnt * 0.95)], 2),
                    "p99_ms": round(s[int(cnt * 0.99)], 2),
                    "max_ms": round(max(s), 2),
                }

        # Accuracy stats
        results = list(self._signal_results)
        go_signals = [r for r in results if r.get("verdict") == "GO"]
        filled = [r for r in go_signals if r.get("outcome") == "FILLED"]
        accuracy_pct = len(filled) / max(1, len(go_signals)) * 100

        false_positives = [r for r in go_signals if r.get("outcome") in ("REJECTED", "SKIPPED")]
        fp_rate = len(false_positives) / max(1, len(go_signals)) * 100

        # Regime breakdown
        regime_signals = {}
        for c in cycles:
            if c.signal_generated:
                r = c.regime
                if r not in regime_signals:
                    regime_signals[r] = {"total": 0, "sla_passed": 0}
                regime_signals[r]["total"] += 1
                if c.sla_passed:
                    regime_signals[r]["sla_passed"] += 1

        report = {
            "date": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
            "total_cycles": len(cycles),
            "sla_passed_rate_pct": round(sum(1 for c in cycles if c.sla_passed) / max(1, len(cycles)) * 100, 2),
            "latency": {
                "avg_ms": round(sum(latencies) / n, 2),
                "p50_ms": round(sorted_lat[n // 2], 2),
                "p95_ms": round(sorted_lat[int(n * 0.95)], 2),
                "p99_ms": round(sorted_lat[int(n * 0.99)], 2),
                "max_ms": round(max(latencies), 2),
            },
            "stages": stage_stats,
            "accuracy": {
                "total_signals": len(go_signals),
                "filled": len(filled),
                "accuracy_pct": round(accuracy_pct, 2),
                "false_positives": len(false_positives),
                "fp_rate_pct": round(fp_rate, 2),
            },
            "regime_breakdown": regime_signals,
            "alerts_today": self._alert_count_today,
            "ts": datetime.now(timezone.utc).isoformat(),
        }

        # Write daily report
        DAILY_REPORT_DIR = LOGS_DIR / "daily_reports"
        DAILY_REPORT_DIR.mkdir(parents=True, exist_ok=True)
        report_file = DAILY_REPORT_DIR / f"report_{datetime.now(timezone.utc).strftime('%Y-%m-%d')}.json"
        report_file.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")

        print(f"[DAILY_REPORT] Latency: avg={report['latency']['avg_ms']:.1f}ms "
              f"p99={report['latency']['p99_ms']:.1f}ms | "
              f"Accuracy: {report['accuracy']['accuracy_pct']:.1f}% | "
              f"FP: {report['accuracy']['fp_rate_pct']:.2f}%")

        # Reset daily counters
        self._alert_count_today = 0

    def get_status(self) -> dict[str, Any]:
        """Get current monitoring status."""
        with self._lock:
            cycles = list(self._cycles)

        latencies = [c.total_ms for c in cycles if c.total_ms > 0]
        if not latencies:
            return {"cycles": 0, "is_running": self._running}

        sorted_lat = sorted(latencies)
        n = len(sorted_lat)

        return {
            "is_running": self._running,
            "total_cycles": self._cycle_count,
            "sla_ms": self.sla_ms,
            "p99_threshold_ms": self.p99_threshold_ms,
            "latency": {
                "avg_ms": round(sum(latencies) / n, 2),
                "p50_ms": round(sorted_lat[n // 2], 2),
                "p95_ms": round(sorted_lat[int(n * 0.95)], 2),
                "p99_ms": round(sorted_lat[int(n * 0.99)], 2),
                "max_ms": round(max(latencies), 2),
            },
            "sla_passed_pct": round(sum(1 for c in cycles if c.sla_passed) / max(1, n) * 100, 2),
            "alerts_today": self._alert_count_today,
            "stage_metrics": {
                stage: {
                    "avg_ms": round(sum(v) / max(1, len(v)), 2),
                    "p99_ms": round(sorted(v)[int(len(v) * 0.99)] if len(v) >= 10 else max(v or [0]), 2),
                    "samples": len(v),
                }
                for stage, v in self._stage_metrics.items()
                if v
            },
        }


# ── Global singleton ────────────────────────────────────────────

_monitor_instance: LatencyMonitor | None = None
_monitor_lock = threading.Lock()


def get_monitor(sla_ms: float = DIRECTION_CHANGE_SLA_MS) -> LatencyMonitor:
    global _monitor_instance
    with _monitor_lock:
        if _monitor_instance is None:
            _monitor_instance = LatencyMonitor(sla_ms=sla_ms)
            _monitor_instance.start()
        return _monitor_instance


def report_delay_event(cycle_id: str, stage: str, duration_ms: float, budget_ms: float) -> None:
    """Convenience function to log a delay event."""
    get_monitor()._log_delay_event(cycle_id, stage, duration_ms, budget_ms)


# ── CLI ─────────────────────────────────────────────────────────

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Latency monitor")
    parser.add_argument("--status", action="store_true", help="Show current status")
    parser.add_argument("--daily", action="store_true", help="Produce daily report")
    parser.add_argument("--sla", type=float, default=500, help="SLA threshold in ms")
    args = parser.parse_args()

    monitor = get_monitor(sla_ms=args.sla)

    if args.status:
        status = monitor.get_status()
        print(json.dumps(status, indent=2, ensure_ascii=False))
    elif args.daily:
        monitor._produce_daily_report()
    else:
        print(f"LatencyMonitor running. SLA: {args.sla}ms, P99 threshold: {args.sla * 1.2:.0f}ms")