"""Shared instance management tools for all eRegistrations MCP servers.

Provides instance_add, instance_list, instance_remove — registered once
and shared across BPA, DS, and GDB servers.
"""

from __future__ import annotations

import asyncio
import logging
from typing import Any
from urllib.parse import urlparse

import httpx
from fastmcp.exceptions import ToolError
from mcp.types import ToolAnnotations

from mcp_eregistrations_common.config import (
    load_builtin_instances,
    load_profiles,
    save_profiles,
)

logger = logging.getLogger(__name__)

__all__ = [
    "_derive_urls",
    "_verify_url",
    "instance_add",
    "instance_list",
    "instance_remove",
    "register_instance_tools",
    "validate_url",
]

# ToolAnnotation constants
READ = ToolAnnotations(readOnlyHint=True, destructiveHint=False, openWorldHint=True)
WRITE = ToolAnnotations(readOnlyHint=False, destructiveHint=False, openWorldHint=True)
DESTRUCTIVE = ToolAnnotations(
    readOnlyHint=False, destructiveHint=True, openWorldHint=True
)


# ---------------------------------------------------------------------------
# URL validation
# ---------------------------------------------------------------------------


def validate_url(url: str) -> str:
    """Validate and normalize an instance URL.

    Requires HTTPS, valid host, no query/fragment. Returns URL with
    trailing slash stripped.

    Args:
        url: URL to validate.

    Returns:
        Normalized URL string.

    Raises:
        ToolError: On invalid URL.
    """
    if not url or not url.strip():
        raise ToolError("URL is required and cannot be empty.")

    parsed = urlparse(url.strip())

    if not parsed.scheme:
        raise ToolError(
            f"Invalid URL '{url}': no scheme. Use HTTPS (e.g. https://example.com)."
        )

    if parsed.scheme != "https":
        raise ToolError(f"Invalid URL '{url}': must use HTTPS, not '{parsed.scheme}'.")

    if not parsed.hostname:
        raise ToolError(f"Invalid URL '{url}': no valid hostname.")

    if parsed.query:
        raise ToolError(
            f"Invalid URL '{url}': query strings not allowed. "
            "Provide the base URL only."
        )

    if parsed.fragment:
        raise ToolError(
            f"Invalid URL '{url}': fragment not allowed. Provide the base URL only."
        )

    return url.strip().rstrip("/")


# ---------------------------------------------------------------------------
# URL derivation from BPA URL
# ---------------------------------------------------------------------------


def _derive_urls(bpa_url: str) -> dict[str, str]:
    """Derive DS, GDB, and Keycloak URLs from a BPA URL.

    Only works when the BPA hostname starts with 'bpa.'.

    Args:
        bpa_url: Validated BPA URL.

    Returns:
        dict with ds_url, gdb_url, keycloak_url (or empty if not derivable).
    """
    parsed = urlparse(bpa_url)
    hostname = parsed.hostname or ""

    if not hostname.startswith("bpa."):
        return {}

    base_host = hostname[4:]  # strip "bpa."
    port_suffix = f":{parsed.port}" if parsed.port and parsed.port != 443 else ""

    def _build(prefix: str) -> str:
        host = f"{prefix}{base_host}" if prefix else base_host
        return f"https://{host}{port_suffix}"

    return {
        "ds_url": _build(""),
        "gdb_url": _build("gdb."),
        "keycloak_url": _build("login."),
    }


# ---------------------------------------------------------------------------
# URL verification
# ---------------------------------------------------------------------------


async def _verify_url(url: str, verify_ssl: bool = True) -> tuple[bool, str]:
    """Verify a URL is reachable via HTTP HEAD request.

    Accepts any status < 500 as "reachable" (including 401/403).

    Args:
        url: URL to verify.
        verify_ssl: Whether to verify SSL certificates.

    Returns:
        Tuple of (reachable, error_message). error_message is empty on success.
    """
    try:
        async with httpx.AsyncClient(timeout=5.0, verify=verify_ssl) as client:
            resp = await client.head(url)
            if resp.status_code < 500:
                return True, ""
            return False, f"HTTP {resp.status_code}"
    except (httpx.ConnectError, httpx.TimeoutException) as e:
        return False, str(e)
    except httpx.HTTPError as e:
        return False, str(e)


# ---------------------------------------------------------------------------
# instance_list
# ---------------------------------------------------------------------------


async def instance_list() -> dict[str, Any]:
    """List all available instances (built-in + custom profiles).

    Returns:
        dict with total, profiles (name -> urls, auto_login, source).
    """
    # Lazy import to avoid circular dependency
    from mcp_eregistrations_common import auth_store

    builtin = load_builtin_instances()
    custom = load_profiles()

    profiles: dict[str, dict[str, Any]] = {}
    seen: set[str] = set()

    # Custom profiles override built-in by name
    for name, profile in custom.items():
        profiles[name] = {
            "bpa_url": profile.get("bpa_url"),
            "ds_url": profile.get("ds_url"),
            "gdb_url": profile.get("gdb_url"),
            "auto_login": auth_store.get_credentials(name) is not None,
            "source": "custom",
        }
        seen.add(name)

    # Built-in instances (not overridden)
    for inst in builtin:
        name = inst["name"]
        if name not in seen:
            profiles[name] = {
                "bpa_url": inst.get("bpa_url"),
                "ds_url": inst.get("ds_url"),
                "gdb_url": inst.get("gdb_url"),
                "auto_login": auth_store.get_credentials(name) is not None,
                "source": "builtin",
            }

    return {"total": len(profiles), "profiles": profiles}


# ---------------------------------------------------------------------------
# instance_add
# ---------------------------------------------------------------------------


async def instance_add(
    name: str,
    bpa_url: str | None = None,
    ds_url: str | None = None,
    gdb_url: str | None = None,
    keycloak_url: str | None = None,
    keycloak_realm: str | None = None,
    keycloak_client_id: str = "mcp-eregistrations-bpa",
    cas_url: str | None = None,
    cas_client_id: str | None = None,
    cas_client_secret: str | None = None,
    verify_ssl: bool = True,
    username: str | None = None,
    password: str | None = None,
) -> dict[str, Any]:
    """Register or update an eRegistrations instance profile.

    Auto-fills missing URLs from built-in instances or derives them from
    bpa_url. Updates merge with existing profiles.

    Args:
        name: Profile name (e.g. "jamaica", "staging").
        bpa_url: BPA instance URL (HTTPS).
        ds_url: Display System URL (HTTPS).
        gdb_url: GDB instance URL (HTTPS).
        keycloak_url: Keycloak server URL.
        keycloak_realm: Keycloak realm name.
        keycloak_client_id: Keycloak client ID (default: mcp-eregistrations-bpa).
        cas_url: CAS server URL for legacy auth.
        cas_client_id: CAS client ID (required if cas_url set).
        cas_client_secret: CAS client secret (required if cas_url set).
        verify_ssl: Verify SSL certs (default: True). False for self-signed.
        username: Username for auto-login.
        password: Password for auto-login (required if username set).

    Returns:
        dict with name, urls, created, replaced, sources, url_warnings.
    """
    # --- Validate name ---
    if not name or not name.strip():
        raise ToolError("Profile name is required.")
    name = name.strip().lower()

    # --- Collect explicit (non-None) URL params ---
    explicit: dict[str, str] = {}
    if bpa_url:
        explicit["bpa_url"] = validate_url(bpa_url)
    if ds_url:
        explicit["ds_url"] = validate_url(ds_url)
    if gdb_url:
        explicit["gdb_url"] = validate_url(gdb_url)
    if keycloak_url:
        explicit["keycloak_url"] = validate_url(keycloak_url)
    if keycloak_realm:
        explicit["keycloak_realm"] = keycloak_realm
    if cas_url:
        cas_url = validate_url(cas_url)

    # --- CAS all-or-nothing ---
    cas_fields = [cas_url, cas_client_id, cas_client_secret]
    cas_provided = [f for f in cas_fields if f]
    if cas_provided and len(cas_provided) != 3:
        missing = []
        if not cas_url:
            missing.append("cas_url")
        if not cas_client_id:
            missing.append("cas_client_id")
        if not cas_client_secret:
            missing.append("cas_client_secret")
        raise ToolError(
            f"CAS auth requires all three: cas_url, cas_client_id, cas_client_secret. "
            f"Missing: {', '.join(missing)}."
        )

    # --- Credential pair validation ---
    if username and not password:
        raise ToolError("Password is required when username is provided.")
    if password and not username:
        raise ToolError("Username is required when password is provided.")

    # --- Priority chain: explicit > derived > builtin > existing ---

    # Layer 4: existing profile (lowest priority)
    profiles = load_profiles()
    existing = profiles.get(name, {})

    # Layer 3: builtin instance lookup
    from mcp_eregistrations_common.config import get_builtin_instance

    builtin = get_builtin_instance(name) or {}

    # Layer 2: derive from bpa_url (use explicit, or builtin, or existing)
    effective_bpa = (
        explicit.get("bpa_url") or builtin.get("bpa_url") or existing.get("bpa_url")
    )
    derived: dict[str, str] = {}
    if effective_bpa:
        derived = _derive_urls(effective_bpa)

    # Merge layers: explicit > derived > builtin > existing
    url_fields = ["bpa_url", "ds_url", "gdb_url", "keycloak_url"]
    resolved: dict[str, Any] = {}
    sources: dict[str, str] = {}
    for field in url_fields:
        if field in explicit:
            resolved[field] = explicit[field]
            sources[field] = "explicit"
        elif field in derived:
            resolved[field] = derived[field]
            sources[field] = "derived"
        elif builtin.get(field):
            resolved[field] = builtin[field]
            sources[field] = "builtin"
        elif existing.get(field):
            resolved[field] = existing[field]
            sources[field] = "existing"

    # keycloak_realm (not a URL, handle separately)
    resolved_realm = (
        explicit.get("keycloak_realm")
        or builtin.get("keycloak_realm")
        or existing.get("keycloak_realm")
    )
    if resolved_realm:
        if "keycloak_realm" in explicit:
            sources["keycloak_realm"] = "explicit"
        elif builtin.get("keycloak_realm"):
            sources["keycloak_realm"] = "builtin"
        else:
            sources["keycloak_realm"] = "existing"

    # --- Validate at least one URL after full resolution ---
    if not any(resolved.get(f) for f in url_fields):
        raise ToolError(
            "At least one URL is required (bpa_url, ds_url, or gdb_url). "
            f"No built-in instance found for '{name}'."
        )

    # --- Verify derived URLs concurrently ---
    url_warnings: list[str] = []
    derived_keys = [k for k, v in sources.items() if v == "derived" and k in url_fields]
    if derived_keys:
        verify_tasks = [
            _verify_url(resolved[key], verify_ssl=verify_ssl) for key in derived_keys
        ]
        verify_results = await asyncio.gather(*verify_tasks)
        for key, (reachable, error) in zip(derived_keys, verify_results):
            if not reachable:
                url_warnings.append(f"{key} {resolved[key]}: {error}")

    # Warn if keycloak_url resolved but realm missing
    if resolved.get("keycloak_url") and not resolved_realm:
        url_warnings.append(
            "keycloak_realm is not set. Auth will fail without it. "
            "Provide keycloak_realm explicitly."
        )

    # --- Build profile dict ---
    profile: dict[str, Any] = dict(existing)  # start with existing for merge
    for field in url_fields:
        if field in resolved:
            profile[field] = resolved[field]
    if resolved_realm:
        profile["keycloak_realm"] = resolved_realm
    if keycloak_client_id != "mcp-eregistrations-bpa":
        profile["keycloak_client_id"] = keycloak_client_id
    elif "keycloak_client_id" not in profile and builtin.get("keycloak_client_id"):
        profile["keycloak_client_id"] = builtin["keycloak_client_id"]
    if cas_url:
        profile["cas_url"] = cas_url
    if cas_client_id:
        profile["cas_client_id"] = cas_client_id
    if not verify_ssl:
        profile["verify_ssl"] = False
    # CAS secret goes to auth_store, NOT in profile

    # --- Store CAS secret in auth_store (not in profile) ---
    if cas_client_secret:
        from mcp_eregistrations_common import auth_store

        auth_store.store_cas_secret(name, cas_client_secret)

    # --- Store credentials in auth_store ---
    auto_login: str | None = None
    if username and password:
        from mcp_eregistrations_common import auth_store

        effective_kc_url = resolved.get("keycloak_url")
        effective_kc_realm = resolved_realm
        try:
            auth_store.store_credentials(
                name,
                username,
                password,
                keycloak_url=effective_kc_url,
                keycloak_realm=effective_kc_realm,
            )
            auto_login = "enabled"

            if effective_kc_url and effective_kc_realm:
                try:
                    from mcp_eregistrations_common.auth import (
                        keycloak_password_grant,
                    )

                    await keycloak_password_grant(
                        keycloak_url=effective_kc_url,
                        realm=effective_kc_realm,
                        client_id=keycloak_client_id,
                        username=username,
                        password=password,
                        verify_ssl=verify_ssl,
                    )
                    auto_login = "enabled_and_verified"
                except Exception:
                    auto_login = "enabled_not_verified"
        except Exception:
            auto_login = "store_failed"

    # --- Save profile ---
    replaced = name in profiles
    profiles[name] = profile
    save_profiles(profiles)

    # --- Build result ---
    result: dict[str, Any] = {
        "name": name,
        "bpa_url": resolved.get("bpa_url"),
        "ds_url": resolved.get("ds_url"),
        "gdb_url": resolved.get("gdb_url"),
        "keycloak_url": resolved.get("keycloak_url"),
        "keycloak_realm": resolved_realm,
        "created": not replaced,
        "replaced": replaced,
        "sources": sources,
    }
    if url_warnings:
        result["url_warnings"] = url_warnings
    if auto_login:
        result["auto_login"] = auto_login
    return result


# ---------------------------------------------------------------------------
# instance_remove
# ---------------------------------------------------------------------------


async def instance_remove(name: str) -> dict[str, Any]:
    """Remove a custom instance profile. Destructive operation.

    Args:
        name: Profile name to remove.

    Returns:
        dict with removed (bool), name.
    """
    if not name or not name.strip():
        raise ToolError("Profile name is required.")
    name = name.strip().lower()

    profiles = load_profiles()
    if name not in profiles:
        available = ", ".join(sorted(profiles.keys())) or "none"
        raise ToolError(
            f"Profile '{name}' not found. Available custom profiles: {available}."
        )

    # Clean up auth data
    try:
        from mcp_eregistrations_common import auth_store

        auth_store.delete_instance(name)
    except Exception:
        pass

    del profiles[name]
    save_profiles(profiles)

    return {"removed": True, "name": name}


# ---------------------------------------------------------------------------
# Registration
# ---------------------------------------------------------------------------


def register_instance_tools(mcp: Any) -> None:
    """Register instance management tools with an MCP server.

    Args:
        mcp: The FastMCP server instance.
    """
    mcp.tool(annotations=READ)(instance_list)
    mcp.tool(annotations=WRITE)(instance_add)
    mcp.tool(annotations=DESTRUCTIVE)(instance_remove)
