"""obol tail - live event stream with visual formatting.

The showcase CLI feature. Pure ANSI, zero external dependencies.
"""
import json
import os
import shutil
import signal
import sys
import time
from datetime import datetime, timezone

from obol.cli._base import get_api, handle_api_error, error, UNICODE


# ---------------------------------------------------------------------------
# ANSI escape codes
# ---------------------------------------------------------------------------
_RST = "\033[0m"
_BOLD = "\033[1m"
_DIM = "\033[2m"
_UL = "\033[4m"

# Foreground
_BLACK = "\033[30m"
_RED = "\033[31m"
_GREEN = "\033[32m"
_YELLOW = "\033[33m"
_BLUE = "\033[34m"
_MAGENTA = "\033[35m"
_CYAN = "\033[36m"
_WHITE = "\033[37m"
_GRAY = "\033[90m"
_BR_RED = "\033[91m"
_BR_GREEN = "\033[92m"
_BR_YELLOW = "\033[93m"
_BR_BLUE = "\033[94m"
_BR_MAGENTA = "\033[95m"
_BR_CYAN = "\033[96m"
_BR_WHITE = "\033[97m"

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

# Line control
_CLEAR_LINE = "\033[2K"

# Characters
if UNICODE:
    _DOT = "\u25c9"       # ◉
    _CHECK = "\u2713"      # ✓
    _CROSS = "\u2717"      # ✗
    _WARN = "\u26a0"       # ⚠
    _HLINE = "\u2500"      # ─
    _DLINE = "\u2550"      # ═
    _BLOCK_FULL = "\u2588" # █
    _BLOCK_MED = "\u2593"  # ▓
    _BLOCK_LO = "\u2591"   # ░
    _TL = "\u250c"         # ┌
    _TR = "\u2510"         # ┐
    _BL = "\u2514"         # └
    _BR = "\u2518"         # ┘
    _VL = "\u2502"         # │
    _ELLIPSIS = "\u2026"   # …
    _ARROW = "\u25b6"      # ▶
else:
    _DOT = "*"
    _CHECK = "+"
    _CROSS = "x"
    _WARN = "!"
    _HLINE = "-"
    _DLINE = "="
    _BLOCK_FULL = "#"
    _BLOCK_MED = "="
    _BLOCK_LO = "-"
    _TL = "+"
    _TR = "+"
    _BL = "+"
    _BR = "+"
    _VL = "|"
    _ELLIPSIS = "~"
    _ARROW = ">"


# ---------------------------------------------------------------------------
# Color detection
# ---------------------------------------------------------------------------
_IS_TTY = sys.stdout.isatty()


def _c(code: str, text: str) -> str:
    """Apply ANSI color when outputting to a terminal."""
    if _IS_TTY:
        return f"{code}{text}{_RST}"
    return text


# ---------------------------------------------------------------------------
# Action color map
# ---------------------------------------------------------------------------
_ACTION_COLORS = {
    "Bash":          _YELLOW,
    "Read":          _CYAN,
    "Write":         _GREEN,
    "Edit":          _MAGENTA,
    "Grep":          _BLUE,
    "Glob":          _BLUE,
    "WebFetch":      _BR_CYAN,
    "WebSearch":     _BR_CYAN,
    "Task":          _BR_YELLOW,
    "TaskCreate":    _BR_YELLOW,
    "TaskUpdate":    _BR_YELLOW,
    "TaskList":      _BR_YELLOW,
    "TaskGet":       _BR_YELLOW,
    "NotebookEdit":  _MAGENTA,
    "Skill":         _BR_BLUE,
    "session.start": f"{_BOLD}{_BR_GREEN}",
    "session.end":   f"{_BOLD}{_BR_RED}",
}


def _action_color(action: str) -> str:
    """Return the ANSI color for an action type."""
    if action in _ACTION_COLORS:
        return _ACTION_COLORS[action]
    if action.startswith("mcp__"):
        return _BR_MAGENTA
    return _WHITE


# ---------------------------------------------------------------------------
# Smart target formatting
# ---------------------------------------------------------------------------

def _smart_target(target: str, max_width: int) -> str:
    """Intelligently truncate a target string for display."""
    if not target:
        return _c(_DIM, "-")
    # Strip common prefixes that add noise
    display = target

    # URLs: show domain + path, drop query strings
    if display.startswith(("http://", "https://")):
        try:
            from urllib.parse import urlparse
            u = urlparse(display)
            short = u.netloc + u.path.rstrip("/")
            if len(short) > max_width:
                short = short[:max_width - 1] + _ELLIPSIS
            return short
        except Exception:
            pass

    # File paths: collapse to ~/.../<parent>/<file>
    if display.startswith("/") and len(display) > max_width:
        home = os.path.expanduser("~")
        if display.startswith(home):
            display = "~" + display[len(home):]
        parts = display.split("/")
        if len(parts) > 3:
            display = _ELLIPSIS.join(["", parts[-2], parts[-1]])
            if len(display) > max_width:
                display = parts[-1]

    # Generic truncation
    if len(display) > max_width:
        display = display[:max_width - 1] + _ELLIPSIS

    return display


def _smart_bash_target(target: str, max_width: int) -> str:
    """Format a Bash command for display. First line only, truncated."""
    if not target:
        return _c(_DIM, "-")
    # Take only first line of multi-line commands
    first_line = target.split("\n")[0].strip()
    if len(first_line) > max_width:
        first_line = first_line[:max_width - 1] + _ELLIPSIS
    return first_line


# ---------------------------------------------------------------------------
# Rate calculation
# ---------------------------------------------------------------------------

class _RateTracker:
    """Track events/minute using a sliding window."""

    def __init__(self, window: int = 60):
        self._window = window
        self._timestamps: list[float] = []

    def tick(self, count: int = 1):
        now = time.monotonic()
        for _ in range(count):
            self._timestamps.append(now)

    def rate(self) -> float:
        now = time.monotonic()
        cutoff = now - self._window
        self._timestamps = [t for t in self._timestamps if t > cutoff]
        return len(self._timestamps) * (60.0 / self._window)


# ---------------------------------------------------------------------------
# Stats tracker
# ---------------------------------------------------------------------------

class _Stats:
    """Accumulate event statistics."""

    def __init__(self):
        self.total = 0
        self.by_action: dict[str, int] = {}
        self.errors = 0
        self.warnings = 0
        self.sessions = 0
        self.api_errors = 0
        self.start_time = time.monotonic()

    def record(self, event: dict):
        self.total += 1
        action = event.get("action", "?")
        self.by_action[action] = self.by_action.get(action, 0) + 1
        result = event.get("result", "")
        if result in ("error", "failure", "denied"):
            self.errors += 1
        if action in ("session.start", "session.end"):
            self.sessions += 1
        patterns = event.get("detected_patterns") or event.get("metadata", {}).get("detected_patterns", [])
        if patterns:
            self.warnings += 1

    def elapsed(self) -> str:
        secs = int(time.monotonic() - self.start_time)
        if secs < 60:
            return f"{secs}s"
        if secs < 3600:
            return f"{secs // 60}m {secs % 60}s"
        return f"{secs // 3600}h {(secs % 3600) // 60}m"


# ---------------------------------------------------------------------------
# Formatters
# ---------------------------------------------------------------------------

def _parse_timestamp(ts: str) -> str:
    """Parse ISO timestamp and return local HH:MM:SS."""
    try:
        dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
        return dt.astimezone().strftime("%H:%M:%S")
    except (ValueError, AttributeError, TypeError):
        return str(ts)[:8] if ts else "??:??:??"


def _mini_bar(rate: float, width: int = 10) -> str:
    """Render a small activity bar for the header."""
    max_rate = 120.0  # events/min for a full bar
    fill = min(int((rate / max_rate) * width), width)
    bar = _BLOCK_FULL * fill + _BLOCK_LO * (width - fill)
    return bar


def _result_icon(result: str) -> str:
    """Colored result indicator."""
    if result in ("success", "ok"):
        return _c(_GREEN, f" {_CHECK} ")
    if result in ("error", "failure", "denied"):
        return _c(_RED, f" {_CROSS} ")
    if result in ("warning", "blocked"):
        return _c(_BR_YELLOW, f" {_WARN} ")
    return _c(_DIM, " - ")


def _is_tamper_event(event: dict) -> bool:
    """Check if this is a tamper/integrity alert."""
    action = event.get("action", "")
    meta = event.get("metadata", {})
    if "tamper" in action.lower() or "integrity" in action.lower():
        return True
    if meta.get("tamper") or meta.get("integrity_violation"):
        return True
    return False


def _is_session_event(action: str) -> bool:
    return action in ("session.start", "session.end")


def _format_event(event: dict, term_width: int) -> str:
    """Format a single event into a colored line."""
    ts = _parse_timestamp(event.get("timestamp", ""))
    action = event.get("action", "?")
    target = event.get("target", "") or ""
    result = event.get("result", "?")
    meta = event.get("metadata", {}) or {}
    patterns = event.get("detected_patterns") or meta.get("detected_patterns", [])

    # Column widths
    ts_width = 10        # " HH:MM:SS "
    action_width = 13    # " Action      "
    result_width = 5     # " X "
    margin = 2           # left pad
    # remaining for target + optional patterns
    fixed = margin + ts_width + action_width + result_width
    target_budget = max(20, term_width - fixed - 2)

    # Patterns suffix
    pattern_suffix = ""
    if patterns:
        tags = ", ".join(patterns[:3])
        pattern_suffix = f"  [{tags}]"
        target_budget -= len(pattern_suffix) + 2

    # Smart target based on action
    if action == "Bash":
        display_target = _smart_bash_target(target, target_budget)
    else:
        display_target = _smart_target(target, target_budget)

    # Build the line
    parts = []

    # Timestamp (dim gray)
    parts.append(_c(_GRAY, f" {ts}"))

    # Action (colored, fixed width)
    color = _action_color(action)
    # For display, strip mcp__ prefix but keep it recognizable
    display_action = action
    if action.startswith("mcp__"):
        # mcp__server__tool -> server:tool
        segs = action.split("__")
        if len(segs) >= 3:
            display_action = f"{segs[1]}:{segs[2]}"
        else:
            display_action = action[5:]
    padded_action = f"  {display_action:<11}"
    parts.append(_c(color, padded_action))

    # Result icon
    if patterns and result in ("success", "ok"):
        parts.append(_c(_BR_YELLOW, f" {_WARN} "))
    else:
        parts.append(_result_icon(result))

    # Tamper events get red background
    if _is_tamper_event(event):
        parts.append(_c(f"{_BG_RED}{_BR_WHITE}{_BOLD}", f" {display_target} "))
    else:
        parts.append(f" {display_target}")

    # Pattern tags
    if patterns:
        parts.append(_c(_BR_YELLOW, f"  [{', '.join(patterns[:3])}]"))

    return "".join(parts)


def _format_compact(event: dict) -> str:
    """Compact format for piping -- no colors, tab-separated."""
    ts = _parse_timestamp(event.get("timestamp", ""))
    action = event.get("action", "?")
    target = event.get("target", "") or "-"
    result = event.get("result", "?")
    meta = event.get("metadata", {}) or {}
    patterns = event.get("detected_patterns") or meta.get("detected_patterns", [])
    pat_str = ",".join(patterns) if patterns else ""
    return f"{ts}\t{action}\t{result}\t{target}\t{pat_str}"


def _session_separator(action: str, term_width: int) -> str:
    """Draw a visual separator for session boundaries."""
    if action == "session.start":
        label = " session started "
        color = _BR_GREEN
    else:
        label = " session ended "
        color = _BR_RED

    pad = max(0, term_width - len(label) - 4)
    left = pad // 6
    right = pad - left
    line = f" {_HLINE * left}{label}{_HLINE * right}"
    return _c(f"{_BOLD}{color}", line)


def _stats_line(stats: _Stats, term_width: int) -> str:
    """Render the periodic stats footer."""
    # Top actions by count
    top = sorted(stats.by_action.items(), key=lambda x: -x[1])[:5]
    segments = [f"{a}: {n}" for a, n in top]
    inner = f" {stats.total} events {_VL} " + f" {_VL} ".join(segments)
    if stats.errors:
        inner += f" {_VL} errors: {stats.errors}"
    if stats.warnings:
        inner += f" {_VL} flagged: {stats.warnings}"

    pad = max(0, term_width - len(inner) - 4)
    left = 3
    right = max(0, pad - left)
    line = f" {_HLINE * left}{inner} {_HLINE * right}"
    return _c(_DIM, line)


# ---------------------------------------------------------------------------
# Header bar
# ---------------------------------------------------------------------------

def _header_bar(agent_id: int, rate: float, trust: float, mode: str,
                connected: bool, term_width: int) -> str:
    """Render the refreshing header bar."""
    # Connection dot
    if connected:
        dot = _c(f"{_BOLD}{_BR_GREEN}", f" {_DOT}")
    else:
        dot = _c(f"{_BOLD}{_BR_RED}", f" {_DOT}")

    title = _c(f"{_BOLD}{_BR_WHITE}", " OBOL TAIL")
    agent = _c(_DIM, f"  agent {agent_id}")

    # Activity bar
    bar = _mini_bar(rate)
    rate_str = f"{rate:.0f}"
    activity = _c(_CYAN, f"  {bar}") + _c(_DIM, f" {rate_str} evt/min")

    # Trust score
    if trust is not None:
        t = int(trust)
        if t >= 70:
            t_color = _BR_GREEN
        elif t >= 40:
            t_color = _BR_YELLOW
        else:
            t_color = _BR_RED
        trust_part = _c(_DIM, "  trust: ") + _c(f"{_BOLD}{t_color}", str(t))
    else:
        trust_part = _c(_DIM, "  trust: ") + _c(_DIM, "--")

    # Mode
    mode_colors = {"audit": _BR_BLUE, "alert": _BR_YELLOW, "enforce": _BR_RED}
    m_color = mode_colors.get(mode, _WHITE)
    mode_part = _c(_DIM, "  mode: ") + _c(m_color, mode)

    return f"{dot}{title}{agent}{activity}{trust_part}{mode_part}"


# ---------------------------------------------------------------------------
# Startup banner
# ---------------------------------------------------------------------------

def _startup_banner(agent_id: int, mode: str, interval: float,
                    action_filter: str, term_width: int) -> str:
    """Render the startup box."""
    inner_width = max(37, min(50, term_width - 6))

    def pad_line(text: str) -> str:
        # Strip ANSI for length calculation
        stripped = text
        import re
        stripped = re.sub(r'\033\[[0-9;]*m', '', text)
        padding = inner_width - len(stripped)
        return f"  {_VL}  {text}{' ' * max(0, padding)}  {_VL}"

    lines = []
    lines.append(f"  {_TL}{_HLINE * (inner_width + 4)}{_TR}")

    dot_line = _c(f"{_BOLD}{_BR_GREEN}", _DOT) + _c(f"{_BOLD}{_BR_WHITE}", f" Obol Tail") + _c(_DIM, f" {_VL} Agent {agent_id}")
    lines.append(pad_line(dot_line))

    mode_str = _c(_DIM, "Mode: ") + _c(_BR_CYAN, mode)
    poll_str = _c(_DIM, f" {_VL} Polling: ") + _c(_BR_CYAN, f"{interval:.0f}s" if interval == int(interval) else f"{interval}s")
    lines.append(pad_line(f"{mode_str}{poll_str}"))

    if action_filter:
        filter_str = _c(_DIM, "Filter: ") + _c(_BR_YELLOW, action_filter)
        lines.append(pad_line(filter_str))

    lines.append(pad_line(_c(_DIM, "Ctrl+C to exit")))
    lines.append(f"  {_BL}{_HLINE * (inner_width + 4)}{_BR}")
    return "\n".join(lines)


# ---------------------------------------------------------------------------
# Graceful shutdown summary
# ---------------------------------------------------------------------------

def _shutdown_summary(stats: _Stats) -> str:
    """Render the final stats on Ctrl+C."""
    lines = []
    lines.append("")
    lines.append(_c(f"{_BOLD}{_BR_WHITE}", f"  {_DOT} Session Summary"))
    lines.append(_c(_DIM, f"  {_HLINE * 40}"))
    lines.append(f"  Duration:  {_c(_BR_CYAN, stats.elapsed())}")
    lines.append(f"  Events:    {_c(_BR_WHITE, str(stats.total))}")

    if stats.by_action:
        top = sorted(stats.by_action.items(), key=lambda x: -x[1])[:6]
        breakdown = "  ".join(
            f"{_c(_action_color(a), a)}{_c(_DIM, ':')} {n}"
            for a, n in top
        )
        lines.append(f"  Actions:   {breakdown}")

    if stats.errors:
        lines.append(f"  Errors:    {_c(_BR_RED, str(stats.errors))}")
    if stats.warnings:
        lines.append(f"  Flagged:   {_c(_BR_YELLOW, str(stats.warnings))}")
    if stats.api_errors:
        lines.append(f"  API drops: {_c(_RED, str(stats.api_errors))}")

    lines.append(_c(_DIM, f"  {_HLINE * 40}"))
    lines.append("")
    return "\n".join(lines)


# ---------------------------------------------------------------------------
# Config reader (lightweight, no import of config module)
# ---------------------------------------------------------------------------

def _read_config() -> dict:
    """Read ~/.obol/config.json without importing config module."""
    path = os.path.expanduser("~/.obol/config.json")
    if os.path.exists(path):
        try:
            with open(path) as f:
                return json.load(f)
        except Exception:
            pass
    return {}


# ---------------------------------------------------------------------------
# Main command
# ---------------------------------------------------------------------------

@handle_api_error
def cmd_tail(args):
    api = get_api()
    agent_id = args.agent
    interval = args.interval
    action_filter = getattr(args, "action", None)
    no_header = getattr(args, "no_header", False)
    compact = getattr(args, "compact", False)

    # If compact mode, disable all decoration
    if compact:
        no_header = True

    # Read enforcement mode from config
    config = _read_config()
    mode = config.get("enforcement_mode", "audit")

    # Terminal width
    term_width = shutil.get_terminal_size((80, 24)).columns

    # State
    stats = _Stats()
    rate_tracker = _RateTracker()
    trust_score = None
    connected = True
    last_stats_print = time.monotonic()
    stats_interval = 30  # seconds between stats lines
    header_printed = False

    # Fetch initial trust score
    try:
        score_data = api.get_score(agent_id)
        trust_score = score_data.get("trust_score") or score_data.get("score")
    except Exception:
        trust_score = None

    # Graceful Ctrl+C
    interrupted = False

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

    signal.signal(signal.SIGINT, _on_sigint)

    # Startup banner (unless compact/no-header)
    if not compact:
        banner = _startup_banner(agent_id, mode, interval, action_filter, term_width)
        sys.stdout.write(banner + "\n")
        sys.stdout.flush()

    # Initial fetch for recent context
    try:
        data = api.get_events(agent_id, limit=15)
        events = data.get("events", [])
        connected = True
    except Exception:
        events = []
        connected = False
        stats.api_errors += 1

    if action_filter:
        events = [e for e in events if e.get("action") == action_filter]

    # Track last seen event ID
    last_seen_id = 0
    if events:
        last_seen_id = max(e.get("id", 0) for e in events)
        # Print initial batch (oldest first)
        for event in reversed(events):
            action = event.get("action", "")
            if _is_session_event(action) and not compact:
                sys.stdout.write(_session_separator(action, term_width) + "\n")
            if compact:
                sys.stdout.write(_format_compact(event) + "\n")
            else:
                sys.stdout.write(_format_event(event, term_width) + "\n")
            stats.record(event)
            rate_tracker.tick()
        sys.stdout.flush()

    # Separator after initial context
    if events and not compact:
        sys.stdout.write(_c(_DIM, f" {_HLINE * 3} live {_HLINE * (term_width - 10)}") + "\n")
        sys.stdout.flush()

    # Print initial header
    if not no_header:
        rate = rate_tracker.rate()
        hdr = _header_bar(agent_id, rate, trust_score, mode, connected, term_width)
        sys.stdout.write(hdr + "\n")
        sys.stdout.flush()
        header_printed = True

    # --- Poll loop ---
    events_since_header = 0
    polls_since_score = 0

    while not interrupted:
        try:
            time.sleep(interval)
        except (KeyboardInterrupt, SystemExit):
            break

        if interrupted:
            break

        # Refresh terminal width
        term_width = shutil.get_terminal_size((80, 24)).columns

        # Fetch new events
        try:
            data = api.get_events(agent_id, limit=20)
            events = data.get("events", [])
            connected = True
        except Exception:
            connected = False
            stats.api_errors += 1
            # Print header to show disconnected state
            if not no_header:
                rate = rate_tracker.rate()
                hdr = _header_bar(agent_id, rate, trust_score, mode, connected, term_width)
                sys.stdout.write(f"{_CLEAR_LINE}\r{hdr}\n")
                sys.stdout.flush()
            continue

        # Filter to new events
        new_events = [e for e in events if e.get("id", 0) > last_seen_id]

        if action_filter:
            new_events = [e for e in new_events if e.get("action") == action_filter]

        if new_events:
            last_seen_id = max(e.get("id", 0) for e in new_events)
            rate_tracker.tick(len(new_events))

            for event in sorted(new_events, key=lambda e: e.get("id", 0)):
                action = event.get("action", "")
                stats.record(event)
                events_since_header += 1

                # Session separators
                if _is_session_event(action) and not compact:
                    sys.stdout.write(_session_separator(action, term_width) + "\n")

                # The event line
                if compact:
                    sys.stdout.write(_format_compact(event) + "\n")
                else:
                    sys.stdout.write(_format_event(event, term_width) + "\n")

            sys.stdout.flush()

        # Periodic stats line (every stats_interval seconds)
        now = time.monotonic()
        if not compact and stats.total > 0 and (now - last_stats_print) >= stats_interval:
            sys.stdout.write(_stats_line(stats, term_width) + "\n")
            sys.stdout.flush()
            last_stats_print = now

        # Refresh header periodically (every 5 poll cycles if there were events,
        # or every 10 cycles regardless)
        polls_since_score += 1
        should_refresh_header = (
            not no_header
            and (events_since_header >= 5 or polls_since_score >= 10)
        )

        if should_refresh_header:
            # Refresh trust score every ~20s
            if polls_since_score >= 10:
                try:
                    score_data = api.get_score(agent_id)
                    trust_score = score_data.get("trust_score") or score_data.get("score")
                except Exception:
                    pass
                polls_since_score = 0

            rate = rate_tracker.rate()
            hdr = _header_bar(agent_id, rate, trust_score, mode, connected, term_width)
            sys.stdout.write(hdr + "\n")
            sys.stdout.flush()
            events_since_header = 0

    # --- Shutdown ---
    if compact:
        # Clean exit for piped output
        return

    sys.stdout.write(_shutdown_summary(stats))
    sys.stdout.flush()


# ---------------------------------------------------------------------------
# Subparser registration
# ---------------------------------------------------------------------------

def setup_parser(subparsers):
    """Add tail subparser."""
    parser = subparsers.add_parser(
        "tail",
        help="Live event stream with visual formatting",
    )
    parser.set_defaults(func=cmd_tail)
    parser.add_argument(
        "--agent", type=int, required=True,
        help="Agent ID to follow",
    )
    parser.add_argument(
        "--interval", type=float, default=2.0,
        help="Poll interval in seconds (default: 2)",
    )
    parser.add_argument(
        "--action",
        help="Filter to a single action type (e.g. Bash, Read, Edit)",
    )
    parser.add_argument(
        "--no-header", action="store_true",
        help="Hide the refreshing header bar",
    )
    parser.add_argument(
        "--compact", action="store_true",
        help="Tab-separated output with no colors (for piping)",
    )
