"""obol watch - live-refreshing terminal dashboard for agent monitoring.

An amazing, interactive dashboard that puts your mind at ease about your AI agents.
"""
import json
import os
import re
import select
import shutil
import signal
import sys
import time
from datetime import datetime, timezone

from obol.cli._base import (
    get_api,
    CHAR_FILLED,
    CHAR_EMPTY,
    CHAR_WARNING,
    CHAR_CHECK,
    CHAR_CROSS,
    CHAR_HLINE,
    CHAR_DLINE,
    UNICODE,
)


# ══════════════════════════════════════════════════════════════════════════════
# ANSI CODES
# ══════════════════════════════════════════════════════════════════════════════

_RESET = "\033[0m"
_BOLD = "\033[1m"
_DIM = "\033[2m"
_ITALIC = "\033[3m"
_UNDERLINE = "\033[4m"
_BLINK = "\033[5m"
_REVERSE = "\033[7m"

# Colors
_BLACK = "\033[30m"
_RED = "\033[31m"
_GREEN = "\033[32m"
_YELLOW = "\033[33m"
_BLUE = "\033[34m"
_MAGENTA = "\033[35m"
_CYAN = "\033[36m"
_WHITE = "\033[37m"

# Bright colors
_BRIGHT_BLACK = "\033[90m"
_BRIGHT_RED = "\033[91m"
_BRIGHT_GREEN = "\033[92m"
_BRIGHT_YELLOW = "\033[93m"
_BRIGHT_BLUE = "\033[94m"
_BRIGHT_MAGENTA = "\033[95m"
_BRIGHT_CYAN = "\033[96m"
_BRIGHT_WHITE = "\033[97m"

# Background
_BG_RED = "\033[41m"
_BG_GREEN = "\033[42m"
_BG_YELLOW = "\033[43m"
_BG_BLUE = "\033[44m"

# Control
_CLEAR = "\033[H\033[J"
_HIDE_CURSOR = "\033[?25l"
_SHOW_CURSOR = "\033[?25h"
_MOVE_HOME = "\033[H"

_USE_COLOR = sys.stdout.isatty()

# Box drawing
_TL = "\u250c" if UNICODE else "+"   # ┌
_TR = "\u2510" if UNICODE else "+"   # ┐
_BL = "\u2514" if UNICODE else "+"   # └
_BR = "\u2518" if UNICODE else "+"   # ┘
_H = "\u2500" if UNICODE else "-"    # ─
_V = "\u2502" if UNICODE else "|"    # │
_DOT = "\u25cf" if UNICODE else "*"  # ●
_CIRCLE = "\u25cb" if UNICODE else "o"  # ○
_ARROW_R = "\u25b6" if UNICODE else ">"  # ▶
_ARROW_D = "\u25bc" if UNICODE else "v"  # ▼
_SPARKLE = "\u2728" if UNICODE else "*"  # ✨
_SHIELD = "\u26a1" if UNICODE else "="  # ⚡
_LOCK = "\u26a0" if UNICODE else "!"    # ⚠ (warning as lock)


# ══════════════════════════════════════════════════════════════════════════════
# TERMINAL INPUT
# ══════════════════════════════════════════════════════════════════════════════

class TerminalInput:
    """Cross-platform non-blocking keyboard input."""

    def __init__(self):
        self._old_settings = None
        self._is_windows = sys.platform == "win32"
        self._is_tty = sys.stdin.isatty()

    def __enter__(self):
        if not self._is_tty:
            # Non-interactive mode (e.g., running in CI, redirected input)
            return self
        if not self._is_windows:
            try:
                import termios
                import tty
                self._old_settings = termios.tcgetattr(sys.stdin)
                tty.setcbreak(sys.stdin.fileno())
            except (termios.error, OSError):
                # Terminal doesn't support raw mode
                pass
        return self

    def __exit__(self, *args):
        if self._old_settings is not None:
            try:
                import termios
                termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self._old_settings)
            except (termios.error, OSError):
                pass

    def get_key(self, timeout=0.1):
        """Get a keypress. Returns None if no key pressed within timeout."""
        if not self._is_tty:
            # Non-interactive - just sleep and return None
            time.sleep(timeout)
            return None

        if self._is_windows:
            import msvcrt
            if msvcrt.kbhit():
                ch = msvcrt.getch()
                if ch in (b'\x00', b'\xe0'):
                    ch = msvcrt.getch()
                return ch.decode('utf-8', errors='ignore')
            time.sleep(timeout)
            return None
        else:
            try:
                ready, _, _ = select.select([sys.stdin], [], [], timeout)
                if ready:
                    ch = sys.stdin.read(1)
                    # Handle escape sequences (arrow keys, etc.)
                    if ch == '\x1b':
                        # Try to read more
                        ready2, _, _ = select.select([sys.stdin], [], [], 0.01)
                        if ready2:
                            ch2 = sys.stdin.read(1)
                            if ch2 == '[':
                                ch3 = sys.stdin.read(1)
                                return f'\x1b[{ch3}'
                    return ch
            except (OSError, ValueError):
                pass
            return None


# ══════════════════════════════════════════════════════════════════════════════
# RENDERING HELPERS
# ══════════════════════════════════════════════════════════════════════════════

def _c(code, text):
    """Apply ANSI color code."""
    if _USE_COLOR:
        return f"{code}{text}{_RESET}"
    return str(text)


def _strip_ansi_len(s):
    """Return visible length of a string (excluding ANSI escape sequences)."""
    return len(re.sub(r"\033\[[0-9;]*m", "", s))


def _pad(s, width):
    """Left-pad a string with ANSI-aware width calculation."""
    visible = _strip_ansi_len(s)
    if visible >= width:
        return s
    return s + " " * (width - visible)


def _rpad(s, width):
    """Right-align a string with ANSI-aware width calculation."""
    visible = _strip_ansi_len(s)
    if visible >= width:
        return s
    return " " * (width - visible) + s


def _center(s, width):
    """Center a string with ANSI-aware width calculation."""
    visible = _strip_ansi_len(s)
    if visible >= width:
        return s
    left = (width - visible) // 2
    right = width - visible - left
    return " " * left + s + " " * right


def _truncate(s, max_len, ellipsis="..."):
    """Truncate a plain string to max_len with ellipsis."""
    if len(s) <= max_len:
        return s
    if max_len < len(ellipsis) + 1:
        return s[:max_len]
    return s[: max_len - len(ellipsis)] + ellipsis


def _box(lines, width, title=None, style="thin"):
    """Wrap lines in a box. Returns list of strings."""
    chars = {
        "thin": (_TL, _TR, _BL, _BR, _H, _V),
        "double": ("╔", "╗", "╚", "╝", "═", "║") if UNICODE else ("+", "+", "+", "+", "=", "|"),
    }
    tl, tr, bl, br, h, v = chars.get(style, chars["thin"])
    inner = width - 2

    result = []
    if title:
        title_str = f" {title} "
        left_h = (inner - len(title_str)) // 2
        right_h = inner - left_h - len(title_str)
        result.append(tl + h * left_h + title_str + h * right_h + tr)
    else:
        result.append(tl + h * inner + tr)

    for line in lines:
        visible = _strip_ansi_len(line)
        if visible > inner:
            line = _truncate(line, inner)
            visible = inner
        padding = inner - visible
        result.append(v + line + " " * padding + v)

    result.append(bl + h * inner + br)
    return result


# ══════════════════════════════════════════════════════════════════════════════
# TRUST VISUALIZATION
# ══════════════════════════════════════════════════════════════════════════════

def _trust_color(score):
    """Return ANSI color code for a trust score."""
    if score is None:
        return _DIM
    if score <= 10:
        return _BRIGHT_RED
    if score <= 30:
        return _RED
    if score <= 50:
        return _YELLOW
    if score <= 70:
        return _WHITE
    if score <= 85:
        return _GREEN
    return _BRIGHT_GREEN


def _trust_label(score):
    """Return trust tier label."""
    if score is None:
        return "unknown"
    if score <= 10:
        return "quarantine"
    if score <= 30:
        return "restricted"
    if score <= 50:
        return "limited"
    if score <= 70:
        return "standard"
    if score <= 85:
        return "trusted"
    return "verified"


def _trust_bar(score, width=10):
    """Compact trust bar with gradient coloring."""
    if score is None:
        return _c(_DIM, CHAR_EMPTY * width)

    filled = int(score / 100 * width)
    filled = min(filled, width)
    empty = width - filled

    # Color the filled portion based on score
    color = _trust_color(score)
    bar = _c(color, CHAR_FILLED * filled) + _c(_DIM, CHAR_EMPTY * empty)
    return bar


def _trust_trend(current, previous):
    """Return trend indicator."""
    if previous is None or current is None:
        return _c(_DIM, "─")
    diff = current - previous
    if diff > 5:
        return _c(_BRIGHT_GREEN, "↑")
    if diff > 0:
        return _c(_GREEN, "↗")
    if diff < -5:
        return _c(_BRIGHT_RED, "↓")
    if diff < 0:
        return _c(_RED, "↘")
    return _c(_DIM, "→")


# ══════════════════════════════════════════════════════════════════════════════
# TIME FORMATTING
# ══════════════════════════════════════════════════════════════════════════════

def _time_ago(iso_ts):
    """Convert ISO timestamp to human relative time like '2m ago'."""
    if not iso_ts:
        return ""
    try:
        dt = datetime.fromisoformat(iso_ts.replace("Z", "+00:00"))
        delta = datetime.now(timezone.utc) - dt
        secs = int(delta.total_seconds())
        if secs < 0:
            secs = 0
        if secs < 60:
            return f"{secs}s"
        mins = secs // 60
        if mins < 60:
            return f"{mins}m"
        hrs = mins // 60
        if hrs < 24:
            return f"{hrs}h"
        days = hrs // 24
        return f"{days}d"
    except Exception:
        return ""


def _local_time(iso_ts):
    """Convert ISO timestamp to local HH:MM:SS."""
    if not iso_ts:
        return "??:??:??"
    try:
        dt = datetime.fromisoformat(iso_ts.replace("Z", "+00:00"))
        return dt.astimezone().strftime("%H:%M:%S")
    except Exception:
        return str(iso_ts)[:8] if iso_ts else "??:??:??"


def _seconds_since(iso_ts):
    """Get seconds since timestamp."""
    if not iso_ts:
        return float('inf')
    try:
        dt = datetime.fromisoformat(iso_ts.replace("Z", "+00:00"))
        return (datetime.now(timezone.utc) - dt).total_seconds()
    except Exception:
        return float('inf')


# ══════════════════════════════════════════════════════════════════════════════
# ALERT FORMATTING
# ══════════════════════════════════════════════════════════════════════════════

def _severity_color(sev):
    """ANSI color for alert severity."""
    sev = (sev or "").lower()
    if sev == "critical":
        return _BG_RED + _WHITE + _BOLD
    if sev == "high":
        return _BRIGHT_RED
    if sev in ("medium", "med"):
        return _YELLOW
    return _DIM


def _severity_label(sev):
    """Short severity label."""
    return {
        "critical": "CRIT",
        "high": "HIGH",
        "medium": "MED",
        "low": "LOW",
    }.get((sev or "").lower(), "?")


def _result_symbol(result):
    """Colored check/cross for event result."""
    if result in ("success", "allowed"):
        return _c(_GREEN, CHAR_CHECK)
    if result in ("error", "failure", "denied", "blocked"):
        return _c(_RED, CHAR_CROSS)
    return _c(_YELLOW, "~")


# ══════════════════════════════════════════════════════════════════════════════
# DATA FETCHING
# ══════════════════════════════════════════════════════════════════════════════

def _fetch_agents(api):
    """Fetch agent list. Returns list or None on error."""
    try:
        data = api.list_agents()
        return data.get("agents", [])
    except Exception:
        return None


def _fetch_alerts(api, limit=10):
    """Fetch recent open alerts. Returns dict or None on error."""
    try:
        return api.list_alerts(status="open", limit=limit)
    except Exception:
        return None


def _fetch_events(api, agent_id, limit=20):
    """Fetch recent events for an agent. Returns list or None on error."""
    try:
        data = api.get_events(agent_id, limit=limit)
        return data.get("events", [])
    except Exception:
        return None


def _fetch_all_events(api, limit=30):
    """Fetch recent events across all agents via summary endpoint."""
    try:
        # Use the summary endpoint to get recent events
        data = api.get_summary()
        return data.get("recent_events", [])
    except Exception:
        return None


# ══════════════════════════════════════════════════════════════════════════════
# CONFIG READING
# ══════════════════════════════════════════════════════════════════════════════

def _get_config():
    """Read config file."""
    config_path = os.path.expanduser("~/.obol/config.json")
    try:
        with open(config_path) as f:
            return json.load(f)
    except Exception:
        return {}


def _get_enforcement_mode():
    """Read enforcement mode from config."""
    return _get_config().get("enforcement_mode", "audit")


# ══════════════════════════════════════════════════════════════════════════════
# DASHBOARD STATE
# ══════════════════════════════════════════════════════════════════════════════

class DashboardState:
    """Manages all dashboard state."""

    # Screens
    SCREEN_OVERVIEW = "overview"
    SCREEN_AGENT = "agent"
    SCREEN_ALERTS = "alerts"
    SCREEN_EVENTS = "events"
    SCREEN_HELP = "help"

    def __init__(self, api, preferred_agent=None, show_events=True, interval=2.0):
        self.api = api
        self.preferred_agent = preferred_agent
        self.show_events = show_events
        self.interval = interval

        # Current screen
        self.screen = self.SCREEN_OVERVIEW

        # Data caches
        self.agents = None
        self.alerts_data = None
        self.events = None
        self.api_ok = False
        self.last_fetch = 0

        # Connection tracking
        self.was_connected = False
        self.disconnect_count = 0
        self.last_connected = 0

        # Previous data for trends
        self.prev_agents = {}

        # Selection state
        self.selected_agent_idx = 0
        self.selected_alert_idx = 0
        self.selected_event_idx = 0
        self.events_scroll = 0
        self.alerts_scroll = 0

        # Focused agent
        self.focused_agent_id = preferred_agent

        # Screen-specific data
        self.agent_detail_events = None

        # Message (shows temporarily)
        self.message = None
        self.message_time = 0

    def refresh_data(self):
        """Fetch fresh data from API."""
        # Store previous agent scores for trend calculation
        if self.agents:
            for agent in self.agents:
                self.prev_agents[agent.get("id")] = agent.get("trust_score")

        # Fetch agents
        agents = _fetch_agents(self.api)
        was_ok = self.api_ok
        self.api_ok = agents is not None

        # Track connection status changes
        if self.api_ok:
            if not self.was_connected:
                if self.disconnect_count > 0:
                    self.set_message("Reconnected to Obol API")
            self.was_connected = True
            self.last_connected = time.time()
        else:
            if self.was_connected:
                self.disconnect_count += 1
                self.set_message("Lost connection to Obol API")
            self.was_connected = False

        if agents is not None:
            self.agents = agents

        # Auto-select focused agent if not set
        if self.focused_agent_id is None and self.agents:
            # Pick highest trust score agent
            sorted_agents = sorted(
                self.agents,
                key=lambda a: a.get("trust_score") or 0,
                reverse=True
            )
            if sorted_agents:
                self.focused_agent_id = sorted_agents[0].get("id")

        # Validate selected agent index
        if self.agents and self.selected_agent_idx >= len(self.agents):
            self.selected_agent_idx = len(self.agents) - 1

        # Fetch alerts
        alerts_data = _fetch_alerts(self.api, limit=10)
        if alerts_data is not None:
            self.alerts_data = alerts_data

        # Fetch events for focused agent
        if self.show_events and self.focused_agent_id is not None:
            events = _fetch_events(self.api, self.focused_agent_id, limit=20)
            if events is not None:
                self.events = events

        # Fetch agent detail events if on agent screen
        if self.screen == self.SCREEN_AGENT and self.focused_agent_id:
            events = _fetch_events(self.api, self.focused_agent_id, limit=30)
            if events is not None:
                self.agent_detail_events = events

        self.last_fetch = time.time()

    def set_message(self, msg):
        """Set a temporary message."""
        self.message = msg
        self.message_time = time.time()

    def get_message(self):
        """Get current message if still valid (5 second timeout)."""
        if self.message and time.time() - self.message_time < 5:
            return self.message
        return None

    def get_selected_agent(self):
        """Get currently selected agent."""
        if not self.agents:
            return None
        if self.selected_agent_idx < len(self.agents):
            return self.agents[self.selected_agent_idx]
        return None

    def get_selected_alert(self):
        """Get currently selected alert."""
        if not self.alerts_data:
            return None
        alerts = self.alerts_data.get("alerts", [])
        if self.selected_alert_idx < len(alerts):
            return alerts[self.selected_alert_idx]
        return None

    def get_focused_agent(self):
        """Get the agent that is currently focused for events."""
        if not self.agents or not self.focused_agent_id:
            return None
        for agent in self.agents:
            if agent.get("id") == self.focused_agent_id:
                return agent
        return None

    def get_open_alert_count(self):
        """Get number of open alerts."""
        if not self.alerts_data:
            return 0
        return self.alerts_data.get("open_count", 0)

    def get_critical_alert_count(self):
        """Get number of critical alerts."""
        if not self.alerts_data:
            return 0
        return self.alerts_data.get("critical_count", 0)


# ══════════════════════════════════════════════════════════════════════════════
# SCREEN RENDERERS
# ══════════════════════════════════════════════════════════════════════════════

def _render_header(state, width):
    """Render the status header bar."""
    lines = []

    # Determine overall status
    open_alerts = state.get_open_alert_count()
    critical_alerts = state.get_critical_alert_count()

    if not state.api_ok:
        status = _c(_BG_RED + _WHITE + _BOLD, " DISCONNECTED ")
        status_msg = "Cannot reach Obol API"
    elif critical_alerts > 0:
        status = _c(_BG_RED + _WHITE + _BOLD, f" {_LOCK} {critical_alerts} CRITICAL ")
        status_msg = "Immediate attention required"
    elif open_alerts > 0:
        status = _c(_BG_YELLOW + _BLACK, f" {CHAR_WARNING} {open_alerts} ALERT{'S' if open_alerts != 1 else ''} ")
        status_msg = "Review recommended"
    else:
        status = _c(_BG_GREEN + _BLACK, f" {CHAR_CHECK} ALL CLEAR ")
        status_msg = "All agents operating normally"

    # Title line
    mode = _get_enforcement_mode()
    mode_indicator = {"audit": _c(_DIM, "audit"), "alert": _c(_YELLOW, "alert"), "enforce": _c(_GREEN, "enforce")}.get(mode, mode)
    now = datetime.now().strftime("%H:%M:%S")

    left = f"  {_c(_BOLD, 'OBOL')} {_c(_DIM, 'watch')}"
    right = f"{mode_indicator}  {_c(_DIM, now)}  "

    title_line = _pad(left, width - _strip_ansi_len(right)) + right
    lines.append(title_line)

    # Status line
    status_line = f"  {status}  {_c(_DIM, status_msg)}"
    lines.append(_pad(status_line, width))

    # Separator
    lines.append("  " + _c(_DIM, CHAR_DLINE * (width - 4)))

    return lines


def _render_agents_panel(state, panel_width, panel_height):
    """Render the agents panel."""
    lines = []
    agents = state.agents or []

    inner_w = panel_width - 4

    # Header
    agent_count = len(agents)
    header = f"AGENTS ({agent_count})"
    lines.append(_c(_BOLD, header))
    lines.append(_TL + _H * (panel_width - 2) + _TR)

    if not agents:
        if state.api_ok:
            msg = _c(_DIM, "No agents registered yet")
            hint = _c(_DIM, "Run: obol setup <key>")
            lines.append(_V + " " + _pad(msg, inner_w) + _V)
            lines.append(_V + " " + _pad(hint, inner_w) + _V)
        else:
            lines.append(_V + " " + _pad(_c(_RED, "Connection lost"), inner_w) + _V)
    else:
        # Show agents with selection indicator
        max_agents = panel_height - 4  # Account for header/footer
        for i, agent in enumerate(agents[:max_agents]):
            aid = str(agent.get("id", "?"))
            name = _truncate(agent.get("name", "unknown"), 12)
            score = agent.get("trust_score")
            score_str = f"{score:.0f}" if score is not None else "--"

            # Number key hint and focus indicator
            num_key = str(i + 1) if i < 9 else " "
            is_focused = agent.get("id") == state.focused_agent_id

            if is_focused:
                # This agent's events are being shown
                prefix = _c(_CYAN + _BOLD, f"{num_key}" + _ARROW_R)
            else:
                prefix = _c(_DIM, f"{num_key} ")

            # Trust bar
            bar = _trust_bar(score, width=6)

            # Trend indicator
            prev = state.prev_agents.get(agent.get("id"))
            trend = _trust_trend(score, prev)

            # Active indicator (events in last 5 min)
            last_event = agent.get("last_event_at")
            if last_event and _seconds_since(last_event) < 300:
                active = _c(_GREEN, _DOT)
            else:
                active = _c(_DIM, _CIRCLE)

            # Format: "1▶ 598  claude-code   72 ██████ ● →"
            cell = f"{prefix}{aid:<4} {name:<12} {score_str:>3} {bar} {active} {trend}"
            cell_vis = _strip_ansi_len(cell)
            padding = inner_w - cell_vis
            if padding < 0:
                padding = 0
            lines.append(_V + " " + cell + " " * padding + _V)

    # Footer
    lines.append(_BL + _H * (panel_width - 2) + _BR)

    return lines


def _render_alerts_panel(state, panel_width, panel_height):
    """Render the alerts panel."""
    lines = []
    alerts_data = state.alerts_data

    inner_w = panel_width - 4

    # Header
    open_count = state.get_open_alert_count()
    critical_count = state.get_critical_alert_count()

    if critical_count > 0:
        header = _c(_BRIGHT_RED + _BOLD, f"ALERTS ({open_count})")
    elif open_count > 0:
        header = _c(_YELLOW, f"ALERTS ({open_count})")
    else:
        header = _c(_DIM, "ALERTS (0)")

    lines.append(header)
    lines.append(_TL + _H * (panel_width - 2) + _TR)

    if alerts_data is None:
        lines.append(_V + " " + _pad(_c(_RED, "Connection lost"), inner_w) + _V)
    elif open_count == 0:
        lines.append(_V + " " + _pad(_c(_GREEN, "No open alerts"), inner_w) + _V)
        lines.append(_V + " " + _pad(_c(_DIM, "Your agents are behaving"), inner_w) + _V)
    else:
        alerts = alerts_data.get("alerts", [])
        max_alerts = panel_height - 4
        for i, alert in enumerate(alerts[:max_alerts]):
            sev = alert.get("severity", "")
            sev_label = _severity_label(sev)
            sev_colored = _c(_severity_color(sev), f"{sev_label:<4}")

            atype = _truncate(alert.get("alert_type", alert.get("title", "?")), inner_w - 14)
            ago = _time_ago(alert.get("created_at", ""))

            # Selection indicator
            if i == state.selected_alert_idx:
                prefix = _c(_CYAN, _ARROW_R + " ")
            else:
                prefix = "  "

            entry = f"{prefix}{sev_colored} {atype}"
            entry_vis = _strip_ansi_len(entry)
            ago_vis = len(ago)
            gap = inner_w - entry_vis - ago_vis
            if gap < 1:
                gap = 1

            full = entry + " " * gap + _c(_DIM, ago)
            full_vis = _strip_ansi_len(full)
            padding = inner_w - full_vis
            if padding < 0:
                padding = 0
            lines.append(_V + " " + full + " " * padding + _V)

    lines.append(_BL + _H * (panel_width - 2) + _BR)

    return lines


def _render_events_panel(state, width, max_rows):
    """Render the recent events section."""
    lines = []
    events = state.events
    focused = state.get_focused_agent()

    # Header
    if focused:
        agent_label = f"agent {focused.get('id')} - {focused.get('name', 'unknown')}"
    else:
        agent_label = "no agent selected"

    # Calculate rate from events
    rate_str = ""
    if events and len(events) >= 2:
        try:
            first_ts = events[-1].get("timestamp", "")
            last_ts = events[0].get("timestamp", "")
            dt_first = datetime.fromisoformat(first_ts.replace("Z", "+00:00"))
            dt_last = datetime.fromisoformat(last_ts.replace("Z", "+00:00"))
            span = (dt_last - dt_first).total_seconds()
            if span > 0:
                rate = len(events) / span * 60
                rate_str = f"{rate:.1f}/min"
        except Exception:
            pass

    left = f"  {_c(_BOLD, 'ACTIVITY')} {_c(_DIM, '(' + agent_label + ')')}"
    right = _c(_DIM, rate_str) if rate_str else ""
    header = _pad(left, width - _strip_ansi_len(right) - 2) + right
    lines.append(header)
    lines.append("  " + _c(_DIM, CHAR_HLINE * (width - 4)))

    if events is None:
        lines.append("  " + _c(_RED, "Connection lost"))
        return lines

    if not events:
        lines.append("  " + _c(_DIM, "No recent activity"))
        lines.append("  " + _c(_DIM, "Events will appear here when your agent acts"))
        return lines

    # Show events
    for event in events[:max_rows]:
        ts = _local_time(event.get("timestamp", ""))
        action = event.get("action", "?")
        result = event.get("result", "?")
        target = event.get("target", "") or ""

        time_part = _c(_DIM, ts)
        action_part = _c(_CYAN, f"{action:<14}")
        result_part = _result_symbol(result)

        # Anomaly indicator
        anomaly_flags = event.get("anomaly_flags") or []
        if anomaly_flags:
            anomaly_indicator = _c(_BRIGHT_RED, " " + CHAR_WARNING)
        else:
            anomaly_indicator = "  "

        # Special formatting for session events
        if action in ("session.start", "session.end"):
            target_part = _c(_DIM, "─" * 12 if UNICODE else "-" * 12)
        else:
            max_target = width - 48
            if max_target < 10:
                max_target = 10
            target_part = _truncate(target, max_target) if target else _c(_DIM, "-")

        lines.append(f"  {time_part}  {action_part} {result_part}{anomaly_indicator} {target_part}")

    return lines


def _render_footer(state, width):
    """Render the footer with keyboard shortcuts."""
    lines = []

    # Message if any
    msg = state.get_message()
    if msg:
        lines.append("")
        lines.append("  " + _c(_YELLOW, msg))
        lines.append("")

    # Screen-specific shortcuts
    if state.screen == state.SCREEN_OVERVIEW:
        keys = [
            ("[1-9]", "focus agent"),
            ("[Enter]", "details"),
            ("[a]", "alerts"),
            ("[e]", "events"),
            ("[r]", "refresh"),
            ("[q]", "quit"),
            ("[?]", "help"),
        ]
    elif state.screen == state.SCREEN_AGENT:
        keys = [
            ("[Esc]", "back"),
            ("[e]", "events"),
            ("[r]", "refresh"),
            ("[q]", "quit"),
        ]
    elif state.screen == state.SCREEN_ALERTS:
        keys = [
            ("[↑↓]", "select"),
            ("[Enter]", "resolve"),
            ("[f]", "false positive"),
            ("[Esc]", "back"),
            ("[q]", "quit"),
        ]
    elif state.screen == state.SCREEN_EVENTS:
        keys = [
            ("[↑↓]", "scroll"),
            ("[g/G]", "top/bottom"),
            ("[1-9]", "agent"),
            ("[Esc]", "back"),
            ("[q]", "quit"),
        ]
    elif state.screen == state.SCREEN_HELP:
        keys = [
            ("[Esc]", "back"),
            ("[q]", "quit"),
        ]
    else:
        keys = [("[q]", "quit")]

    parts = []
    for key, label in keys:
        parts.append(f"{_c(_BOLD, key)} {_c(_DIM, label)}")

    footer_line = "  " + "  ".join(parts)
    lines.append("  " + _c(_DIM, CHAR_HLINE * (width - 4)))
    lines.append(footer_line)

    return lines


def _render_help_screen(width, height):
    """Render the help screen."""
    lines = []

    title = f"  {_c(_BOLD, 'OBOL WATCH HELP')}"
    lines.append(title)
    lines.append("  " + _c(_DIM, CHAR_DLINE * (width - 4)))
    lines.append("")

    sections = [
        ("Navigation", [
            ("↑ / k", "Move selection up"),
            ("↓ / j", "Move selection down"),
            ("g / G", "Go to top / bottom (events view)"),
            ("Enter", "View details / confirm action"),
            ("Esc", "Go back / cancel"),
        ]),
        ("Agent Focus", [
            ("1-9", "Focus agent - shows that agent's events"),
            ("Enter", "View agent details (from overview)"),
        ]),
        ("Views", [
            ("a", "Alerts view - see and resolve alerts"),
            ("e", "Events view - scrollable event history"),
        ]),
        ("Actions", [
            ("r", "Refresh data immediately"),
            ("f", "Mark alert as false positive (alerts view)"),
            ("q", "Quit watch"),
        ]),
        ("Understanding Trust Scores", [
            ("90-100", "Verified - agent has strong trust history"),
            ("71-85", "Trusted - normal operation"),
            ("51-70", "Standard - typical starting point"),
            ("31-50", "Limited - some concerns detected"),
            ("11-30", "Restricted - significant anomalies"),
            ("0-10", "Quarantine - critical issues"),
        ]),
        ("Indicators", [
            (_c(_GREEN, _DOT), "Agent active (event in last 5 min)"),
            (_c(_DIM, _CIRCLE), "Agent idle"),
            (_c(_GREEN, CHAR_CHECK), "Success / Allowed"),
            (_c(_RED, CHAR_CROSS), "Error / Blocked"),
            (_c(_BRIGHT_RED, CHAR_WARNING), "Anomaly detected"),
        ]),
    ]

    for section_title, items in sections:
        lines.append(f"  {_c(_BOLD + _CYAN, section_title)}")
        for key, desc in items:
            key_vis = _strip_ansi_len(str(key))
            lines.append(f"    {_c(_BOLD, key):<{12 + (len(str(key)) - key_vis)}} {desc}")
        lines.append("")

    lines.append("  " + _c(_DIM, "Press Esc or ? to close this help"))

    return lines


def _render_agent_detail_screen(state, width, height):
    """Render detailed view for a single agent."""
    lines = []
    agent = state.get_focused_agent()

    if not agent:
        lines.append("  " + _c(_RED, "No agent selected"))
        return lines

    # Agent header
    aid = agent.get("id")
    name = agent.get("name", "unknown")
    score = agent.get("trust_score")
    tier = _trust_label(score)

    lines.append("")
    lines.append(f"  {_c(_BOLD, name)} {_c(_DIM, f'(ID: {aid})')}")
    lines.append("")

    # Trust score large display
    score_str = f"{score:.0f}" if score is not None else "--"
    score_color = _trust_color(score)
    lines.append(f"  Trust Score: {_c(score_color + _BOLD, score_str)}/100  ({_c(score_color, tier)})")
    lines.append(f"  {_trust_bar(score, width=30)}")
    lines.append("")

    # Agent stats
    lines.append(f"  {_c(_BOLD, 'Status')}")
    last_event = agent.get("last_event_at")
    if last_event:
        ago = _time_ago(last_event)
        if _seconds_since(last_event) < 300:
            status = _c(_GREEN, "Active") + f" (last event {ago} ago)"
        else:
            status = _c(_DIM, "Idle") + f" (last event {ago} ago)"
    else:
        status = _c(_DIM, "No events recorded")
    lines.append(f"    {status}")

    created = agent.get("created_at")
    if created:
        lines.append(f"    Created: {_local_time(created)}")
    lines.append("")

    # Recent events for this agent
    lines.append(f"  {_c(_BOLD, 'Recent Activity')}")
    events = state.agent_detail_events or []
    if not events:
        lines.append(f"    {_c(_DIM, 'No recent events')}")
    else:
        for event in events[:10]:
            ts = _local_time(event.get("timestamp", ""))
            action = event.get("action", "?")
            result = _result_symbol(event.get("result", "?"))
            target = _truncate(event.get("target", "") or "-", width - 40)
            lines.append(f"    {_c(_DIM, ts)}  {_c(_CYAN, action):<14} {result}  {target}")

    return lines


def _render_alerts_screen(state, width, height):
    """Render the alerts management screen."""
    lines = []

    lines.append("")
    lines.append(f"  {_c(_BOLD, 'OPEN ALERTS')}")
    lines.append("  " + _c(_DIM, CHAR_HLINE * (width - 4)))
    lines.append("")

    alerts_data = state.alerts_data
    if not alerts_data:
        lines.append("  " + _c(_RED, "Cannot fetch alerts"))
        return lines

    alerts = alerts_data.get("alerts", [])
    if not alerts:
        lines.append("  " + _c(_GREEN, CHAR_CHECK + " No open alerts"))
        lines.append("")
        lines.append("  " + _c(_DIM, "All clear! Your agents are behaving properly."))
        return lines

    # Show alerts with selection
    for i, alert in enumerate(alerts):
        sev = alert.get("severity", "")
        sev_label = _severity_label(sev)
        sev_colored = _c(_severity_color(sev), f"[{sev_label}]")

        atype = alert.get("alert_type", alert.get("title", "Unknown"))
        ago = _time_ago(alert.get("created_at", ""))
        agent_id = alert.get("agent_id")

        # Selection indicator
        if i == state.selected_alert_idx:
            prefix = _c(_CYAN + _BOLD, _ARROW_R + " ")
            is_selected = True
        else:
            prefix = "  "
            is_selected = False

        line1 = f"{prefix}{sev_colored} {_c(_BOLD if is_selected else '', atype)}"
        lines.append(line1)

        details = f"      Agent {agent_id}  |  {ago} ago"
        lines.append(_c(_DIM, details))

        # Show description for selected alert
        if is_selected:
            desc = alert.get("description") or alert.get("details") or ""
            if desc:
                wrapped = _truncate(desc, width - 8)
                lines.append(f"      {_c(_DIM, wrapped)}")
        lines.append("")

    return lines


def _render_events_screen(state, width, height):
    """Render the full events screen with scrolling."""
    lines = []

    lines.append("")
    lines.append(f"  {_c(_BOLD, 'EVENT HISTORY')}")

    # Agent selector
    if state.agents:
        agent_labels = []
        for i, agent in enumerate(state.agents[:9]):
            num = str(i + 1)
            name = _truncate(agent.get("name", "?"), 8)
            if agent.get("id") == state.focused_agent_id:
                agent_labels.append(_c(_CYAN + _BOLD, f"[{num}:{name}]"))
            else:
                agent_labels.append(_c(_DIM, f"[{num}:{name}]"))
        lines.append("  " + " ".join(agent_labels))

    lines.append("  " + _c(_DIM, CHAR_HLINE * (width - 4)))
    lines.append("")

    events = state.events or []
    if not events:
        lines.append("  " + _c(_DIM, "No events to display"))
        return lines

    # Show events with scroll indicator
    start = state.events_scroll
    visible = height - len(lines) - 5
    end = min(start + visible, len(events))

    if start > 0:
        lines.append(_c(_DIM, f"  ... {start} more events above ..."))

    for i, event in enumerate(events[start:end], start=start):
        ts = _local_time(event.get("timestamp", ""))
        action = event.get("action", "?")
        result = event.get("result", "?")
        target = event.get("target", "") or ""
        anomaly_flags = event.get("anomaly_flags") or []

        time_part = _c(_DIM, ts)
        action_part = _c(_CYAN, f"{action:<14}")
        result_part = _result_symbol(result)

        anomaly_str = ""
        if anomaly_flags:
            anomaly_str = _c(_BRIGHT_RED, f" {CHAR_WARNING} {', '.join(anomaly_flags[:2])}")

        max_target = width - 45 - _strip_ansi_len(anomaly_str)
        target_part = _truncate(target, max_target) if target else _c(_DIM, "-")

        lines.append(f"  {time_part}  {action_part} {result_part}  {target_part}{anomaly_str}")

    if end < len(events):
        lines.append(_c(_DIM, f"  ... {len(events) - end} more events below ..."))

    return lines


def _render_screen(state):
    """Compose and print the full screen."""
    cols, rows = shutil.get_terminal_size((80, 24))
    width = min(cols, 130)

    output_lines = []

    # Always show header
    output_lines.extend(_render_header(state, width))
    output_lines.append("")

    if state.screen == state.SCREEN_HELP:
        output_lines.extend(_render_help_screen(width, rows))
    elif state.screen == state.SCREEN_AGENT:
        output_lines.extend(_render_agent_detail_screen(state, width, rows))
    elif state.screen == state.SCREEN_ALERTS:
        output_lines.extend(_render_alerts_screen(state, width, rows))
    elif state.screen == state.SCREEN_EVENTS:
        output_lines.extend(_render_events_screen(state, width, rows))
    else:
        # Overview screen - side by side panels
        # Calculate panel dimensions
        left_width = min(40, width // 2)
        right_width = width - left_width - 3

        # Available height for panels
        available_height = rows - 12  # Reserve for header, events, footer
        panel_height = max(8, available_height // 2)

        left_lines = _render_agents_panel(state, left_width, panel_height)
        right_lines = _render_alerts_panel(state, right_width, panel_height)

        # Merge side by side
        max_panel_lines = max(len(left_lines), len(right_lines))
        for i in range(max_panel_lines):
            left = left_lines[i] if i < len(left_lines) else ""
            right = right_lines[i] if i < len(right_lines) else ""

            left_vis = _strip_ansi_len(left)
            padding = left_width - left_vis + 2
            if padding < 1:
                padding = 1
            output_lines.append(left + " " * padding + right)

        output_lines.append("")

        # Events panel if enabled
        if state.show_events:
            used = len(output_lines) + 5
            available = rows - used
            max_event_rows = max(3, min(available, 12))
            output_lines.extend(_render_events_panel(state, width, max_event_rows))

    # Footer
    output_lines.extend(_render_footer(state, width))

    # Print
    sys.stdout.write(_CLEAR)
    sys.stdout.write("\n".join(output_lines))
    sys.stdout.write("\n")
    sys.stdout.flush()


# ══════════════════════════════════════════════════════════════════════════════
# INPUT HANDLING
# ══════════════════════════════════════════════════════════════════════════════

def _handle_key(state, key):
    """Handle a keypress. Returns True to continue, False to quit."""
    if key is None:
        return True

    # Global keys
    if key in ('q', 'Q'):
        return False

    if key == '?':
        if state.screen == state.SCREEN_HELP:
            state.screen = state.SCREEN_OVERVIEW
        else:
            state.screen = state.SCREEN_HELP
        return True

    if key == 'r':
        state.set_message("Refreshing...")
        state.refresh_data()
        state.set_message("Refreshed")
        return True

    # Escape - go back
    if key == '\x1b' or key == '\x1b[D':  # Esc or Left arrow
        if state.screen != state.SCREEN_OVERVIEW:
            state.screen = state.SCREEN_OVERVIEW
        return True

    # Screen-specific handling
    if state.screen == state.SCREEN_OVERVIEW:
        return _handle_overview_key(state, key)
    elif state.screen == state.SCREEN_AGENT:
        return _handle_agent_key(state, key)
    elif state.screen == state.SCREEN_ALERTS:
        return _handle_alerts_key(state, key)
    elif state.screen == state.SCREEN_EVENTS:
        return _handle_events_key(state, key)
    elif state.screen == state.SCREEN_HELP:
        if key == '\x1b':
            state.screen = state.SCREEN_OVERVIEW
        return True

    return True


def _handle_overview_key(state, key):
    """Handle keys on overview screen."""
    agents = state.agents or []

    # Up/down navigation
    if key in ('\x1b[A', 'k'):  # Up
        if state.selected_agent_idx > 0:
            state.selected_agent_idx -= 1
        return True

    if key in ('\x1b[B', 'j'):  # Down
        if state.selected_agent_idx < len(agents) - 1:
            state.selected_agent_idx += 1
        return True

    # Enter - view agent detail
    if key in ('\n', '\r'):
        agent = state.get_selected_agent()
        if agent:
            state.focused_agent_id = agent.get("id")
            state.screen = state.SCREEN_AGENT
            state.refresh_data()
        return True

    # Shortcuts
    if key == 'a':
        state.screen = state.SCREEN_ALERTS
        return True

    if key == 'e':
        state.screen = state.SCREEN_EVENTS
        return True

    # Number keys to focus agent (shows their events)
    if key.isdigit() and key != '0':
        idx = int(key) - 1
        if idx < len(agents):
            state.selected_agent_idx = idx
            state.focused_agent_id = agents[idx].get("id")
            state.set_message(f"Focused: {agents[idx].get('name', 'agent')} (#{agents[idx].get('id')})")
            state.refresh_data()
        return True

    return True


def _handle_agent_key(state, key):
    """Handle keys on agent detail screen."""
    if key == 'e':
        state.screen = state.SCREEN_EVENTS
        return True

    return True


def _handle_alerts_key(state, key):
    """Handle keys on alerts screen."""
    alerts = (state.alerts_data or {}).get("alerts", [])

    # Up/down navigation
    if key in ('\x1b[A', 'k'):  # Up
        if state.selected_alert_idx > 0:
            state.selected_alert_idx -= 1
        return True

    if key in ('\x1b[B', 'j'):  # Down
        if state.selected_alert_idx < len(alerts) - 1:
            state.selected_alert_idx += 1
        return True

    # Enter - resolve alert
    if key in ('\n', '\r'):
        alert = state.get_selected_alert()
        if alert:
            try:
                state.api.resolve_alert(alert["id"], status="resolved")
                state.set_message(f"Alert {alert['id']} resolved")
                state.refresh_data()
            except Exception as e:
                state.set_message(f"Error: {e}")
        return True

    # f - mark as false positive
    if key == 'f':
        alert = state.get_selected_alert()
        if alert:
            try:
                state.api.resolve_alert(alert["id"], status="false_positive")
                state.set_message(f"Alert {alert['id']} marked as false positive")
                state.refresh_data()
            except Exception as e:
                state.set_message(f"Error: {e}")
        return True

    return True


def _handle_events_key(state, key):
    """Handle keys on events screen."""
    events = state.events or []
    agents = state.agents or []

    # Up/down scroll
    if key in ('\x1b[A', 'k'):  # Up
        if state.events_scroll > 0:
            state.events_scroll -= 1
        return True

    if key in ('\x1b[B', 'j'):  # Down
        if state.events_scroll < len(events) - 5:
            state.events_scroll += 1
        return True

    # Page up/down (using [ and ] as alternatives since escape sequences are complex)
    if key in ('[', '\x1b[5'):  # Page up
        state.events_scroll = max(0, state.events_scroll - 10)
        return True

    if key in (']', '\x1b[6'):  # Page down
        state.events_scroll = min(max(0, len(events) - 5), state.events_scroll + 10)
        return True

    # g - go to top, G - go to bottom
    if key == 'g':
        state.events_scroll = 0
        return True

    if key == 'G':
        state.events_scroll = max(0, len(events) - 10)
        return True

    # Number keys to switch agent
    if key.isdigit() and key != '0':
        idx = int(key) - 1
        if idx < len(agents):
            state.focused_agent_id = agents[idx].get("id")
            state.selected_agent_idx = idx
            state.events_scroll = 0
            state.set_message(f"Switched to: {agents[idx].get('name', 'agent')}")
            state.refresh_data()
        return True

    return True


# ══════════════════════════════════════════════════════════════════════════════
# MAIN LOOP
# ══════════════════════════════════════════════════════════════════════════════

def cmd_watch(args):
    """Main watch loop."""
    interval = getattr(args, "interval", 2.0)
    preferred_agent = getattr(args, "agent", None)
    show_events = not getattr(args, "no_events", False)

    # Check if we're in an interactive terminal
    is_interactive = sys.stdin.isatty() and sys.stdout.isatty()
    if not is_interactive:
        print("Warning: obol watch requires an interactive terminal.")
        print("For non-interactive use, try: obol status")
        return

    # Set up clean exit on Ctrl+C
    interrupted = False

    def _on_sigint(sig, frame):
        nonlocal interrupted
        interrupted = True

    signal.signal(signal.SIGINT, _on_sigint)

    # Get API client
    try:
        api = get_api()
    except SystemExit:
        print("Error: No API key configured. Run: obol setup <key>")
        return

    # Create state
    state = DashboardState(
        api=api,
        preferred_agent=preferred_agent,
        show_events=show_events,
        interval=interval,
    )

    # Initial data fetch
    state.refresh_data()

    # Hide cursor during dashboard display
    if _USE_COLOR:
        sys.stdout.write(_HIDE_CURSOR)
        sys.stdout.flush()

    try:
        with TerminalInput() as term:
            last_render = 0
            last_fetch = 0

            while not interrupted:
                now = time.time()

                # Fetch data periodically
                if now - last_fetch >= interval:
                    state.refresh_data()
                    last_fetch = now

                # Render
                if now - last_render >= 0.1:  # Render at most 10fps
                    _render_screen(state)
                    last_render = now

                # Handle input
                key = term.get_key(timeout=0.1)
                if key:
                    if not _handle_key(state, key):
                        break

    except (KeyboardInterrupt, SystemExit):
        pass
    finally:
        # Clean exit
        if _USE_COLOR:
            sys.stdout.write(_SHOW_CURSOR)
        sys.stdout.write(_CLEAR)
        sys.stdout.flush()
        print("Obol watch ended.")


def setup_parser(subparsers):
    """Add watch subparser."""
    parser = subparsers.add_parser(
        "watch",
        help="Live interactive dashboard for agent monitoring",
        description="""
Interactive terminal dashboard for monitoring your AI agents.

Shows trust scores, alerts, and recent activity in real-time.
Use arrow keys to navigate, Enter to view details, and ? for help.
        """,
    )
    parser.set_defaults(func=cmd_watch)
    parser.add_argument(
        "--agent", type=int, default=None,
        help="Focus on a specific agent ID",
    )
    parser.add_argument(
        "--interval", type=float, default=2.0,
        help="Data refresh interval in seconds (default: 2)",
    )
    parser.add_argument(
        "--no-events", action="store_true",
        help="Hide the activity panel",
    )
