"""
Display layer — renders server events in the terminal using Rich.

Translates WebSocket events into beautiful CLI output that matches
the local CLI experience exactly (verifagent parity).
"""

from __future__ import annotations

import hashlib
import os
import re
import sys
import threading
import time
from pathlib import Path
from typing import Any, Dict

from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.text import Text

console = Console()

# ── PM / specialist log formatting (matches verifagent/agent.py) ──
_RE_TURN = re.compile(r"^\[(PM Nadia|RTL PM Marcus)\] Turn (\d+)")
_RE_DISPATCH = re.compile(r"^\[(PM|RTL PM)\] Dispatching → (.+)")
_RE_STARTING = re.compile(r"^\[([^\]]+)\] Starting\.\.\.")
_RE_DONE_LOG = re.compile(r"^\[([^\]]+)\] Done — (.+)")
_RE_TOOL_CALL = re.compile(r"^  \[([^\]]+)\] (.+)")
_RE_AGENT_TEXT = re.compile(r"^  (?:>|💬) (.+)", re.DOTALL)
_RE_WARNING = re.compile(r"^  \[(?:WARNING|⚠️)")
_RE_PAUSE = re.compile(r"Reached \d+ tool iterations")
_RE_USER_STOPPED = re.compile(r"\] User stopped")
_RE_USER_FEEDBACK = re.compile(r"\] User feedback noted")
_RE_STAGE_PROGRESS = re.compile(r"^\[(\d+)/(\d+)\] (.+)")
_RE_SIM_RESULT = re.compile(r"^\s+(✓|✗|\?)\s+(.+)")
_RE_HEADLESS_AUTO = re.compile(r"\(headless mode")

_SPECIALIST_COLORS = {
    "Arjun": "blue", "Maya": "magenta", "Kai": "cyan",
    "Priya": "green", "Leo": "yellow", "Sam": "bright_black",
    "Ravi": "red", "Zara": "bright_cyan", "Vikram": "blue",
    "Rajan": "magenta", "Meera": "cyan", "Anika": "green",
    "Nadia": "bold yellow", "Marcus": "bold cyan",
    "Tariq": "bright_magenta",
    "Design Architect": "blue", "UVM Engineer": "magenta",
    "SVA Engineer": "cyan", "Coverage Engineer": "green",
    "RAL Engineer": "yellow", "Build Engineer": "bright_black",
    "Compile Engineer": "red", "Simple TB Engineer": "bright_cyan",
    "RTL Architect": "blue", "RTL Module Engineer": "magenta",
    "RTL Integration Engineer": "cyan", "RTL Lint Engineer": "red",
    "RTL Simulation Engineer": "green",
    "Questa Convergence Engineer": "bright_magenta",
}

# ── File hash tracking for auto-sync ─────────────────────────
# Maps relative path (str) → MD5 hash of content (str).
# Updated when the server delivers files; checked before each
# user message to detect local edits that need uploading.
_file_hashes: Dict[str, str] = {}


def _hash_content(content: str) -> str:
    """Compute a fast hash of file content for change detection."""
    return hashlib.md5(content.encode()).hexdigest()


def get_tracked_files() -> Dict[str, str]:
    """Return the current file hash registry (rel_path → hash)."""
    return _file_hashes


def update_file_hash(rel_path: str, content: str) -> None:
    """Update the hash for a tracked file (called after sync upload)."""
    _file_hashes[rel_path] = _hash_content(content)


def remove_file_hash(rel_path: str) -> None:
    """Stop tracking a file (called after deletion)."""
    _file_hashes.pop(rel_path, None)

# ── Spinner for long-running stages ──────────────────────────
_SPIN_CHARS = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
_spin_stop = threading.Event()
_spin_thread = None
_spin_msg = ""


def _get_term_width() -> int:
    try:
        return os.get_terminal_size().columns
    except (OSError, ValueError):
        return 80


def _clear_line():
    """Clear the current terminal line using ANSI escape (robust)."""
    sys.stdout.write("\r\033[2K\r")
    sys.stdout.flush()


def _spinner_loop():
    global _spin_msg
    ci = 0
    t0 = time.time()
    # Brief delay to avoid flicker on rapid events
    if _spin_stop.wait(0.4):
        return
    while not _spin_stop.is_set():
        elapsed = time.time() - t0
        c = _SPIN_CHARS[ci % len(_SPIN_CHARS)]
        dots = "." * (1 + int(elapsed) % 3)
        tw = _get_term_width()
        line = f"      {c} {_spin_msg}{dots}  ({elapsed:.0f}s)"
        # Truncate to terminal width to prevent wrapping
        sys.stdout.write(f"\r{line[:tw - 1]}")
        sys.stdout.flush()
        ci += 1
        _spin_stop.wait(0.08)
    # Clear spinner line (ANSI erase — works regardless of terminal width)
    _clear_line()


def _start_spinner(msg: str):
    global _spin_thread, _spin_msg
    _stop_spinner()
    _spin_msg = msg
    _spin_stop.clear()
    _spin_thread = threading.Thread(target=_spinner_loop, daemon=True)
    _spin_thread.start()


def _stop_spinner():
    global _spin_thread
    was_running = _spin_thread is not None
    _spin_stop.set()
    if _spin_thread and _spin_thread.is_alive():
        _spin_thread.join(timeout=1)
    _spin_thread = None
    if was_running:
        _clear_line()


# ── Result line detection (don't spin after results) ─────────
_RESULT_INDICATORS = ("✓", "✗", "Generated:", "Compilation:", "Lint results:",
                      "Coverage now:", "PASSED", "FAILED", "════", "✅", "❌")


def _is_result_line(msg: str) -> bool:
    return any(ind in msg.strip() for ind in _RESULT_INDICATORS)


def _extract_spin_label(msg: str) -> str:
    stripped = msg.strip()
    if stripped.startswith("[") and "]" in stripped:
        stripped = stripped[stripped.index("]") + 1:].strip()
    if stripped.endswith("..."):
        stripped = stripped[:-3]
    return stripped[:60]


# ── Smart text buffering (suppress filler before tool calls) ──
# Mirrors verifagent's specialist pipeline text handling:
#   - Buffer text_delta chunks, only display once past min threshold
#   - If a tool_start/tool_request arrives while buffered, discard (filler)
_text_buf: list = []
_text_total: list = []
_in_text_stream = False
_TEXT_FLUSH_THRESHOLD = 60
_TEXT_MIN_DISPLAY = 30


def _flush_text_buf():
    """Flush buffered text_delta chunks to stdout (raw, like verifagent)."""
    global _in_text_stream
    if not _text_buf:
        return
    raw = "".join(_text_buf)
    _text_buf.clear()
    if not raw.strip():
        return
    if not _in_text_stream:
        sys.stdout.write("\n")
        _in_text_stream = True
    sys.stdout.write(raw)
    sys.stdout.flush()


def _end_text_stream():
    global _in_text_stream
    total_text = "".join(_text_total).strip()
    _text_total.clear()
    if not _in_text_stream and len(total_text) < _TEXT_MIN_DISPLAY:
        _text_buf.clear()
        _in_text_stream = False
        return
    leftover = "".join(_text_buf)
    _text_buf.clear()
    if leftover.strip():
        if not _in_text_stream:
            sys.stdout.write("\n")
        sys.stdout.write(leftover)
    if _in_text_stream:
        sys.stdout.write("\n")
        sys.stdout.flush()
    _in_text_stream = False


def _suppress_text_buf():
    """Discard buffered text (filler before tool calls)."""
    global _in_text_stream
    _text_buf.clear()
    _text_total.clear()
    if _in_text_stream:
        sys.stdout.write("\n")
        sys.stdout.flush()
    _in_text_stream = False


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

def display_welcome(session_info: Dict[str, Any]) -> None:
    """Display welcome message after connecting — matches the verifagent CLI banner."""
    from sigmanticai import __version__

    plan = session_info.get("plan", "free")
    remaining = session_info.get("quota_remaining", "?")

    used = session_info.get("quota_used", "?")
    maximum = session_info.get("quota_max", "?")
    plan_label = plan.capitalize()
    def _fmt_credits(v):
        if isinstance(v, (int, float)):
            return f"{v:g}"
        return str(v)
    console.print(f"Plan: {plan_label} ({_fmt_credits(used)}/{_fmt_credits(maximum)} credits used, {_fmt_credits(remaining)} remaining)")

    # Load EDA tool config (same as verifagent local CLI)
    tools_line = ""
    try:
        from sigmanticai.tools_config import ToolConfig
        _tool_cfg = ToolConfig.from_file()
        _sim_tool = _tool_cfg.get_tool("simulator")
        _tools_summary = f"{_sim_tool.display_name}"
        if _tool_cfg.coverage != "no_coverage":
            _cov_tool = _tool_cfg.get_tool("coverage")
            _tools_summary += f" + {_cov_tool.display_name}"
        tools_line = (
            f"\n[bold]EDA Tools:[/bold] {_tools_summary}  [dim]—[/dim] "
            "[bold cyan]/tool[/bold cyan] [dim]to configure[/dim]  "
            "[bold cyan]/tool reset[/bold cyan] [dim]to reset to defaults[/dim]\n"
        )
    except Exception:
        pass

    # Scan project and show cache status (mirrors verifagent's _auto_detect_brain)
    brain_line = ""
    try:
        console.print("[dim]Scanning project for HDL files...[/dim]", end=" ")
        from sigmanticai.brain import BrainCache, populate_cache_from_codebase
        from sigmanticai.brain.cache import _find_hdl_files
        from pathlib import Path as _Path

        cache = BrainCache()
        if cache.exists and not cache.is_stale():
            brain_status = cache.get_cache_summary()
        else:
            hdl_files = _find_hdl_files(_Path.cwd())
            if hdl_files:
                cache = populate_cache_from_codebase(force=True)
            brain_status = cache.get_cache_summary() if cache.exists else None

        console.print("[dim]Done.[/dim]")
        if brain_status:
            brain_line = f"\n\n[dim]{brain_status}[/dim]"
    except Exception:
        try:
            console.print("[dim]Done.[/dim]")
        except Exception:
            pass

    console.print()
    console.print(Panel(
        f"[bold blue]SigmanticAI v{__version__}[/bold blue]\n\n"
        "[bold]Your Universal Chip Design Assistant[/bold]\n\n"
        "I can help you:\n"
        "  • Generate complete chip verification environments from natural language\n"
        "  • Design RTL modules, interfaces, and IP blocks\n"
        "  • Create UVM testbenches, assertions, and coverage\n"
        "  • Edit, debug, and improve existing designs\n"
        "  • Search and analyze any hardware codebase\n"
        "  • Run compilation, simulation, and lint checks\n"
        f"{tools_line}\n"
        f"[dim]Ctrl+C to cancel, double Ctrl+C to exit  •  Type 'exit' to quit[/dim]{brain_line}",
        title="Welcome",
        border_style="blue",
    ))
    console.print()


def display_event(event: Dict[str, Any], output_dir: Path) -> None:
    """
    Render a single event from the server.

    Called for each event in the main event loop. Mirrors the exact
    display behavior of the local CLI.
    """
    event_type = event.get("type", "")

    if event_type == "text":
        _end_text_stream()
        _stop_spinner()
        _display_text(event.get("content", ""))

    elif event_type == "text_delta":
        chunk = event.get("content", "")
        if not chunk:
            return
        _text_buf.append(chunk)
        _text_total.append(chunk)
        total_so_far = len("".join(_text_total).strip())
        if total_so_far >= _TEXT_MIN_DISPLAY:
            _stop_spinner()
            buf_len = len("".join(_text_buf))
            if buf_len >= _TEXT_FLUSH_THRESHOLD:
                _flush_text_buf()

    elif event_type == "text_done":
        _end_text_stream()

    elif event_type == "text_suppress":
        _suppress_text_buf()

    elif event_type == "stage":
        _suppress_text_buf()
        _stop_spinner()
        _display_stage(event)
        label = event.get("label", "")
        if label and not _is_result_line(label):
            _start_spinner(_extract_spin_label(label))

    elif event_type == "stage_item":
        _suppress_text_buf()
        _stop_spinner()
        _display_stage_item(event)
        item = event.get("item", "")
        if item and not _is_result_line(item):
            _start_spinner(_extract_spin_label(item))

    elif event_type == "log":
        _suppress_text_buf()
        _stop_spinner()
        msg = event.get("message", "")
        if not msg:
            return
        if msg.strip().endswith("thinking..."):
            _start_spinner(_extract_spin_label(msg))
        else:
            _print_agent_log(msg)
            if msg.strip() and not _is_result_line(msg):
                _start_spinner(_extract_spin_label(msg))

    elif event_type == "tool_start":
        _suppress_text_buf()
        _stop_spinner()
        _display_tool_start(event)
        tool = event.get("tool", "")
        args_str = event.get("args", "")
        _start_spinner(_humanize_tool(tool, args_str)[:50])

    elif event_type == "tool_result":
        _stop_spinner()
        _display_tool_result(event)

    elif event_type == "fix_start":
        _stop_spinner()
        _display_fix_start(event)
        # Start a spinner for the fix agent
        _start_spinner("Fix agent working")

    elif event_type == "fix_end":
        _stop_spinner()
        _display_fix_end(event)

    elif event_type == "file":
        _stop_spinner()
        _save_and_display_file(event, output_dir)

    elif event_type == "file_deleted":
        _stop_spinner()
        _handle_file_deleted(event, output_dir)

    elif event_type == "error":
        _stop_spinner()
        _display_error(event.get("message", "Unknown error"))

    elif event_type == "input_needed":
        _end_text_stream()
        _stop_spinner()

    elif event_type == "tool_request":
        _suppress_text_buf()
        _stop_spinner()
        tool = event.get("tool", "?")
        _start_spinner(f"Running {tool}")

    elif event_type == "command_request":
        _stop_spinner()
        pass  # Handled by the main loop (async execution needed)

    elif event_type == "done":
        _end_text_stream()
        _stop_spinner()
        _display_done(event)

    elif event_type == "failed":
        _end_text_stream()
        _stop_spinner()
        _display_failed(event)

    elif event_type == "credits_exhausted":
        _end_text_stream()
        _stop_spinner()
        _display_credits_exhausted(event)

    elif event_type == "quota":
        remaining = event.get("remaining", "?")
        limit = event.get("limit", "?")
        try:
            remaining = f"{float(remaining):g}"
        except (ValueError, TypeError):
            pass
        try:
            limit = f"{float(limit):g}"
        except (ValueError, TypeError):
            pass
        console.print(f"  [dim]Quota: {remaining}/{limit} remaining[/dim]")

    elif event_type == "thinking":
        # Agent is making an API call
        label = event.get("label", "")
        if label:
            _start_spinner(label)

    elif event_type in ("ping", "pong", "session"):
        pass  # Silent

    else:
        # Unknown event — show raw for debugging
        data = event.get("data", "")
        if data:
            console.print(f"  [dim][{event_type}] {data}[/dim]")


# ── PM / specialist log renderer (mirrors verifagent _print_agent_log) ──

def _print_agent_log(msg: str):
    """Print a multi-agent log message with Rich styling.

    Detects the message pattern (turn header, tool call, agent text, dispatch,
    etc.) and applies appropriate Rich formatting to match the Director UI.
    """
    stripped = msg.strip()
    if not stripped:
        return

    m = _RE_TURN.match(stripped)
    if m:
        pm_name, turn_num = m.group(1), m.group(2)
        t = Text()
        t.append("  > ", style="dim")
        t.append(pm_name, style="bold cyan")
        t.append(f" — turn {turn_num}", style="dim italic")
        console.print(t)
        return

    m = _RE_DISPATCH.match(stripped)
    if m:
        specialist = m.group(2).strip()
        name_part = specialist.split(" (")[0].split("(")[0].strip()
        color = _SPECIALIST_COLORS.get(name_part, "bold white")
        t = Text()
        t.append("  >> ", style="dim")
        t.append(specialist, style=color)
        console.print(t)
        return

    m = _RE_STARTING.match(stripped)
    if m:
        name = m.group(1)
        color = _SPECIALIST_COLORS.get(name, "bold white")
        t = Text()
        t.append("  | ", style="dim")
        t.append(name, style=color)
        t.append(" starting...", style="dim italic")
        console.print(t)
        return

    m = _RE_DONE_LOG.match(stripped)
    if m:
        name, summary = m.group(1), m.group(2)
        color = _SPECIALIST_COLORS.get(name, "bold white")
        t = Text()
        t.append("  + ", style="green")
        t.append(name, style=color)
        t.append(f" — {summary}", style="dim")
        console.print(t)
        return

    m = _RE_STAGE_PROGRESS.match(stripped)
    if m:
        cur, total, label = m.group(1), m.group(2), m.group(3)
        t = Text()
        t.append(f"  [{cur}/{total}] ", style="bold cyan")
        t.append(label, style="")
        console.print(t)
        return

    m = _RE_SIM_RESULT.match(stripped)
    if m:
        icon, detail = m.group(1), m.group(2)
        style = "green" if icon == "✓" else "red" if icon == "✗" else "yellow"
        t = Text()
        t.append(f"    {icon} ", style=style)
        t.append(detail, style=style)
        console.print(t)
        return

    m = _RE_TOOL_CALL.match(stripped)
    if m:
        _tool_name, human_desc = m.group(1), m.group(2)
        t = Text()
        t.append("    ", style="dim")
        t.append(human_desc, style="dim")
        console.print(t)
        return

    m = _RE_AGENT_TEXT.match(stripped)
    if m:
        text = m.group(1).strip()
        console.print(f"\n    {text}\n", highlight=False)
        return

    if _RE_WARNING.match(stripped):
        t = Text(f"  {stripped}", style="yellow")
        console.print(t)
        return

    if _RE_PAUSE.search(stripped):
        t = Text(f"\n  {stripped}", style="bold yellow")
        console.print(t)
        return

    if (_RE_USER_STOPPED.search(stripped)
            or _RE_USER_FEEDBACK.search(stripped)
            or _RE_HEADLESS_AUTO.search(stripped)):
        t = Text(f"  {stripped}", style="dim italic")
        console.print(t)
        return

    if msg.startswith("    ") and not stripped.startswith("["):
        t = Text(f"    {stripped}", style="dim")
        console.print(t)
        return

    if stripped.startswith("      ") or stripped.startswith("        "):
        console.print(f"    {stripped.strip()}", markup=False, highlight=False)
        return

    console.print(f"  {stripped}", markup=False, highlight=False)


# ── Renderers ─────────────────────────────────────────────────

def _display_text(content: str) -> None:
    """Render agent text response."""
    if not content or not content.strip():
        return
    console.print()
    try:
        console.print(Markdown(content))
    except Exception:
        console.print(content)
    console.print()


def _display_stage(event: Dict[str, Any]) -> None:
    """Render pipeline stage progress."""
    current = event.get("current", "?")
    total = event.get("total", "?")
    label = event.get("label", "")
    detail = event.get("detail", "")

    if not label:
        return

    console.print(f"\n[bold cyan][{current}/{total}][/bold cyan] {label}")
    if detail:
        console.print(f"      [dim]{detail}[/dim]")


def _display_stage_item(event: Dict[str, Any]) -> None:
    """Render a sub-item within a stage (pipeline log line)."""
    item = event.get("item", "")
    status = event.get("status", "")

    if not item:
        return

    # Detect result indicators and style accordingly
    if any(ind in item for ind in ("✓", "✅", "PASSED")):
        console.print(f"      [green]{item}[/green]")
    elif any(ind in item for ind in ("✗", "❌", "FAILED", "ERROR")):
        console.print(f"      [red]{item}[/red]")
    elif any(ind in item for ind in ("⚠", "WARNING")):
        console.print(f"      [yellow]{item}[/yellow]")
    elif item.strip().startswith("[") and "]" in item:
        # Stage header like "[1/12] Analyzing design..."
        console.print(f"\n{item}")
    else:
        console.print(f"      [dim]{item}[/dim]")


def _humanize_tool(tool_name: str, args_str: str) -> str:
    """Convert a tool call to a human-readable one-liner (matches verifagent)."""
    import json as _json
    try:
        args = _json.loads(args_str) if isinstance(args_str, str) else args_str
    except Exception:
        args = {}
    if not isinstance(args, dict):
        args = {}

    fp = args.get("file_path", args.get("path", ""))

    if tool_name in ("read_file",):
        return f"Reading {fp}" if fp else "Reading file"
    if tool_name in ("write_file", "create_file"):
        return f"Writing {fp}" if fp else "Writing file"
    if tool_name == "edit_file":
        return f"Editing {fp}" if fp else "Editing file"
    if tool_name == "apply_diff":
        return f"Patching {fp}" if fp else "Applying diff"
    if tool_name in ("delete_file",):
        return f"Deleting {fp}" if fp else "Deleting file"
    if tool_name == "move_file":
        return f"Moving {args.get('source', '')} -> {args.get('destination', '')}"
    if tool_name == "copy_file":
        return f"Copying {args.get('source', '')} -> {args.get('destination', '')}"
    if tool_name == "create_directory":
        return f"Creating directory {fp}" if fp else "Creating directory"
    if tool_name == "run_command":
        cmd = args.get("command", "")
        short = cmd.split("\n")[0][:120]
        return f"Running: {short}" if short else "Running shell command"
    if tool_name == "grep_search":
        pat = args.get("pattern", "")
        path = args.get("path", ".")
        return f"Searching for '{pat}' in {path}"
    if tool_name == "list_directory":
        path = args.get("path", ".")
        return f"Listing {path}"
    if tool_name == "generate_verification":
        return "Generating verification environment..."
    if tool_name == "generate_rtl":
        return "Generating RTL design..."
    if tool_name == "plan_design":
        return "Planning design architecture..."
    if tool_name == "web_search":
        query = args.get("query", "")
        return f"Web search: {query[:100]}" if query else "Searching the web..."

    short = str(args)
    if len(short) > 100:
        short = short[:100] + "..."
    return f"{tool_name}: {short}"


def _display_tool_start(event: Dict[str, Any]) -> None:
    """Show tool invocation as a clean human-readable one-liner."""
    tool = event.get("tool", "")
    args_str = event.get("args", "")
    desc = _humanize_tool(tool, args_str)
    t = Text()
    t.append("  ", style="dim")
    t.append(desc, style="dim")
    console.print(t)


def _display_tool_result(event: Dict[str, Any]) -> None:
    """Show tool result — minimal to avoid UI clutter (matches verifagent).

    Only shows output when it's short, single-line, and useful (errors,
    confirmations).  Multi-line results (directory listings, file contents)
    and large results are silently consumed by the agent.
    """
    result = event.get("result", "")
    if not result or not result.strip():
        return
    if len(result) > 500:
        return
    stripped = result.strip()
    if "\n" in stripped:
        return
    t = Text(f"    {stripped[:300]}", style="dim")
    console.print(t)


def _display_fix_start(event: Dict[str, Any]) -> None:
    """Show fix agent starting."""
    filename = event.get("filename", "")
    attempt = event.get("attempt", "?")
    max_attempts = event.get("max_attempts", "?")
    console.print(
        f"      [yellow]→ Coding agent fixing[/yellow] {filename} "
        f"[dim](attempt {attempt}/{max_attempts})[/dim]"
    )


def _display_fix_end(event: Dict[str, Any]) -> None:
    """Show fix agent result."""
    filename = event.get("filename", "")
    success = event.get("success", "false")
    if isinstance(success, str):
        success = success.lower() == "true"
    summary = event.get("summary", "")

    if success:
        console.print(f"      [green]✓ Fixed[/green] {filename}")
    else:
        console.print(f"      [red]✗ Could not fix[/red] {filename}")
    if summary:
        console.print(f"        [dim]{summary}[/dim]")


def _handle_file_deleted(event: Dict[str, Any], output_dir: Path) -> None:
    """Handle a file_deleted event — remove local file and stop tracking."""
    rel_path = event.get("path", "")
    if not rel_path:
        return

    full_path = (output_dir / rel_path).resolve()
    # SECURITY: prevent path traversal
    if not str(full_path).startswith(str(output_dir.resolve())):
        return

    if full_path.is_file():
        full_path.unlink()
        t = Text()
        t.append("    - ", style="dim")
        t.append(rel_path, style="dim bold")
        t.append(" (deleted)", style="dim italic")
        console.print(t)

    # Stop tracking this file
    _file_hashes.pop(rel_path, None)


def _save_and_display_file(event: Dict[str, Any], output_dir: Path) -> None:
    """Save a delivered file to disk with path traversal protection."""
    rel_path = event.get("path", "")
    content = event.get("content", "")
    if not rel_path:
        return

    full_path = (output_dir / rel_path).resolve()
    # SECURITY: prevent path traversal — block writes outside output_dir
    if not str(full_path).startswith(str(output_dir.resolve())):
        console.print(f"  [red]⚠ Blocked suspicious file path: {rel_path}[/red]")
        return
    full_path.parent.mkdir(parents=True, exist_ok=True)
    full_path.write_text(content)

    # Track hash so we can detect local edits later (auto-sync)
    _file_hashes[rel_path] = _hash_content(content)

    lines = content.count("\n") + 1 if content else 0
    t = Text()
    t.append("    + ", style="dim")
    t.append(rel_path, style="dim bold")
    t.append(f" ({lines} lines)", style="dim italic")
    console.print(t)


def _display_error(message: str) -> None:
    """Display an error inline (matches verifagent)."""
    t = Text()
    t.append("    error: ", style="bold red")
    t.append(message, style="red")
    console.print(t)


def _display_done(event: Dict[str, Any]) -> None:
    """Display job completion summary."""
    files_count = event.get("files_count", "0")
    output_dir = event.get("output_dir", "")
    coverage = event.get("coverage_score", "")

    console.print()
    lines = [f"[bold green]✅ Generation complete![/bold green]"]
    if files_count and files_count != "0":
        lines.append(f"  Files: {files_count}")
    if output_dir:
        lines.append(f"  Output: {output_dir}")
    if coverage and coverage != "0.0":
        lines.append(f"  Coverage: {coverage}%")

    console.print(Panel("\n".join(lines), border_style="green"))
    console.print()


def _display_failed(event: Dict[str, Any]) -> None:
    """Display job failure."""
    reason = event.get("reason", "Unknown error")
    console.print()
    console.print(
        Panel(
            f"[red]Job failed: {reason}[/red]",
            title="Failed",
            border_style="red",
        )
    )
    console.print()


def _display_credits_exhausted(event: Dict[str, Any]) -> None:
    """Display credits-exhausted notice with upgrade link."""
    used = event.get("credits_used", "?")
    total = event.get("credits_total", "?")
    try:
        used = f"{float(used):g}"
    except (ValueError, TypeError):
        pass
    try:
        total = f"{float(total):g}"
    except (ValueError, TypeError):
        pass
    url = event.get("upgrade_url", "https://www.sigmanticai.com/pricing.html")
    summary = event.get("summary", "")
    lines = [
        f"[bold yellow]Credits exhausted ({used}/{total} used)[/bold yellow]",
        "",
        "Your generation was saved — resume after upgrading.",
        f"  Upgrade: [link={url}]{url}[/link]",
    ]
    if summary:
        lines.insert(1, f"  Progress: {summary}")
    console.print()
    console.print(Panel("\n".join(lines), title="Credits Exhausted", border_style="yellow"))
    console.print()
