"""
Interactive setup wizard for Mesh.

Guides the developer through:
1. Codebase analysis (Phase 1)
2. Ollama detection and installation
3. Model selection from installed models
4. MCP server registration with Cursor/Claude Code
5. Saves config to .mesh/config.json
"""

import json
import subprocess
import time
import webbrowser
from pathlib import Path

from rich import box
from rich.console import Console
from rich.prompt import IntPrompt, Prompt
from rich.table import Table

from mesh.core.storage import MeshStorage
from mesh.ollama.detector import (
    OllamaModel,
    OllamaStatus,
    detect_ollama,
    get_recommended_model,
)

console = Console()


class SetupAborted(Exception):
    """Raised when setup is cancelled or cannot complete."""

    pass


def run_setup_wizard(codebase_root: Path) -> dict:
    """Run the full interactive setup wizard.

    Args:
        codebase_root: Root directory of the codebase.

    Returns:
        The saved config dict on success.

    Raises:
        SetupAborted: If user cancels or setup cannot complete.
    """
    console.print("\n[bold]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold]")
    console.print("[bold cyan]  Mesh Setup Wizard[/bold cyan]")
    console.print("[bold]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold]\n")

    # Step 1: Verify codebase is analysed
    _step_verify_codebase(codebase_root)

    # Step 2: Check/install Ollama
    status = _step_check_ollama()

    # Step 3: Select model
    selected_model = _step_select_model(status)

    # Step 4: Register with editors
    _step_register_editors(codebase_root)

    # Step 5: Save config
    config = _save_config(codebase_root, selected_model)

    # Done
    _print_success(selected_model)

    return config


def _step_verify_codebase(codebase_root: Path) -> None:
    """Verify mesh init has been run.

    Args:
        codebase_root: Root directory of the codebase.
    """
    console.print("[bold]Step 1/4:[/bold] Checking codebase analysis...")

    storage = MeshStorage(codebase_root)
    if not storage.graphs_exist():
        console.print("  [yellow]⚠ Codebase not analysed yet.[/yellow]")
        console.print("  Running mesh init...")
        # Import and run init
        from mesh.cli import init

        init.callback(root=str(codebase_root))
    else:
        call_graph = storage.load_call_graph()
        console.print(
            f"  [green]✓[/green] "
            f"{len(call_graph.nodes)} functions | "
            f"{len(call_graph.edges)} dependencies"
        )


def _step_check_ollama() -> OllamaStatus:
    """Check Ollama installation, guide install if missing.

    Returns:
        OllamaStatus with installation and model information.

    Raises:
        SetupAborted: If Ollama cannot be installed or started.
    """
    console.print("\n[bold]Step 2/4:[/bold] Checking for Ollama...")

    status = detect_ollama()

    if not status.is_installed:
        console.print("  [yellow]✗ Ollama not found.[/yellow]")
        console.print("\n  Ollama runs AI models locally.")
        console.print("  Install it from: [link]https://ollama.com/download[/link]")
        console.print("\n  Opening download page...")
        webbrowser.open("https://ollama.com/download")
        Prompt.ask("\n  Press Enter when Ollama is installed")
        status = detect_ollama()
        if not status.is_installed:
            console.print(
                "[red]  Ollama still not found. "
                "Please install and run 'mesh setup' again.[/red]"
            )
            raise SetupAborted("Ollama not installed")

    if not status.is_running:
        console.print("  [yellow]⚠ Ollama installed but not running.[/yellow]")
        console.print("  Starting Ollama...")
        subprocess.Popen(
            ["ollama", "serve"],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
        time.sleep(3)
        status = detect_ollama()
        if not status.is_running:
            console.print(
                "[red]  Could not start Ollama. "
                "Run 'ollama serve' in another terminal.[/red]"
            )
            raise SetupAborted("Ollama not running")

    console.print("  [green]✓[/green] Ollama running")

    if not status.models:
        console.print("\n  [yellow]No models installed yet.[/yellow]")
        console.print("  Recommended: [bold]ollama pull qwen3.5:9b[/bold]")
        console.print("  Smaller option: [bold]ollama pull qwen3.5:4b[/bold]")
        Prompt.ask("\n  Install a model then press Enter")
        status = detect_ollama()
        if not status.models:
            raise SetupAborted("No models installed")

    return status


def _step_select_model(status: OllamaStatus) -> OllamaModel:
    """Display model table and let user select.

    Args:
        status: OllamaStatus with installed models.

    Returns:
        Selected OllamaModel.

    Raises:
        SetupAborted: If no valid model is selected.
    """
    console.print("\n[bold]Step 3/4:[/bold] Select AI model...")
    console.print(
        f"  Found [bold]{len(status.models)}[/bold] " f"model(s) on your machine:\n"
    )

    # Build table
    table = Table(box=box.SIMPLE, show_header=True, header_style="bold")
    table.add_column("#", style="dim", width=3)
    table.add_column("Model", min_width=24)
    table.add_column("Size", justify="right", width=7)
    table.add_column("Rating", width=10)
    table.add_column("Notes", style="dim")

    recommended = get_recommended_model(status.models)
    compat_style = {
        "excellent": "green",
        "good": "cyan",
        "partial": "yellow",
        "unknown": "dim",
    }

    for i, model in enumerate(status.models, 1):
        is_rec = model == recommended
        style = compat_style.get(model.compatibility, "dim")
        notes = []
        if is_rec:
            notes.append("★ Recommended")
        if model.supports_thinking:
            notes.append("thinking mode")
        table.add_row(
            str(i),
            f"[bold]{model.name}[/bold]" if is_rec else model.name,
            f"{model.size_gb}GB",
            f"[{style}]{model.compatibility}[/{style}]",
            " · ".join(notes),
        )

    console.print(table)

    # Default to recommended
    default = str(status.models.index(recommended) + 1 if recommended else 1)

    choice = IntPrompt.ask("  Select model", default=int(default))

    # Validate choice
    if choice < 1 or choice > len(status.models):
        console.print("  [red]Invalid choice. Using model 1.[/red]")
        choice = 1

    selected = status.models[choice - 1]
    console.print(f"\n  [green]✓[/green] Connected to [bold]{selected.name}[/bold]")

    return selected


def _step_register_editors(codebase_root: Path) -> None:
    """Register MCP server with Cursor and Claude Code.

    Args:
        codebase_root: Root directory of the codebase.
    """
    console.print("\n[bold]Step 4/4:[/bold] Registering with editors...")

    cursor_config = codebase_root / ".cursor" / "mcp.json"
    claude_config = Path.home() / ".claude" / "mcp.json"

    registered = []

    # Cursor
    _write_mcp_config(cursor_config)
    registered.append("Cursor")

    # Claude Code
    _write_mcp_config(claude_config)
    registered.append("Claude Code")

    for editor in registered:
        console.print(f"  [green]✓[/green] {editor} registered")


def _write_mcp_config(config_path: Path) -> None:
    """Write or merge mesh MCP server config.

    Args:
        config_path: Path to config file.
    """
    config_path.parent.mkdir(parents=True, exist_ok=True)

    mesh_entry = {
        "mesh": {
            "command": "mesh",
            "args": ["serve"],
            "env": {},
        }
    }

    if config_path.exists():
        try:
            existing = json.loads(config_path.read_text())
            if "mcpServers" not in existing:
                existing["mcpServers"] = {}
            existing["mcpServers"].update(mesh_entry)
            config_path.write_text(json.dumps(existing, indent=2))
        except (json.JSONDecodeError, KeyError):
            # Corrupted config — write fresh
            config_path.write_text(json.dumps({"mcpServers": mesh_entry}, indent=2))
    else:
        config_path.write_text(json.dumps({"mcpServers": mesh_entry}, indent=2))


def _save_config(codebase_root: Path, model: OllamaModel) -> dict:
    """Save selected model to .mesh/config.json.

    Args:
        codebase_root: Root directory of the codebase.
        model: Selected OllamaModel.

    Returns:
        Saved config dict.
    """
    config_path = codebase_root / ".mesh" / "config.json"

    # Load existing config if present
    if config_path.exists():
        try:
            config = json.loads(config_path.read_text())
        except json.JSONDecodeError:
            config = {}
    else:
        config = {}

    config.update(
        {
            "model": model.name,
            "model_family": model.family,
            "provider": "ollama",
            "supports_thinking": model.supports_thinking,
            "is_instruction_tuned": model.is_instruction_tuned,
            "compatibility": model.compatibility,
        }
    )

    config_path.write_text(json.dumps(config, indent=2))
    return config


def _print_success(model: OllamaModel) -> None:
    """Print success message.

    Args:
        model: Selected OllamaModel.
    """
    console.print("\n[bold]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold]")
    console.print("[bold green]  ✓ Mesh is ready.[/bold green]")
    console.print("[bold]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold]\n")
    console.print(f"  Model:  [bold]{model.name}[/bold]")
    console.print(f"  Rating: {model.compatibility}")
    console.print("\n  [dim]Start the MCP server:[/dim]")
    console.print("    [bold]mesh serve[/bold]")
    console.print("\n  [dim]Then open Cursor — architectural context[/dim]")
    console.print("  [dim]will be injected automatically.[/dim]\n")
