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

Translates WebSocket events into beautiful CLI output that matches
the local CLI experience:
  - stage → progress with step numbers
  - stage_item → pipeline log lines (indented)
  - text  → markdown rendering
  - file  → file save notification
  - tool_start / tool_result → tool use display
  - fix_start / fix_end → coding agent progress
  - error → red error panel
  - done / failed → completion summary
"""

from __future__ import annotations

import hashlib
import os
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.syntax import Syntax
from rich.text import Text

console = Console()

# ── 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
    _spin_stop.set()
    if _spin_thread and _spin_thread.is_alive():
        _spin_thread.join(timeout=1)
    _spin_thread = None
    # Extra safety: clear line in case thread didn't clean up
    _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]


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

def display_welcome(session_info: Dict[str, Any]) -> None:
    """Display welcome message after connecting — matches the original CLI banner."""
    plan = session_info.get("plan", "free")
    remaining = session_info.get("quota_remaining", "?")

    console.print()
    console.print(Panel(
        f"[bold blue]🚀 SigmanticAI v0.1.0[/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\n"
        f"[dim]Plan: {plan}  |  Generations remaining: {remaining}[/dim]\n"
        "[dim]Type 'exit' or Ctrl+C to quit[/dim]",
        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":
        _stop_spinner()
        _display_text(event.get("content", ""))

    elif event_type == "stage":
        _stop_spinner()
        _display_stage(event)
        # Start spinner for the stage work
        label = event.get("label", "")
        if label and not _is_result_line(label):
            _start_spinner(_extract_spin_label(label))

    elif event_type == "stage_item":
        _stop_spinner()
        _display_stage_item(event)
        # Start spinner if this is a "working" line (not a result)
        item = event.get("item", "")
        if item and not _is_result_line(item):
            _start_spinner(_extract_spin_label(item))

    elif event_type == "log":
        # Pipeline log messages — same as stage_item but with raw message
        _stop_spinner()
        msg = event.get("message", "")
        if msg:
            console.print(msg)
        if msg and not _is_result_line(msg):
            _start_spinner(_extract_spin_label(msg))

    elif event_type == "tool_start":
        _stop_spinner()
        _display_tool_start(event)

    elif event_type == "tool_result":
        _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":
        _stop_spinner()
        pass  # Handled by the main loop

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

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

    elif event_type == "quota":
        remaining = event.get("remaining", "?")
        limit = event.get("limit", "?")
        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]")


# ── 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", "")

    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 _display_tool_start(event: Dict[str, Any]) -> None:
    """Show tool invocation."""
    tool = event.get("tool", "")
    args_str = event.get("args", "")
    if isinstance(args_str, dict):
        import json
        args_str = json.dumps(args_str, indent=2)
    if isinstance(args_str, str) and len(args_str) > 200:
        args_str = args_str[:200] + "..."
    console.print(f"\n[dim]🔧 Using tool: [bold]{tool}[/bold][/dim]")
    if args_str:
        console.print(f"[dim]{args_str}[/dim]")


def _display_tool_result(event: Dict[str, Any]) -> None:
    """Show tool result (truncated)."""
    result = event.get("result", "")
    if not result:
        return
    if len(result) > 2000:
        result = result[:2000] + "\n... (truncated)"

    # Try syntax highlighting for code results
    if any(kw in result for kw in ["module ", "interface ", "class ", "function "]):
        try:
            console.print(Syntax(result, "systemverilog", theme="monokai"))
            return
        except Exception:
            pass

    console.print(Panel(result, border_style="green"))


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()
        console.print(f"  [yellow]🗑 Deleted:[/yellow] {rel_path}")

    # 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
    console.print(f"  [green]📄 Saved:[/green] {rel_path} ({lines} lines)")


def _display_error(message: str) -> None:
    """Display an error panel."""
    console.print()
    console.print(Panel(f"[red]{message}[/red]", title="Error", border_style="red"))
    console.print()


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()
