"""Unified output system for the MCPBundles CLI.

Modes:
  - pretty: Rich tables, panels, syntax highlighting (default for discovery)
  - json: Raw JSON to stdout, pipe-friendly (default for execution)
  - raw: Full MCP response envelope (for debugging)

Respects NO_COLOR (https://no-color.org/) and --quiet mode.
"""

import base64
import hashlib
import json
import os
import sys
import time
import webbrowser
from pathlib import Path
from typing import Any, Optional

from rich.console import Console
from rich.panel import Panel
from rich.syntax import Syntax
from rich.table import Table
from rich.text import Text
from mcpbundles.mcp_client import CallToolResult, TextContent, ImageContent, EmbeddedResource
from mcpbundles.config import CONFIG_DIR

_no_color = "NO_COLOR" in os.environ

console = Console(stderr=True, no_color=_no_color)
stdout_console = Console(file=sys.stdout, no_color=_no_color)

_quiet = False

_CACHE_DIR = CONFIG_DIR / "cache"
_CACHE_MAX_AGE_SECONDS = 86400  # 24 hours

_MIME_EXTENSIONS = {
    "image/png": ".png",
    "image/jpeg": ".jpg",
    "image/gif": ".gif",
    "image/webp": ".webp",
    "image/svg+xml": ".svg",
    "application/pdf": ".pdf",
    "audio/mpeg": ".mp3",
    "audio/wav": ".wav",
    "video/mp4": ".mp4",
}

_output_dir: Optional[Path] = None


def set_output_dir(path: Optional[str]) -> None:
    """Set the directory for saving binary content. None = default cache dir."""
    global _output_dir
    _output_dir = Path(path) if path else None


def _get_output_dir() -> Path:
    return _output_dir or _CACHE_DIR


def _prune_cache() -> None:
    """Remove cache files older than 24 hours. Only prunes the default cache dir."""
    cache = _CACHE_DIR
    if not cache.exists():
        return
    cutoff = time.time() - _CACHE_MAX_AGE_SECONDS
    for f in cache.iterdir():
        if f.is_file() and f.stat().st_mtime < cutoff:
            f.unlink(missing_ok=True)


def _save_binary(data_b64: str, mime_type: str) -> str:
    """Decode base64 data, save to output dir, return the absolute file path."""
    out_dir = _get_output_dir()
    out_dir.mkdir(parents=True, exist_ok=True)

    if out_dir == _CACHE_DIR:
        _prune_cache()

    raw = base64.b64decode(data_b64)
    digest = hashlib.sha256(raw).hexdigest()[:12]
    ext = _MIME_EXTENSIONS.get(mime_type, ".bin")
    filename = f"{digest}{ext}"
    filepath = out_dir / filename
    filepath.write_bytes(raw)
    return str(filepath.resolve())


def set_quiet(enabled: bool) -> None:
    global _quiet
    _quiet = enabled


def is_tty() -> bool:
    return sys.stdout.isatty()


def print_json(data: Any) -> None:
    if isinstance(data, str):
        try:
            parsed = json.loads(data)
            print(json.dumps(parsed, indent=2))
        except json.JSONDecodeError:
            print(data)
    else:
        print(json.dumps(data, indent=2, default=str))


def print_raw(data: Any) -> None:
    print(json.dumps(data, indent=2, default=str))


def render_tools_table(tools: list, filter_str: str | None = None) -> None:
    if filter_str:
        lower = filter_str.lower()
        tools = [t for t in tools if lower in t.name.lower() or (t.description and lower in t.description.lower())]

    table = Table(show_header=True, header_style="bold", box=None, pad_edge=False)
    table.add_column("Name", style="cyan", no_wrap=True)
    table.add_column("Description", style="dim")

    for tool in sorted(tools, key=lambda t: t.name):
        desc = (tool.description or "")[:80]
        table.add_row(tool.name, desc)

    panel = Panel(
        table,
        title=f"MCP Tools ({len(tools)} available)",
        border_style="blue",
        padding=(1, 2),
    )
    stdout_console.print(panel)
    stdout_console.print()
    stdout_console.print("  Use [cyan]mcpbundles tools <name>[/] for full schema", highlight=False)
    stdout_console.print("  Use [cyan]mcpbundles call <name> ...[/] to execute", highlight=False)


def render_tools_json(tools: list) -> None:
    data = [{"name": t.name, "description": t.description, "inputSchema": t.inputSchema} for t in tools]
    print_json(data)


def render_resources_table(resources: list) -> None:
    table = Table(show_header=True, header_style="bold", box=None, pad_edge=False)
    table.add_column("URI", style="cyan", no_wrap=True)
    table.add_column("Name", style="white")
    table.add_column("MIME Type", style="dim")

    for r in resources:
        table.add_row(str(r.uri), r.name or "", r.mimeType or "")

    panel = Panel(
        table,
        title=f"MCP Resources ({len(resources)} available)",
        border_style="blue",
        padding=(1, 2),
    )
    stdout_console.print(panel)
    stdout_console.print()
    stdout_console.print("  Use [cyan]mcpbundles read <uri>[/] to fetch", highlight=False)


def render_resources_json(resources: list) -> None:
    data = [{"uri": str(r.uri), "name": r.name, "description": r.description, "mimeType": r.mimeType} for r in resources]
    print_json(data)


def render_prompts_table(prompts: list) -> None:
    if not prompts:
        stdout_console.print(Panel("No prompts currently registered.", title="MCP Prompts (0 available)", border_style="blue", padding=(1, 2)))
        return

    table = Table(show_header=True, header_style="bold", box=None, pad_edge=False)
    table.add_column("Name", style="cyan", no_wrap=True)
    table.add_column("Description", style="dim")

    for p in prompts:
        table.add_row(p.name, (p.description or "")[:80])

    panel = Panel(
        table,
        title=f"MCP Prompts ({len(prompts)} available)",
        border_style="blue",
        padding=(1, 2),
    )
    stdout_console.print(panel)


def render_prompts_json(prompts: list) -> None:
    data = [{"name": p.name, "description": p.description, "arguments": [{"name": a.name, "description": a.description, "required": a.required} for a in (p.arguments or [])]} for p in prompts]
    print_json(data)


def _maybe_handle_oauth_url(content: list) -> None:
    """Detect oauth_required responses and open the credential URL in a browser."""
    if not content or len(content) != 1 or not isinstance(content[0], TextContent):
        return
    try:
        parsed = json.loads(content[0].text)
    except (json.JSONDecodeError, TypeError):
        return
    if not isinstance(parsed, dict):
        return
    if parsed.get("status") != "oauth_required":
        return
    url = parsed.get("credential_url")
    if not url or not isinstance(url, str):
        return
    console.print(
        Panel(
            f"[bold]OAuth authentication required[/bold]\n\n"
            f"Opening browser to complete authorization:\n"
            f"[cyan]{url}[/cyan]\n\n"
            f"[dim]Complete the OAuth flow in your browser, then the provider will be ready to use.[/dim]",
            title="🔐 OAuth Setup",
            border_style="yellow",
        )
    )
    try:
        webbrowser.open(url)
    except Exception:
        pass


def render_call_result(result: CallToolResult, mode: str = "json") -> None:
    if mode == "raw":
        print_raw({"content": [_content_to_dict(c) for c in result.content], "isError": result.isError})
        return

    if not result.content and not result.isError:
        render_warning(
            "Tool returned no content. If this is a bundle tool, use --bundle to connect directly:\n"
            "  mcpbundles tools --bundle <slug>        # list tools in a bundle\n"
            "  mcpbundles call <tool> --bundle <slug>   # call a bundle tool directly\n"
            "  mcpbundles call get_bundles              # list available bundles"
        )
        return

    _maybe_handle_oauth_url(result.content)

    if mode == "json":
        _render_json_content(result.content)
    else:
        for block in result.content:
            if isinstance(block, TextContent):
                if mode == "stream":
                    _render_stream_text(block.text)
                else:
                    try:
                        parsed = json.loads(block.text)
                        syntax = Syntax(json.dumps(parsed, indent=2), "json", theme="monokai")
                        stdout_console.print(syntax)
                    except json.JSONDecodeError:
                        stdout_console.print(block.text)
            elif isinstance(block, ImageContent):
                path = _save_binary(block.data, block.mimeType)
                stdout_console.print(f"[dim]Saved {block.mimeType} →[/] [cyan]{path}[/]")
            elif isinstance(block, EmbeddedResource):
                mime = block.resourceMimeType
                path = _save_binary(block.data, mime)
                stdout_console.print(f"[dim]Saved {mime} →[/] [cyan]{path}[/]")


def _render_json_content(content: list) -> None:
    """Render content blocks as a single valid JSON document.

    Single TextContent with valid JSON → print the parsed JSON directly.
    Multiple blocks or mixed types → wrap in a JSON array.
    """
    if len(content) == 1 and isinstance(content[0], TextContent):
        text = content[0].text
        try:
            parsed = json.loads(text)
            print(json.dumps(parsed, indent=2))
        except json.JSONDecodeError:
            print(text)
        return

    parts: list[Any] = []
    for block in content:
        if isinstance(block, TextContent):
            try:
                parts.append(json.loads(block.text))
            except json.JSONDecodeError:
                parts.append({"type": "text", "text": block.text})
        elif isinstance(block, ImageContent):
            path = _save_binary(block.data, block.mimeType)
            parts.append({"type": "image", "mimeType": block.mimeType, "path": path})
        elif isinstance(block, EmbeddedResource):
            mime = block.resourceMimeType
            path = _save_binary(block.data, mime)
            parts.append({"type": "resource", "mimeType": mime, "path": path, "uri": block.resource.uri})

    if len(parts) == 1:
        print(json.dumps(parts[0], indent=2))
    else:
        print(json.dumps(parts, indent=2))


def _render_stream_text(text: str) -> None:
    """Render text in streaming style — line by line with a gutter."""
    try:
        parsed = json.loads(text)
        if isinstance(parsed, dict):
            stdout_output = parsed.get("stdout", "")
            stderr_output = parsed.get("stderr", "")
            if stdout_output:
                for line in stdout_output.rstrip("\n").split("\n"):
                    stdout_console.print(f"  [dim]│[/] {line}")
            if stderr_output:
                for line in stderr_output.rstrip("\n").split("\n"):
                    console.print(f"  [dim]│[/] [yellow]{line}[/]")
            result_val = parsed.get("result")
            if result_val is not None:
                stdout_console.print(f"\n  [green]→[/] {result_val}")
            return
    except json.JSONDecodeError:
        pass
    for line in text.rstrip("\n").split("\n"):
        stdout_console.print(f"  [dim]│[/] {line}")


def render_error(message: str) -> None:
    console.print(f"[red]Error:[/] {message}")


def render_hint(message: str) -> None:
    console.print(f"[dim]Hint:[/] {message}")


def render_success(message: str) -> None:
    console.print(f"[green]✓[/] {message}")


def render_warning(message: str) -> None:
    console.print(f"[yellow]⚠[/]  {message}")


def _content_to_dict(content: Any) -> dict:
    if isinstance(content, TextContent):
        return {"type": "text", "text": content.text}
    if isinstance(content, ImageContent):
        path = _save_binary(content.data, content.mimeType)
        return {"type": "image", "mimeType": content.mimeType, "path": path}
    if isinstance(content, EmbeddedResource):
        mime = content.resourceMimeType
        path = _save_binary(content.data, mime)
        return {"type": "resource", "mimeType": mime, "path": path, "uri": content.resource.uri}
    return {"type": "unknown"}
