"""Shared instance configuration for all MCP eRegistrations components.

Provides:
- Built-in instance definitions from instances.yaml
- Unified profile management (~/.config/mcp-eregistrations/profiles.json)
- Instance name resolution (profiles override built-in)
- Auth key resolution (profile name lookup for authentication)
"""

from __future__ import annotations

import hashlib
import json
import logging
import re
from pathlib import Path
from typing import Any
from urllib.parse import urlparse

import yaml

logger = logging.getLogger(__name__)


def _generate_instance_slug(url: str) -> str:
    """Generate a filesystem-safe slug from a BPA instance URL.

    Creates a human-readable identifier from the hostname, plus a short hash
    for uniqueness. Format: {sanitized_hostname}-{short_hash}

    Examples:
        https://bpa.dev.els.eregistrations.org -> els-dev-a1b2c3
        https://bpa.test.cuba.eregistrations.org -> cuba-test-d4e5f6

    Args:
        url: The BPA instance URL.

    Returns:
        A filesystem-safe slug like "els-dev-a1b2c3".
    """
    parsed = urlparse(url)
    hostname = parsed.netloc or parsed.path  # Handle edge cases

    # Extract meaningful parts from hostname
    # e.g., "bpa.dev.els.eregistrations.org" ->
    # ["bpa", "dev", "els", "eregistrations", "org"]
    parts = hostname.lower().split(".")

    # Remove common prefixes/suffixes for cleaner slug
    skip_parts = {"bpa", "eregistrations", "org", "com", "www"}
    meaningful_parts = [p for p in parts if p not in skip_parts and p]

    # Build readable slug (reversed to get country/env first: "els-dev")
    if meaningful_parts:
        slug_base = "-".join(reversed(meaningful_parts[:2]))  # Max 2 parts
    else:
        slug_base = "default"

    # Add short hash for uniqueness (handles edge cases)
    url_hash = hashlib.sha256(url.encode()).hexdigest()[:6]
    slug = f"{slug_base}-{url_hash}"

    # Sanitize: only allow alphanumeric and hyphens
    slug = re.sub(r"[^a-z0-9-]", "-", slug)
    slug = re.sub(r"-+", "-", slug).strip("-")

    return slug


class ConfigurationError(Exception):
    """Raised when instance configuration is invalid or missing."""

    pass


# Patchable base dir for tests (monkeypatch this to tmp_path)
CONFIG_BASE_DIR = Path.home() / ".config"


def load_builtin_instances() -> list[dict[str, Any]]:
    """Load built-in instance definitions from common instances.yaml.

    Returns:
        List of instance dicts with name, display_name, URLs, auth config.
    """
    yaml_path = Path(__file__).parent / "instances.yaml"
    with open(yaml_path) as f:
        data = yaml.safe_load(f)
    result: list[dict[str, Any]] = data.get("instances", [])
    return result


def get_builtin_instance(name: str) -> dict[str, Any] | None:
    """Find a built-in instance by name.

    Args:
        name: Instance name (e.g., "jamaica").

    Returns:
        Instance config dict, or None if not found.
    """
    for inst in load_builtin_instances():
        if inst["name"] == name:
            return inst
    return None


# Unified profile store path
PROFILES_PATH = CONFIG_BASE_DIR / "mcp-eregistrations" / "profiles.json"

# Migration state — set to True after migration runs (or in tests)
_profiles_migrated = False


def _legacy_profiles_path(component: str) -> Path:
    """Get legacy profiles.json path for a component.

    Args:
        component: Component name ("bpa", "ds", "gdb").

    Returns:
        Path like ~/.config/mcp-eregistrations-ds/profiles.json
    """
    return CONFIG_BASE_DIR / f"mcp-eregistrations-{component}" / "profiles.json"


def _migrate_profiles() -> None:
    """Migrate legacy per-component profiles into the unified store.

    Reads BPA legacy profiles (wrapped format: {"profiles": {"name": {...}}})
    and DS legacy profiles (flat format: {"name": {...}}), merges them,
    and writes the unified store. Old files are left in place.

    All errors are logged as warnings and never block execution.
    """
    unified_path = CONFIG_BASE_DIR / "mcp-eregistrations" / "profiles.json"
    if unified_path.exists():
        return

    merged: dict[str, dict[str, Any]] = {}

    # Read BPA legacy profiles
    bpa_path = _legacy_profiles_path("bpa")
    if bpa_path.exists():
        try:
            raw = json.loads(bpa_path.read_text())
            # BPA format: {"profiles": {"name": {...}}} — note the wrapper
            bpa_profiles: dict[str, dict[str, Any]] = {}
            if isinstance(raw, dict):
                if "profiles" in raw and isinstance(raw["profiles"], dict):
                    bpa_profiles = raw["profiles"]
                else:
                    # Flat format (no wrapper)
                    bpa_profiles = raw

            for name, profile in bpa_profiles.items():
                cleaned = dict(profile)
                # Rename bpa_instance_url -> bpa_url
                if "bpa_instance_url" in cleaned:
                    cleaned["bpa_url"] = cleaned.pop("bpa_instance_url")
                # Remove keyring sentinels
                if cleaned.get("cas_client_secret") == "__keyring__":
                    del cleaned["cas_client_secret"]
                merged[name] = cleaned
        except Exception:
            logger.warning("Failed to read BPA legacy profiles from %s", bpa_path)

    # Read DS legacy profiles
    ds_path = _legacy_profiles_path("ds")
    if ds_path.exists():
        try:
            ds_profiles: dict[str, dict[str, Any]] = json.loads(ds_path.read_text())
            if isinstance(ds_profiles, dict):
                for name, profile in ds_profiles.items():
                    if name in merged:
                        # Fill in DS fields absent from BPA profile
                        for key, value in profile.items():
                            if key not in merged[name]:
                                merged[name][key] = value
                    else:
                        # DS-only profile
                        merged[name] = dict(profile)
        except Exception:
            logger.warning("Failed to read DS legacy profiles from %s", ds_path)

    if merged:
        try:
            unified_path.parent.mkdir(parents=True, exist_ok=True)
            tmp = Path(str(unified_path) + ".tmp")
            tmp.write_text(json.dumps(merged, indent=2))
            tmp.replace(unified_path)
        except Exception:
            logger.warning("Failed to write unified profiles to %s", unified_path)


def _ensure_profiles_migrated() -> None:
    """Run migration once per process if not already done."""
    global _profiles_migrated  # noqa: PLW0603
    if not _profiles_migrated:
        _migrate_profiles()
        _profiles_migrated = True


def load_profiles() -> dict[str, dict[str, Any]]:
    """Load saved instance profiles from the unified store.

    Returns:
        Dict of profile_name -> config dict.
    """
    _ensure_profiles_migrated()
    path = CONFIG_BASE_DIR / "mcp-eregistrations" / "profiles.json"
    if not path.exists():
        return {}
    try:
        result: dict[str, dict[str, Any]] = json.loads(path.read_text())
        return result
    except (json.JSONDecodeError, OSError):
        return {}


def save_profiles(profiles: dict[str, dict[str, Any]]) -> None:
    """Save instance profiles to the unified store.

    Args:
        profiles: Dict of profile_name -> config dict.
    """
    _ensure_profiles_migrated()
    path = CONFIG_BASE_DIR / "mcp-eregistrations" / "profiles.json"
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = Path(str(path) + ".tmp")
    tmp.write_text(json.dumps(profiles, indent=2))
    tmp.replace(path)


def get_instance_config(
    instance: str | None,
) -> dict[str, Any]:
    """Resolve instance name to config dict.

    Lookup order:
    1. Custom profiles (unified store)
    2. Built-in instances.yaml

    Args:
        instance: Instance name (e.g., "jamaica"). None raises ToolError.

    Returns:
        Config dict with URLs, auth config, etc.

    Raises:
        ToolError: If instance is None or not found.
    """
    from fastmcp.exceptions import ToolError

    if instance is None:
        raise ToolError(
            "No instance specified. Pass instance='<name>'. "
            "Use instance_list to see available instances."
        )

    # Check custom profiles first
    profiles = load_profiles()
    if instance in profiles:
        return profiles[instance]

    # Fall back to built-in instances
    builtin = get_builtin_instance(instance)
    if builtin is not None:
        return builtin

    available = [i["name"] for i in load_builtin_instances()]
    raise ToolError(
        f"Unknown instance '{instance}'. "
        f"Available: {', '.join(available)}. "
        f"Or run instance_add to register a custom instance."
    )


def resolve_auth_key(instance: str | None) -> str:
    """Resolve the universal auth key (profile name) for an instance.

    1. Explicit name -> return as-is
    2. None + exactly one profile -> auto-select
    3. None + multiple -> raise with list

    Args:
        instance: Instance name, or None for auto-select.

    Returns:
        Profile name to use as auth key.

    Raises:
        ConfigurationError: If auto-select fails (multiple or no instances).
    """
    if instance:
        return instance

    # Auto-select from unified profiles
    profiles = load_profiles()
    names = list(profiles.keys())
    if len(names) == 1:
        return names[0]
    if names:
        raise ConfigurationError(
            f"Multiple instances configured: {', '.join(names)}. "
            "Specify instance= parameter."
        )

    # No profiles — check built-in instances
    instances = load_builtin_instances()
    if len(instances) == 1:
        name: str = instances[0]["name"]
        return name

    raise ConfigurationError("No instance configured. Use instance_add first.")
