#!/usr/bin/env python3
"""Install vm-flightsimulator plugin skills, agents, and MCP server across AI CLI tools.

Copies skills/ and agents/ to target config directories and registers the MCP server.
Supports Claude Code, OpenCode, Gemini CLI, and Codex.
"""

from __future__ import annotations

import json
import re
import shutil
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Annotated, Literal, TypedDict

import typer
from rich.panel import Panel

from mcp_vm_blackbox.rich_console import make_console, print_measured_panel

if TYPE_CHECKING:
    from rich.console import Console

__all__ = ["app", "install_platform", "main"]

Platform = Literal["claude", "opencode", "gemini", "codex"]

# Platform config: (config_base_dir, mcp_config_path, mcp_config_format)
PLATFORM_CONFIG: dict[Platform, tuple[str, str, Literal["json", "toml"]]] = {
    "claude": ("claude", "claude.json", "json"),
    "opencode": ("config/opencode", "opencode.json", "json"),
    "gemini": ("gemini", "settings.json", "json"),
    "codex": ("codex", "config.toml", "toml"),
}

MCP_SERVER_ENTRY = {"command": "uvx", "args": ["mcp-vm-blackbox"]}


console = make_console(stderr=False)


def _emit_install_banner(repo_root: Path, *, banner_console: Console | None = None) -> None:
    """Emit the installer header. ``banner_console`` overrides module ``console`` for tests."""
    out = banner_console if banner_console is not None else console
    banner = Panel(
        f"[bold]vm-flightsimulator installer[/]\n[dim]Repo: {repo_root}[/]",
        title=":package: Install",
        border_style="cyan",
    )
    if sys.stdout.isatty():
        out.print(banner)
    else:
        print_measured_panel(out, banner)


def _resolve_base(global_install: bool, cwd: Path) -> Path:
    """Resolve base directory for global vs local install."""
    if global_install:
        return Path.home()
    return cwd


def _get_platform_paths(platform: Platform, global_install: bool, repo_root: Path) -> tuple[Path, Path, Path]:
    """Return (config_dir, skills_dest, agents_dest) for the platform."""
    base = _resolve_base(global_install, repo_root)
    config_key, _, _ = PLATFORM_CONFIG[platform]

    if platform == "opencode":
        config_dir = base / ".config" / "opencode" if global_install else repo_root / ".opencode"
    else:
        dot_key = f".{config_key}"
        config_dir = base / dot_key if global_install else repo_root / dot_key

    skills_dest = config_dir / "plugins" / "vm-flightsimulator" / "skills"
    agents_dest = config_dir / "plugins" / "vm-flightsimulator" / "agents"

    return config_dir, skills_dest, agents_dest


def _get_mcp_config_path(platform: Platform, global_install: bool, repo_root: Path) -> Path:
    """Return the path to the MCP config file."""
    base = _resolve_base(global_install, repo_root)
    _, config_file, _ = PLATFORM_CONFIG[platform]

    match platform:
        case "opencode":
            return (
                base / ".config" / "opencode" / config_file if global_install else repo_root / ".opencode" / config_file
            )
        case "claude":
            return base / f".{config_file}" if global_install else repo_root / f".{config_file}"
        case "gemini":
            config_dir = base / ".gemini" if global_install else repo_root / ".gemini"
            return config_dir / config_file
        case "codex":
            config_dir = base / ".codex" if global_install else repo_root / ".codex"
            return config_dir / config_file
        case _:
            raise ValueError(f"Unknown platform: {platform}")


def _parse_frontmatter(content: str) -> tuple[str | None, str, str]:
    """Extract YAML frontmatter from Markdown. Returns (frontmatter, body, raw_delim)."""
    match = re.match(r"^---\r?\n(.*?)\r?\n---\r?\n(.*)$", content, re.DOTALL)
    if not match:
        return None, content, ""
    return match.group(1), match.group(2), "---"


def _convert_comma_separated_to_yaml_list(value: str) -> str:
    """Convert 'a, b, c' to YAML list format."""
    items = [s.strip() for s in value.split(",") if s.strip()]
    if not items:
        return "[]"
    return "\n" + "\n".join(f"- {item}" for item in items)


def _read_indented_block(lines: list[str], i: int) -> tuple[str, int]:
    """Read YAML indented block. Returns (value, new_index)."""
    acc: list[str] = []
    while i < len(lines) and (lines[i].startswith(" ") or lines[i].startswith("\t")):
        acc.append(lines[i].strip())
        i += 1
    return " ".join(acc), i


def _extract_yaml_value(lines: list[str], i: int) -> tuple[str, int]:
    """Extract value for a key; handle multiline folded/literal. Returns (value, new_i)."""
    line = lines[i]
    rest = line.split(":", 1)[1].strip() if ":" in line else ""
    if rest in {"|-", ">-", "|", ">"}:
        return _read_indented_block(lines, i + 1)
    if not rest and i + 1 < len(lines) and (lines[i + 1].startswith(" ") or lines[i + 1].startswith("\t")):
        return _read_indented_block(lines, i + 1)
    return rest, i


def _transform_frontmatter_for_gemini(frontmatter: str) -> str:
    """Remove color, convert tools/skills comma-separated strings to YAML lists."""
    lines = frontmatter.split("\n")
    result: list[str] = []
    i = 0
    while i < len(lines):
        line = lines[i]
        stripped = line.strip()
        if stripped.startswith("color:"):
            i += 1
            continue
        if stripped.startswith(("tools:", "skills:")):
            key = stripped.split(":")[0]
            value, i = _extract_yaml_value(lines, i)
            list_yaml = _convert_comma_separated_to_yaml_list(value)
            result.append(f"{key}:{list_yaml}")
        else:
            result.append(line)
        i += 1
    return "\n".join(result)


def _process_markdown_for_platform(content: str, platform: Platform) -> str:
    """Transform Markdown frontmatter for target platform."""
    if platform != "gemini":
        return content

    frontmatter, body, delim = _parse_frontmatter(content)
    if not frontmatter:
        return content

    transformed = _transform_frontmatter_for_gemini(frontmatter)
    return f"{delim}\n{transformed}\n{delim}\n{body}"


class CopyStats(TypedDict, total=False):
    files_copied: int


def _copy_tree_with_transform(src: Path, dest: Path, platform: Platform, stats: CopyStats) -> None:
    """Copy directory tree, transforming .md files for platform."""
    for item in src.rglob("*"):
        rel = item.relative_to(src)
        dest_file = dest / rel

        if item.is_dir():
            dest_file.mkdir(parents=True, exist_ok=True)
            continue

        if item.suffix.lower() == ".md":
            content = item.read_text(encoding="utf-8", errors="replace")
            transformed = _process_markdown_for_platform(content, platform)
            dest_file.parent.mkdir(parents=True, exist_ok=True)
            dest_file.write_text(transformed, encoding="utf-8")
            stats["files_copied"] += 1
        else:
            dest_file.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(item, dest_file)
            stats["files_copied"] += 1


def _merge_json_mcp_config(config_path: Path, server_name: str, entry: dict) -> None:
    """Read JSON config, merge MCP server entry, write back."""
    if config_path.exists():
        try:
            data = json.loads(config_path.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError) as e:
            e.add_note(f"Config path: {config_path}")
            console.print(f"  [red]Error:[/] Invalid or unreadable config: {e}")
            raise typer.Exit(1) from e
    else:
        data = {}

    if "mcpServers" not in data:
        data["mcpServers"] = {}
    data["mcpServers"][server_name] = entry

    config_path.parent.mkdir(parents=True, exist_ok=True)
    config_path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")


def _merge_toml_mcp_config_stdlib(config_path: Path, server_name: str, entry: dict) -> None:
    """Append MCP server to TOML using stdlib only."""
    config_path.parent.mkdir(parents=True, exist_ok=True)

    section_underscore = f"mcp_servers.{server_name.replace('-', '_')}"
    section_hyphen = f'mcp_servers."{server_name}"'
    args_str = json.dumps(entry["args"])
    cmd = entry["command"]
    block = f'\n[{section_underscore}]\ncommand = "{cmd}"\nargs = {args_str}\n'

    if config_path.exists():
        content = config_path.read_text(encoding="utf-8")
        if f"[{section_underscore}]" in content or section_hyphen in content:
            return
        content = content.rstrip() + block
    else:
        content = f"# vm-flightsimulator MCP server{block}"

    config_path.write_text(content, encoding="utf-8")


def _register_mcp(platform: Platform, global_install: bool, repo_root: Path) -> None:
    """Register MCP server in platform config file."""
    config_path = _get_mcp_config_path(platform, global_install, repo_root)
    _, _, fmt = PLATFORM_CONFIG[platform]

    if fmt == "json":
        _merge_json_mcp_config(config_path, "vm-blackbox", MCP_SERVER_ENTRY)
    else:
        _merge_toml_mcp_config_stdlib(config_path, "vm-blackbox", MCP_SERVER_ENTRY)


def install_platform(
    platform: Platform, global_install: bool, repo_root: Path, skills_src: Path, agents_src: Path
) -> bool:
    """Install to one platform. Returns True on success."""
    _config_dir, skills_dest, agents_dest = _get_platform_paths(platform, global_install, repo_root)
    scope = "global" if global_install else "local"
    console.print(f"\n[bold cyan]→ {platform.upper()} ({scope})[/]")

    stats: CopyStats = {"files_copied": 0}

    if skills_src.exists():
        _copy_tree_with_transform(skills_src, skills_dest, platform, stats)
        console.print(f"  [green]✓[/] Copied skills to {skills_dest}")
    else:
        console.print("  [yellow]![/] skills/ not found, skipping")

    if agents_src.exists():
        _copy_tree_with_transform(agents_src, agents_dest, platform, stats)
        console.print(f"  [green]✓[/] Copied agents to {agents_dest}")
    else:
        console.print("  [yellow]![/] agents/ not found, skipping")

    try:
        _register_mcp(platform, global_install, repo_root)
        mcp_path = _get_mcp_config_path(platform, global_install, repo_root)
        console.print(f"  [green]✓[/] Registered MCP server in {mcp_path}")
    except OSError as e:
        console.print(f"  [red]✗[/] Failed to register MCP: {e}")
        return False

    return True


app = typer.Typer(
    name="vm-blackbox-installer",
    help="Install vm-flightsimulator skills, agents, and MCP server to AI CLI tools.",
    epilog="""
Examples:
  vm-blackbox-installer --all --global
  vm-blackbox-installer --claude --gemini --local
  vm-blackbox-installer --codex --global
""",
)


@app.callback(invoke_without_command=True)
def _run(
    claude: Annotated[bool, typer.Option("--claude", help="Install for Claude Code")] = False,
    opencode: Annotated[bool, typer.Option("--opencode", help="Install for OpenCode")] = False,
    gemini: Annotated[bool, typer.Option("--gemini", help="Install for Gemini CLI")] = False,
    codex: Annotated[bool, typer.Option("--codex", help="Install for Codex")] = False,
    all_platforms: Annotated[bool, typer.Option("--all", help="Install for all platforms")] = False,
    global_install: Annotated[bool, typer.Option("--global", help="Install to user home (~/.claude, etc.)")] = False,
    local_install: Annotated[
        bool, typer.Option("--local", help="Install to current directory (.claude, .gemini, etc.)")
    ] = False,
) -> None:
    """Install vm-flightsimulator skills, agents, and MCP server to AI CLI tools."""
    if global_install and local_install:
        raise typer.BadParameter("Use either --global or --local, not both")

    targets: list[Platform] = []
    if all_platforms:
        targets = ["claude", "opencode", "gemini", "codex"]
    else:
        if claude:
            targets.append("claude")
        if opencode:
            targets.append("opencode")
        if gemini:
            targets.append("gemini")
        if codex:
            targets.append("codex")

    if not targets:
        console.print("[red]Error:[/] Specify at least one platform (--claude, --opencode, --gemini, --codex) or --all")
        raise typer.Exit(1)

    install_scope = global_install or (not local_install)

    repo_root = Path(__file__).resolve().parent
    skills_src = repo_root / "skills"
    agents_src = repo_root / "agents"

    _emit_install_banner(repo_root)

    ok = True
    for platform in targets:
        if not install_platform(platform, install_scope, repo_root, skills_src, agents_src):
            ok = False

    console.print()
    if ok:
        console.print("[green bold]Done.[/]")
    else:
        console.print("[yellow]Completed with warnings.[/]")
        raise typer.Exit(1)


def main() -> None:
    """Entry point for project.scripts."""
    app()


if __name__ == "__main__":
    main()
