"""Migration CLI: converts per-server BPA entries into multi-instance profiles.

Users who installed via `setup` have N separate MCP server entries (one per
BPA instance) in Claude Desktop and Claude Code configs. This wastes resources
(N processes), floods tool context, and creates config sprawl.

The `migrate` command converts old per-server entries into named profiles and
cleans up config files.

Usage:
    mcp-eregistrations-bpa migrate          # dry-run (default, safe)
    mcp-eregistrations-bpa migrate --apply  # execute migration
"""

from __future__ import annotations

import json
import shutil
import sys
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any

from mcp_eregistrations_bpa.config import (
    _generate_instance_slug,
    load_known_instances,
    load_profiles,
    save_profiles,
)
from mcp_eregistrations_bpa.setup_cli import (
    _BLUE,
    _BOLD,
    _GREEN,
    _NC,
    _RED,
    _YELLOW,
    PACKAGE_NAME,
    _get_claude_code_config_path,
    _get_claude_desktop_config_path,
)

# ---------------------------------------------------------------------------
# Canonical URL → instance metadata (loaded from bundled instances.yaml)
# ---------------------------------------------------------------------------

_URL_TO_CANONICAL = load_known_instances()


# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------


@dataclass
class DiscoveredEntry:
    """A BPA server entry found in a Claude config file."""

    server_name: str
    config_source: str  # "desktop" or "code"
    bpa_url: str
    env: dict[str, str]
    command: str = ""
    args: list[str] = field(default_factory=list)
    # Classification (set during Phase 2)
    action: str = "pending"  # migrate | skip_local | skip_duplicate
    canonical_name: str = ""
    duplicate_of: str = ""
    auth_type: str = "keycloak"  # keycloak | cas


@dataclass
class MigrationPlan:
    """Complete migration plan computed from discovered entries."""

    entries: list[DiscoveredEntry] = field(default_factory=list)
    profiles_to_create: dict[str, dict[str, Any]] = field(default_factory=dict)
    cas_secrets: dict[str, str] = field(default_factory=dict)
    servers_to_remove: dict[str, list[str]] = field(
        default_factory=lambda: {"desktop": [], "code": []}
    )
    stale_references: list[tuple[str, str, str]] = field(default_factory=list)
    # (project_path, array_key, server_name)
    skipped_existing: list[str] = field(default_factory=list)


# ---------------------------------------------------------------------------
# Detection helpers
# ---------------------------------------------------------------------------


def _is_bpa_server(server_config: dict[str, Any]) -> bool:
    """Check if an mcpServers entry is a BPA server."""
    command = server_config.get("command", "")
    args = server_config.get("args", [])
    # Match "mcp-eregistrations-bpa" anywhere in command or args
    haystack = f"{command} {' '.join(str(a) for a in args)}"
    return PACKAGE_NAME in haystack


def _is_local_entry(server_name: str, args: list[str]) -> bool:
    """Check if a server entry is a local dev entry that should be kept."""
    name_lower = server_name.lower()
    # Name-based detection
    if name_lower.endswith("-local"):
        return True
    if name_lower.startswith("bpa-local-"):
        return True
    # Args-based detection (running from source with --directory)
    if "--directory" in args:
        return True
    return False


def _resolve_canonical_name(server_name: str, bpa_url: str) -> tuple[str, str]:
    """Resolve canonical profile name and auth type for a BPA URL.

    Returns:
        (canonical_name, auth_type)
    """
    normalized_url = bpa_url.rstrip("/")

    # Priority 1: URL match in canonical instances
    if normalized_url in _URL_TO_CANONICAL:
        info = _URL_TO_CANONICAL[normalized_url]
        return info["name"], info["auth"]

    # Priority 2: Strip BPA- prefix, lowercase
    name = server_name
    if name.upper().startswith("BPA-"):
        name = name[4:]
    return name.lower(), "keycloak"


def _build_profile_dict(entry: DiscoveredEntry) -> dict[str, Any]:
    """Build a profile dict from a discovered entry, using canonical metadata."""
    normalized_url = entry.bpa_url.rstrip("/")

    # Use canonical metadata if available (preferred — has correct auth config)
    if normalized_url in _URL_TO_CANONICAL:
        info = _URL_TO_CANONICAL[normalized_url]
        profile: dict[str, Any] = {"bpa_instance_url": normalized_url}
        if info.get("ds_url"):
            profile["ds_url"] = info["ds_url"]
        if info["auth"] == "keycloak":
            profile["keycloak_url"] = info["keycloak_url"]
            profile["keycloak_realm"] = info["keycloak_realm"]
            profile["keycloak_client_id"] = PACKAGE_NAME
        elif info["auth"] == "cas":
            profile["cas_url"] = info["cas_url"]
            profile["cas_client_id"] = info.get("cas_client_id", "mcp-bpa")
            # CAS secret handled separately (Phase 4)
            if entry.env.get("CAS_CLIENT_SECRET"):
                profile["cas_client_secret"] = "__keyring__"
        return profile

    # Fallback: reconstruct from env vars
    profile = {"bpa_instance_url": normalized_url}
    env = entry.env
    if env.get("KEYCLOAK_URL"):
        profile["keycloak_url"] = env["KEYCLOAK_URL"]
    if env.get("KEYCLOAK_REALM"):
        profile["keycloak_realm"] = env["KEYCLOAK_REALM"]
    if env.get("KEYCLOAK_CLIENT_ID"):
        profile["keycloak_client_id"] = env["KEYCLOAK_CLIENT_ID"]
    if env.get("CAS_URL"):
        profile["cas_url"] = env["CAS_URL"]
    if env.get("CAS_CLIENT_ID"):
        profile["cas_client_id"] = env["CAS_CLIENT_ID"]
    if env.get("CAS_CLIENT_SECRET"):
        profile["cas_client_secret"] = "__keyring__"
    return profile


# ---------------------------------------------------------------------------
# Config file I/O
# ---------------------------------------------------------------------------


def _read_config(path: Path) -> dict[str, Any] | None:
    """Read a JSON config file. Returns None on missing/corrupt files."""
    if not path.exists():
        return None
    try:
        result: dict[str, Any] | None = json.loads(path.read_text(encoding="utf-8"))
        return result
    except (json.JSONDecodeError, OSError):
        return None


def _backup_config(path: Path) -> Path | None:
    """Create a timestamped backup of a config file."""
    if not path.exists():
        return None
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_path = path.with_suffix(f".backup.{timestamp}.json")
    try:
        shutil.copy2(path, backup_path)
        return backup_path
    except OSError:
        return None


def _write_config(path: Path, data: dict[str, Any]) -> bool:
    """Write JSON config back to disk."""
    try:
        path.write_text(
            json.dumps(data, indent=2, ensure_ascii=False) + "\n",
            encoding="utf-8",
        )
        return True
    except OSError:
        return False


# ---------------------------------------------------------------------------
# Discovery (Phase 1)
# ---------------------------------------------------------------------------


def _discover_entries(
    config_data: dict[str, Any], source: str
) -> list[DiscoveredEntry]:
    """Extract BPA server entries from an mcpServers dict."""
    entries: list[DiscoveredEntry] = []
    mcp_servers = config_data.get("mcpServers", {})

    for name, server_config in mcp_servers.items():
        if not isinstance(server_config, dict):
            continue
        if not _is_bpa_server(server_config):
            continue

        env = server_config.get("env", {})
        bpa_url = env.get("BPA_INSTANCE_URL", "")
        if not bpa_url:
            continue

        entries.append(
            DiscoveredEntry(
                server_name=name,
                config_source=source,
                bpa_url=bpa_url.rstrip("/"),
                env=env,
                command=server_config.get("command", ""),
                args=server_config.get("args", []),
            )
        )

    return entries


# ---------------------------------------------------------------------------
# Classification (Phase 2)
# ---------------------------------------------------------------------------


def _classify_entries(entries: list[DiscoveredEntry]) -> list[DiscoveredEntry]:
    """Classify entries: local, migrate, or duplicate."""
    # Step 1: Mark local entries
    for entry in entries:
        if _is_local_entry(entry.server_name, entry.args):
            entry.action = "skip_local"
            continue

        # Resolve canonical name
        canonical, auth = _resolve_canonical_name(entry.server_name, entry.bpa_url)
        entry.canonical_name = canonical
        entry.auth_type = auth

    # Step 2: Group non-local entries by normalized bpa_url
    url_groups: dict[str, list[DiscoveredEntry]] = {}
    for entry in entries:
        if entry.action == "skip_local":
            continue
        url = entry.bpa_url.rstrip("/")
        url_groups.setdefault(url, []).append(entry)

    # Step 3: First entry per URL → migrate, rest → duplicate
    for url, group in url_groups.items():
        # Use the first entry's canonical name for the group
        canonical_name = group[0].canonical_name
        group[0].action = "migrate"

        for dup in group[1:]:
            dup.action = "skip_duplicate"
            dup.duplicate_of = canonical_name
            dup.canonical_name = canonical_name

    return entries


# ---------------------------------------------------------------------------
# Plan computation (Phases 3-5)
# ---------------------------------------------------------------------------


def _compute_plan(
    desktop_data: dict[str, Any] | None,
    code_data: dict[str, Any] | None,
) -> MigrationPlan:
    """Compute the full migration plan without modifying anything."""
    plan = MigrationPlan()

    # Phase 1: Discovery
    if desktop_data:
        plan.entries.extend(_discover_entries(desktop_data, "desktop"))
    if code_data:
        plan.entries.extend(_discover_entries(code_data, "code"))

    # Phase 2: Classification
    _classify_entries(plan.entries)

    # Phase 3: Merge with existing profiles
    existing_profiles = load_profiles()
    seen_canonical: set[str] = set()

    for entry in plan.entries:
        if entry.action != "migrate":
            continue
        if entry.canonical_name in seen_canonical:
            continue
        seen_canonical.add(entry.canonical_name)

        if entry.canonical_name in existing_profiles:
            plan.skipped_existing.append(entry.canonical_name)
            continue

        profile = _build_profile_dict(entry)
        plan.profiles_to_create[entry.canonical_name] = profile

    # Phase 4: CAS secret handling
    for entry in plan.entries:
        if entry.action not in ("migrate", "skip_duplicate"):
            continue
        secret = entry.env.get("CAS_CLIENT_SECRET")
        if secret:
            slug = _generate_instance_slug(entry.bpa_url)
            if slug not in plan.cas_secrets:
                plan.cas_secrets[slug] = secret

    # Phase 5: Config cleanup planning
    for entry in plan.entries:
        if entry.action == "skip_local":
            continue
        source = entry.config_source
        if entry.server_name not in plan.servers_to_remove[source]:
            plan.servers_to_remove[source].append(entry.server_name)

    # Scan .claude.json project scopes for stale BPA-* references
    if code_data:
        projects = code_data.get("projects", {})
        # Collect all non-local BPA server names we're removing
        removing = set()
        for entry in plan.entries:
            if entry.action != "skip_local":
                removing.add(entry.server_name)

        for project_path, project_config in projects.items():
            if not isinstance(project_config, dict):
                continue
            for key in ("disabledMcpServers", "enabledMcpServers"):
                arr = project_config.get(key, [])
                if not isinstance(arr, list):
                    continue
                for name in arr:
                    if name in removing:
                        plan.stale_references.append((project_path, key, name))

    return plan


# ---------------------------------------------------------------------------
# Output formatting
# ---------------------------------------------------------------------------


def _print_banner() -> None:
    print()
    print(f"{_BLUE}{'=' * 56}{_NC}")
    print(f"{_BLUE}    eRegistrations BPA — Migration Tool{_NC}")
    print(f"{_BLUE}{'=' * 56}{_NC}")
    print()


def _print_plan(
    plan: MigrationPlan,
    desktop_data: dict[str, Any] | None,
    code_data: dict[str, Any] | None,
) -> None:
    """Print the migration plan in a human-readable format."""
    # Discovery summary
    desktop_count = sum(1 for e in plan.entries if e.config_source == "desktop")
    code_count = sum(1 for e in plan.entries if e.config_source == "code")
    code_with_defs = sum(
        1 for e in plan.entries if e.config_source == "code" and e.command
    )

    if desktop_data is not None:
        print("  Scanning Claude Desktop config...")
        print(f"    Found {_BOLD}{desktop_count}{_NC} BPA server entries")
    else:
        print(f"  {_YELLOW}Claude Desktop config not found{_NC}")

    if code_data is not None:
        print("  Scanning Claude Code config...")
        if code_count > 0:
            extra = (
                f" ({code_with_defs} with definitions)"
                if code_with_defs != code_count
                else ""
            )
            print(f"    Found {_BOLD}{code_count}{_NC} BPA server entries{extra}")
        else:
            print(f"    Found {_BOLD}0{_NC} BPA server entries")
    else:
        print(f"  {_YELLOW}Claude Code config not found{_NC}")

    print()

    # Check if there's anything to do
    migrate_entries = [e for e in plan.entries if e.action == "migrate"]
    dup_entries = [e for e in plan.entries if e.action == "skip_duplicate"]
    local_entries = [e for e in plan.entries if e.action == "skip_local"]

    if not migrate_entries and not dup_entries:
        print(f"  {_GREEN}Nothing to migrate.{_NC}")
        if plan.skipped_existing:
            print(
                f"  All discovered instances already exist as profiles: "
                f"{', '.join(plan.skipped_existing)}"
            )
        print()
        return

    # Migration plan details
    print(f"  {_BOLD}Migration Plan:{_NC}")

    # Migrate entries
    for entry in plan.entries:
        if entry.action != "migrate":
            continue
        name = entry.canonical_name
        auth_label = "Keycloak"
        if entry.auth_type == "cas":
            auth_label = "CAS"
            if entry.env.get("CAS_CLIENT_SECRET"):
                auth_label += ", secret → keyring"

        status = "EXISTS" if name in plan.skipped_existing else "MIGRATE"
        color = _YELLOW if status == "EXISTS" else _GREEN

        print(
            f"    {color}{status:9s}{_NC} {entry.server_name:<36s} "
            f'→ profile {_BOLD}"{name}"{_NC} ({auth_label})'
        )

    # Duplicate entries
    for entry in plan.entries:
        if entry.action != "skip_duplicate":
            continue
        print(
            f"    {_YELLOW}DUPLICATE{_NC} {entry.server_name:<36s} "
            f'→ same URL as "{entry.duplicate_of}", will remove'
        )

    # Local entries
    for entry in plan.entries:
        if entry.action != "skip_local":
            continue
        print(
            f"    {_BLUE}SKIP     {_NC} {entry.server_name:<36s} "
            f"→ local dev entry (kept)"
        )

    # Summary
    print()
    print(f"  {_BOLD}Summary:{_NC}")

    profiles_count = len(plan.profiles_to_create)
    # Deduplicate: same server name may appear in both desktop and code
    unique_remove = len(
        set(plan.servers_to_remove["desktop"]) | set(plan.servers_to_remove["code"])
    )

    print(f"    Profiles to create: {_BOLD}{profiles_count}{_NC}")
    print(f"    Servers to remove:  {_BOLD}{unique_remove}{_NC}")
    print(f"    Local entries kept: {_BOLD}{len(local_entries)}{_NC}")
    if plan.skipped_existing:
        print(f"    Already existing:   {_BOLD}{len(plan.skipped_existing)}{_NC}")

    stale_count = len(plan.stale_references)
    if stale_count:
        print(f"    Stale project references to clean: {_BOLD}{stale_count}{_NC}")

    print()


# ---------------------------------------------------------------------------
# Execution (Phase 6)
# ---------------------------------------------------------------------------


def _execute_plan(
    plan: MigrationPlan,
    desktop_path: Path,
    code_path: Path,
    desktop_data: dict[str, Any] | None,
    code_data: dict[str, Any] | None,
) -> bool:
    """Execute the migration plan. Returns True on success."""
    success = True

    # Step 1: Create profiles
    if plan.profiles_to_create:
        existing = load_profiles()
        existing.update(plan.profiles_to_create)
        save_profiles(existing)
        print(
            f"  {_GREEN}Created {len(plan.profiles_to_create)} profile(s){_NC} "
            f"in profiles.json"
        )

    # Step 2: Store CAS secrets in keyring
    if plan.cas_secrets:
        from mcp_eregistrations_bpa.auth.credentials import store_cas_secret

        for slug, secret in plan.cas_secrets.items():
            stored = store_cas_secret(slug, secret)
            if stored:
                print(f"  {_GREEN}Stored CAS secret{_NC} for {slug} in keyring")
            else:
                print(
                    f"  {_YELLOW}Warning: could not store CAS secret for {slug} "
                    f"in keyring{_NC}"
                )

    # Step 3: Backup and modify config files
    configs: list[tuple[str, Path, dict[str, Any] | None, list[str]]] = [
        (
            "Claude Desktop",
            desktop_path,
            desktop_data,
            plan.servers_to_remove["desktop"],
        ),
        ("Claude Code", code_path, code_data, plan.servers_to_remove["code"]),
    ]

    for label, path, data, to_remove in configs:
        if data is None or not to_remove:
            continue

        # Backup
        backup = _backup_config(path)
        if backup:
            print(f"  {_GREEN}Backup{_NC} {label}: {backup.name}")
        else:
            print(f"  {_YELLOW}Warning: could not backup {label} config{_NC}")

        # Remove BPA-* entries from mcpServers
        mcp_servers = data.get("mcpServers", {})
        removed = 0
        for name in to_remove:
            if name in mcp_servers:
                del mcp_servers[name]
                removed += 1

        print(f"  {_GREEN}Removed {removed} server(s){_NC} from {label} mcpServers")

        # Write back
        if not _write_config(path, data):
            print(f"  {_RED}Error writing {label} config!{_NC}")
            success = False

    # Step 4: Clean stale project references in .claude.json
    if plan.stale_references and code_data is not None:
        projects = code_data.get("projects", {})
        cleaned = 0
        for project_path, key, name in plan.stale_references:
            proj = projects.get(project_path)
            if not proj or not isinstance(proj, dict):
                continue
            arr = proj.get(key, [])
            if isinstance(arr, list) and name in arr:
                arr.remove(name)
                cleaned += 1

        if cleaned:
            # Write code config again (stale refs cleaned)
            if _write_config(code_path, code_data):
                print(
                    f"  {_GREEN}Cleaned {cleaned} stale reference(s){_NC} "
                    f"from project scopes"
                )
            else:
                print(
                    f"  {_RED}Error writing Claude Code config "
                    f"after cleaning references!{_NC}"
                )
                success = False

    return success


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------


def run_migrate() -> None:
    """Main entry point for `mcp-eregistrations-bpa migrate`."""
    args = sys.argv[2:]  # skip "mcp-eregistrations-bpa" and "migrate"

    if "--help" in args or "-h" in args:
        print()
        print(f"  {_BOLD}Migration Tool{_NC}")
        print()
        print("  Converts per-server BPA entries into multi-instance profiles.")
        print("  Cleans up old config entries from Claude Desktop and Claude Code.")
        print()
        print(f"  {_BOLD}Usage:{_NC}")
        print("    mcp-eregistrations-bpa migrate          # dry-run (safe)")
        print("    mcp-eregistrations-bpa migrate --apply  # execute migration")
        print()
        print(f"  {_BOLD}Options:{_NC}")
        print("    --apply    Execute the migration (default is dry-run)")
        print("    --help     Show this help message")
        print()
        sys.exit(0)

    apply = "--apply" in args

    _print_banner()

    # Read config files
    desktop_path = _get_claude_desktop_config_path()
    code_path = _get_claude_code_config_path()
    desktop_data = _read_config(desktop_path)
    code_data = _read_config(code_path)

    if desktop_data is None and code_data is None:
        print(f"  {_RED}No config files found. Nothing to migrate.{_NC}")
        print()
        sys.exit(0)

    # Compute plan
    plan = _compute_plan(desktop_data, code_data)

    # Print plan
    _print_plan(plan, desktop_data, code_data)

    # Check if there's work to do
    if not plan.profiles_to_create and not any(plan.servers_to_remove.values()):
        sys.exit(0)

    if not apply:
        print(f"  Run with {_BOLD}--apply{_NC} to execute.")
        print()
        sys.exit(0)

    # Execute
    print(f"  {_BOLD}Executing migration...{_NC}")
    print()

    success = _execute_plan(plan, desktop_path, code_path, desktop_data, code_data)

    print()
    if success:
        print(f"  {_GREEN}Migration complete!{_NC}")
        print()
        print(f"  {_BLUE}Next steps:{_NC}")
        print("  1. Restart Claude Desktop (quit and reopen)")
        print("  2. Run: mcp-eregistrations-bpa migrate  (verify nothing left)")
        print("  3. Start the server and test: instance_list, auth_login")
    else:
        print(f"  {_YELLOW}Migration completed with warnings.{_NC}")
        print("  Check the output above for details.")

    print()
