"""Real-Time Agent Monitoring Dashboard (TUI).

Provides a text-based dashboard that shows live agent activity,
anomaly alerts, and policy enforcement statistics. No curses
dependency -- the dashboard is rendered as a plain formatted string
that can be reprinted to the terminal after a clear.

Thread-safe: all mutable state is guarded by a single lock so
``record_event`` can be called from multiple threads.
"""

from __future__ import annotations

import threading
import time
from collections import defaultdict
from dataclasses import dataclass, field

from aegis.core.action import Action
from aegis.core.anomaly import AnomalyDetector, AnomalyResult

# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------


@dataclass
class MonitorState:
    """Snapshot of all dashboard metrics.

    Attributes:
        active_agents: Mapping of agent_id to cumulative action count.
        blocked_counts: Per-agent count of blocked actions.
        anomaly_counts: Per-agent count of anomalies detected.
        recent_alerts: Most recent anomaly alert strings (bounded).
        decision_counters: Counts keyed by decision type (auto/approve/block).
        action_type_counts: Frequency counter for action types.
        total_actions: Total events recorded since start.
        start_time: Monotonic time when monitoring started.
        event_timestamps: Rolling window of event timestamps for rate calc.
        max_alerts: Maximum number of recent alerts to retain.
    """

    active_agents: dict[str, int] = field(default_factory=lambda: defaultdict(int))
    blocked_counts: dict[str, int] = field(default_factory=lambda: defaultdict(int))
    anomaly_counts: dict[str, int] = field(default_factory=lambda: defaultdict(int))
    recent_alerts: list[str] = field(default_factory=list)
    decision_counters: dict[str, int] = field(
        default_factory=lambda: defaultdict(int),
    )
    action_type_counts: dict[str, int] = field(
        default_factory=lambda: defaultdict(int),
    )
    total_actions: int = 0
    start_time: float = field(default_factory=time.monotonic)
    event_timestamps: list[float] = field(default_factory=list)
    max_alerts: int = 20


# ---------------------------------------------------------------------------
# Monitor
# ---------------------------------------------------------------------------

# Maximum number of timestamps kept for rate calculation.
_MAX_RATE_WINDOW: int = 500


class AgentMonitor:
    """Live agent monitor that collects events and renders a dashboard.

    Parameters:
        detector: An optional :class:`AnomalyDetector` instance.  When
            provided, every recorded event is also checked for anomalies.
        max_alerts: Maximum number of recent alert strings to keep.
        rate_window: Seconds over which the actions-per-minute rate is
            calculated.  Default ``60.0``.
    """

    def __init__(
        self,
        detector: AnomalyDetector | None = None,
        *,
        max_alerts: int = 20,
        rate_window: float = 60.0,
    ) -> None:
        self._detector = detector or AnomalyDetector()
        self._state = MonitorState(max_alerts=max_alerts)
        self._rate_window = rate_window
        self._lock = threading.Lock()

    # -- public API ---------------------------------------------------------

    @property
    def state(self) -> MonitorState:
        """Return the current state (for testing/inspection)."""
        return self._state

    def record_event(
        self,
        action: str | Action,
        agent_id: str = "default",
        decision: str = "auto",
        *,
        target: str = "",
    ) -> AnomalyResult | None:
        """Record an action event and optionally run anomaly detection.

        Args:
            action: Either an :class:`Action` object or a string action
                type.  When a string is provided, *target* is used.
            agent_id: Agent identifier.
            decision: Policy decision -- one of ``"auto"``,
                ``"approve"``, ``"block"``.
            target: Action target (used when *action* is a string).

        Returns:
            An :class:`AnomalyResult` when the detector flags an anomaly,
            otherwise ``None``.
        """
        if isinstance(action, str):
            action_obj = Action(type=action, target=target or "unknown")
        else:
            action_obj = action

        blocked = decision == "block"
        now = time.monotonic()

        # Check for anomalies *before* recording so that the detector
        # compares against the historical profile (not the new event).
        anomaly = self._detector.check(action_obj, agent_id)
        self._detector.record(action_obj, agent_id, blocked=blocked)

        with self._lock:
            s = self._state

            # Agent tracking
            s.active_agents[agent_id] += 1
            if blocked:
                s.blocked_counts[agent_id] += 1

            # Decision counters
            s.decision_counters[decision] += 1

            # Action type frequency
            s.action_type_counts[action_obj.type] += 1

            # Total
            s.total_actions += 1

            # Rate timestamps (bounded)
            s.event_timestamps.append(now)
            if len(s.event_timestamps) > _MAX_RATE_WINDOW:
                s.event_timestamps[:] = s.event_timestamps[-_MAX_RATE_WINDOW:]

            # Anomaly alerts
            if anomaly is not None and anomaly.is_anomalous:
                s.anomaly_counts[agent_id] += 1
                alert = f"[WARN] {agent_id}: {anomaly.anomaly_type} on {action_obj.type}"
                if anomaly.anomaly_type == "unusual_target":
                    alert = f'[WARN] {agent_id}: {anomaly.anomaly_type} "{action_obj.target}"'
                s.recent_alerts.append(alert)
                if len(s.recent_alerts) > s.max_alerts:
                    s.recent_alerts[:] = s.recent_alerts[-s.max_alerts :]
                return anomaly

        return None

    def actions_per_minute(self) -> float:
        """Compute the current actions-per-minute rate.

        Uses event timestamps within ``rate_window`` seconds of now.
        """
        now = time.monotonic()
        with self._lock:
            timestamps = self._state.event_timestamps
            cutoff = now - self._rate_window
            recent = [t for t in timestamps if t >= cutoff]
        if len(recent) < 2:
            return 0.0
        span = recent[-1] - recent[0]
        if span <= 0:
            return 0.0
        return (len(recent) - 1) / (span / 60.0)

    def top_action_types(self, n: int = 5) -> list[tuple[str, int]]:
        """Return the top *n* action types by frequency."""
        with self._lock:
            items = sorted(
                self._state.action_type_counts.items(),
                key=lambda kv: kv[1],
                reverse=True,
            )
        return items[:n]

    def uptime_str(self) -> str:
        """Return a human-readable uptime string like ``2h 15m``."""
        elapsed = time.monotonic() - self._state.start_time
        return _format_duration(elapsed)

    def render(self, width: int = 56) -> str:
        """Render the dashboard as a formatted string.

        Args:
            width: Inner width of the box (between the vertical bars).

        Returns:
            A multi-line string containing the full dashboard.
        """
        with self._lock:
            s = self._state
            total = s.total_actions
            rate = self._actions_per_minute_unlocked()
            uptime = _format_duration(time.monotonic() - s.start_time)

            # --- agent table ---
            agents: list[tuple[str, int, int, int]] = []
            for aid in sorted(s.active_agents):
                agents.append(
                    (
                        aid,
                        s.active_agents[aid],
                        s.blocked_counts.get(aid, 0),
                        s.anomaly_counts.get(aid, 0),
                    )
                )

            # --- decision distribution ---
            auto = s.decision_counters.get("auto", 0)
            approve = s.decision_counters.get("approve", 0)
            block = s.decision_counters.get("block", 0)

            alerts = list(s.recent_alerts)

        lines: list[str] = []
        iw = width  # inner width
        h_bar = "\u2550"

        # Top border
        lines.append(f"\u2554\u2550\u2550 Aegis Agent Monitor {h_bar * (iw - 21)}\u2557")

        # Summary line
        summary = f" Uptime: {uptime} | Actions: {total:,} | Rate: {rate:.1f}/min "
        lines.append(f"\u2551{summary:<{iw}}\u2551")

        # Separator
        lines.append(f"\u2560{h_bar * iw}\u2563")

        # Agent table header
        hdr = f" {'Agent':<12} | {'Actions':>7} | {'Blocked':>7} | {'Anomalies':>9} "
        lines.append(f"\u2551{hdr:<{iw}}\u2551")
        for aid, acts, blk, ano in agents:
            row = f" {aid:<12} | {acts:>7,} | {blk:>7,} | {ano:>9,} "
            lines.append(f"\u2551{row:<{iw}}\u2551")

        # Separator
        lines.append(f"\u2560{h_bar * iw}\u2563")

        # Recent alerts
        alert_hdr = " Recent Alerts"
        lines.append(f"\u2551{alert_hdr:<{iw}}\u2551")
        if alerts:
            for a in alerts[-5:]:
                lines.append(f"\u2551 {a:<{iw - 1}}\u2551")
        else:
            lines.append(f"\u2551{' (none)':<{iw}}\u2551")

        # Separator
        lines.append(f"\u2560{h_bar * iw}\u2563")

        # Decision distribution
        dist_hdr = " Decision Distribution"
        lines.append(f"\u2551{dist_hdr:<{iw}}\u2551")
        dist_parts: list[str] = []
        for label, cnt in [("auto", auto), ("approve", approve), ("block", block)]:
            pct = (cnt / total * 100) if total > 0 else 0.0
            dist_parts.append(f"{label}: {cnt:,} ({pct:.1f}%)")
        dist_line = " " + " | ".join(dist_parts) + " "
        lines.append(f"\u2551{dist_line:<{iw}}\u2551")

        # Bottom border
        lines.append(f"\u255a{h_bar * iw}\u255d")

        return "\n".join(lines)

    # -- helpers (private) --------------------------------------------------

    def _actions_per_minute_unlocked(self) -> float:
        """Compute rate without acquiring the lock (caller holds it)."""
        now = time.monotonic()
        cutoff = now - self._rate_window
        recent = [t for t in self._state.event_timestamps if t >= cutoff]
        if len(recent) < 2:
            return 0.0
        span = recent[-1] - recent[0]
        if span <= 0:
            return 0.0
        return (len(recent) - 1) / (span / 60.0)

    def reset(self) -> None:
        """Reset all monitor state."""
        with self._lock:
            self._state = MonitorState(max_alerts=self._state.max_alerts)
        self._detector.reset()


# ---------------------------------------------------------------------------
# Formatting helpers
# ---------------------------------------------------------------------------


def _format_duration(seconds: float) -> str:
    """Format *seconds* as a human-readable duration string.

    Examples: ``"0m"``, ``"5m"``, ``"2h 15m"``, ``"1d 3h 12m"``.
    """
    total_minutes = int(seconds) // 60
    if total_minutes < 60:
        return f"{total_minutes}m"
    hours = total_minutes // 60
    mins = total_minutes % 60
    if hours < 24:
        return f"{hours}h {mins}m"
    days = hours // 24
    hours = hours % 24
    return f"{days}d {hours}h {mins}m"


# ---------------------------------------------------------------------------
# CLI command implementation
# ---------------------------------------------------------------------------


def run_monitor(db_path: str, interval: float = 2.0) -> None:  # pragma: no cover
    """Poll an audit database and display a live dashboard.

    This is the implementation behind ``aegis monitor --db <path>``.

    Args:
        db_path: Path to the Aegis audit SQLite database.
        interval: Seconds between refreshes.
    """
    import os
    import sqlite3
    import sys

    from aegis.core.anomaly import AnomalyDetector as _AD

    monitor = AgentMonitor(detector=_AD(), max_alerts=10)
    last_id = 0

    conn = sqlite3.connect(db_path)
    conn.row_factory = sqlite3.Row

    try:
        while True:
            rows = conn.execute(
                "SELECT * FROM audit_log WHERE id > ? ORDER BY id",
                (last_id,),
            ).fetchall()

            for row in rows:
                row_id: int = row["id"]
                if row_id > last_id:
                    last_id = row_id
                action_type: str = row["action_type"]
                target: str = row["action_target"]
                agent_id: str = row["agent_id"] or "default"
                approval: str = row["approval"] or "auto"

                monitor.record_event(
                    action_type,
                    agent_id=agent_id,
                    decision=approval,
                    target=target,
                )

            # Clear + reprint
            if sys.platform == "win32":
                os.system("cls")
            else:
                os.system("clear")

            sys.stdout.write(monitor.render() + "\n")
            sys.stdout.flush()

            time.sleep(interval)
    except KeyboardInterrupt:
        print("\nMonitor stopped.")
    finally:
        conn.close()
