"""Helpers to install AIDA (AI Developer Assistant) into assistant configs.

This module is used by `aida-mcp install-mcp`.
It is meant to work when `aida-mcp` is installed as a tool and invoked outside of the repo.
"""

from __future__ import annotations

import os
import shutil
import subprocess
import sys
from collections.abc import Iterable, Sequence
from datetime import datetime
from pathlib import Path
from typing import cast

import orjson


def log(message: str) -> None:
    print(f"[aida-mcp][install-mcp] {message}")


def err(message: str) -> None:
    print(f"[aida-mcp][install-mcp][error] {message}", file=sys.stderr)


def _dedupe_targets(raw: Iterable[str]) -> list[str]:
    ordered: list[str] = []
    seen: set[str] = set()
    for item in raw:
        if item not in seen:
            ordered.append(item)
            seen.add(item)
    return ordered


def resolve_targets(raw_targets: Sequence[str] | None) -> list[str]:
    """Resolve targets; supports `all` and `jetbrains` expansions."""

    if not raw_targets:
        return []

    expanded: list[str] = []
    for entry in raw_targets:
        if entry == "all":
            expanded.extend(["cursor", "claude", "jetbrains", "junie"])
        elif entry == "jetbrains":
            expanded.extend(["jetbrains", "junie"])
        else:
            expanded.append(entry)
    return _dedupe_targets(expanded)


def _find_in_path(command: str) -> Path | None:
    result = shutil.which(command)
    return Path(result) if result else None


def _npm_prefix_path() -> Path | None:
    try:
        completed = subprocess.run(
            ["npm", "config", "get", "prefix"],
            check=True,
            capture_output=True,
            text=True,
        )
    except (subprocess.CalledProcessError, FileNotFoundError):
        return None
    value = completed.stdout.strip()
    return Path(value) if value else None


def _newest_nvm_claude(nvm_dir: Path) -> Path | None:
    node_dir = nvm_dir / "versions" / "node"
    if not node_dir.exists():
        return None

    candidates = [path for path in node_dir.iterdir() if path.is_dir()]
    if not candidates:
        return None

    def version_key(path: Path) -> list[tuple[int, object]]:
        key: list[tuple[int, object]] = []
        for part in path.name.split("."):
            try:
                key.append((0, int(part)))
            except ValueError:
                key.append((1, part))
        return key

    candidates.sort(key=version_key)
    candidate = candidates[-1] / "bin" / "claude"
    return candidate if candidate.exists() else None


def resolve_claude_bin() -> Path | None:
    env_candidate = os.environ.get("CLAUDE_BIN")
    if env_candidate:
        path = Path(env_candidate).expanduser()
        if path.exists():
            return path

    path_candidate = _find_in_path("claude")
    if path_candidate:
        return path_candidate

    prefix = _npm_prefix_path()
    if prefix:
        candidate = prefix / "bin" / "claude"
        if candidate.exists():
            return candidate

    nvm_dir = Path(os.environ.get("NVM_DIR", Path.home() / ".nvm"))
    candidate = _newest_nvm_claude(nvm_dir)
    if candidate and os.access(candidate, os.X_OK):
        return candidate

    return None


def backup_file(path: Path) -> None:
    if not path.exists():
        return
    timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    backup_path = path.with_name(f"{path.name}.{timestamp}.bak")
    shutil.copy2(path, backup_path)


def _make_server_entry(
    *,
    command: str,
    args: Sequence[str],
    workspace_root: str | None,
    config_root: str | None,
    extra_env: dict[str, str] | None = None,
) -> dict[str, object]:
    entry: dict[str, object] = {"command": command, "args": list(args)}

    env: dict[str, str] = {}
    if extra_env:
        env.update(extra_env)
    if workspace_root:
        env["AIDA_WORKSPACE_ROOT"] = workspace_root
    if config_root:
        env["AIDA_CONFIG_ROOT"] = config_root
    if env:
        entry["env"] = env
    return entry


def write_mcp_json_config(
    *,
    dest: Path,
    command_path: Path,
    args: Sequence[str] = (),
    workspace_root: str | None = None,
    config_root: str | None = None,
    extra_env: dict[str, str] | None = None,
) -> None:
    dest.parent.mkdir(parents=True, exist_ok=True)

    data: dict[str, object] = {}
    if dest.exists():
        try:
            loaded: object = orjson.loads(dest.read_text(encoding="utf-8"))
            if isinstance(loaded, dict):
                data = cast(dict[str, object], loaded)
        except Exception:
            data = {}

    servers_raw: object = data.get("mcpServers")
    if isinstance(servers_raw, dict):
        servers: dict[str, object] = cast(dict[str, object], servers_raw)
    else:
        servers = {}
        data["mcpServers"] = servers

    servers["aida"] = _make_server_entry(
        command=str(command_path),
        args=args,
        workspace_root=workspace_root,
        config_root=config_root,
        extra_env=extra_env,
    )
    dest.write_text(orjson.dumps(data, option=orjson.OPT_INDENT_2).decode("utf-8") + "\n", encoding="utf-8")


def install_for_cursor(
    *,
    command_path: Path,
    args: Sequence[str] = (),
    workspace_root: str | None = None,
    config_root: str | None = None,
    allow_global: bool = False,
) -> None:
    if not allow_global:
        err("Refusing to write global Cursor MCP config without --global.")
        err("Recommended: run `aida-mcp init` in the repo root for repo-local config.")
        raise SystemExit(2)
    dest = Path.home() / ".cursor" / "mcp.json"
    log(f"Installing MCP config for Cursor at {dest}")
    backup_file(dest)
    write_mcp_json_config(
        dest=dest,
        command_path=command_path,
        args=args,
        workspace_root=workspace_root,
        config_root=config_root,
    )


def install_for_jetbrains(
    *,
    command_path: Path,
    args: Sequence[str] = (),
    workspace_root: str | None = None,
    config_root: str | None = None,
    allow_global: bool = False,
) -> None:
    if not allow_global:
        err("Refusing to write global JetBrains MCP config without --global.")
        err("Recommended: run `aida-mcp init` in the repo root for repo-local config.")
        raise SystemExit(2)
    dest = Path.home() / ".ai" / "mcp" / "mcp.json"
    log(f"Installing MCP config for JetBrains AI at {dest}")
    backup_file(dest)
    write_mcp_json_config(
        dest=dest,
        command_path=command_path,
        args=args,
        workspace_root=workspace_root,
        config_root=config_root,
    )


def install_for_junie(
    *,
    command_path: Path,
    args: Sequence[str] = (),
    workspace_root: str | None = None,
    config_root: str | None = None,
    allow_global: bool = False,
) -> None:
    if not allow_global:
        err("Refusing to write global Junie MCP config without --global.")
        err("Recommended: run `aida-mcp init` in the repo root for repo-local config.")
        raise SystemExit(2)
    dest = Path.home() / ".junie" / "mcp" / "mcp.json"
    log(f"Installing MCP config for Junie at {dest}")
    backup_file(dest)
    write_mcp_json_config(
        dest=dest,
        command_path=command_path,
        args=args,
        workspace_root=workspace_root,
        config_root=config_root,
    )


def install_for_claude(
    *,
    command_path: Path,
    args: Sequence[str] = (),
    workspace_root: str | None = None,
    config_root: str | None = None,
    allow_global: bool = False,
) -> None:
    if not allow_global:
        err("Refusing to configure Claude Code MCP without --global.")
        err("Recommended: run `aida-mcp init` in the repo root for repo-local .mcp.json.")
        raise SystemExit(2)
    claude_cli = resolve_claude_bin()
    if not claude_cli:
        err("Claude CLI not found. Install it with `npm i -g @anthropic-ai/claude-code` or set CLAUDE_BIN to its path.")
        err("If you use nvm, ensure nvm is initialized so 'claude' is on PATH.")
        raise SystemExit(1)

    log(f"Configuring Claude Code via CLI ({claude_cli})")

    # Remove existing entry (if present) to keep the operation idempotent.
    try:
        get_result = subprocess.run(
            [str(claude_cli), "mcp", "get", "aida"],
            check=False,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
    except FileNotFoundError:
        err(f"Claude CLI not executable at {claude_cli}")
        raise SystemExit(1)

    if get_result.returncode == 0:
        log("Existing aida entry detected; removing before re-adding")
        remove = subprocess.run(
            [str(claude_cli), "mcp", "remove", "aida"],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.PIPE,
            text=True,
        )
        if remove.returncode != 0:
            err("Failed to remove existing aida entry from Claude CLI")
            err(remove.stderr.strip())
            raise SystemExit(remove.returncode)

    # Claude CLI does not expose structured env injection in this script interface.
    # For optional pinning, wrap the command in `/usr/bin/env KEY=VAL ...`.
    cmd: list[str]
    if workspace_root or config_root:
        cmd = ["/usr/bin/env"]
        if workspace_root:
            cmd.append(f"AIDA_WORKSPACE_ROOT={workspace_root}")
        if config_root:
            cmd.append(f"AIDA_CONFIG_ROOT={config_root}")
        cmd.append(str(command_path))
        cmd.extend(args)
    else:
        cmd = [str(command_path), *args]

    result = subprocess.run(
        [str(claude_cli), "mcp", "add", "--transport", "stdio", "aida", "--", *cmd],
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        err("Claude CLI failed to register aida MCP server")
        err(result.stderr.strip())
        raise SystemExit(result.returncode)
    log("Claude CLI now trusts aida MCP server")


def resolve_self_command_path(argv0: str) -> Path:
    """Resolve the currently running `aida-mcp` command path."""

    candidate = Path(argv0).expanduser()
    if not candidate.is_absolute():
        candidate = (Path.cwd() / candidate).resolve()
    if candidate.exists():
        return candidate

    which = _find_in_path("aida-mcp")
    if which:
        return which

    raise RuntimeError("Unable to resolve aida-mcp command path")
