"""MCP server for eRegistrations BPA platform."""

from __future__ import annotations

import logging
import os
import time

from fastmcp import FastMCP
from fastmcp.exceptions import ToolError

from mcp_eregistrations_bpa import __version__
from mcp_eregistrations_bpa.auth import TokenManager
from mcp_eregistrations_bpa.auth.credentials import (
    delete_credentials,
    get_credentials,
    store_credentials,
)
from mcp_eregistrations_bpa.auth.oidc import is_browser_available
from mcp_eregistrations_bpa.auth.permissions import browser_login, password_login
from mcp_eregistrations_bpa.config import (
    Config,
    get_current_instance_id,
    load_config,
    resolve_instance_config,
)
from mcp_eregistrations_bpa.tools import (
    register_action_tools,
    register_analysis_tools,
    register_audit_tools,
    register_behaviour_tools,
    register_bot_mapping_tools,
    register_bot_tools,
    register_classification_tools,
    register_cost_tools,
    register_debug_tools,
    register_determinant_tools,
    register_document_requirement_tools,
    register_export_tools,
    register_external_service_tools,
    register_field_tools,
    register_form_cache_tools,
    register_form_tools,
    register_message_tools,
    register_notification_tools,
    register_print_document_tools,
    register_registration_institution_tools,
    register_registration_tools,
    register_role_form_tools,
    register_role_status_tools,
    register_role_tools,
    register_role_unit_tools,
    register_rollback_tools,
    register_service_insights_tools,
    register_service_tools,
    register_workflow_tools,
)

logger = logging.getLogger(__name__)

# GitHub releases API for version checks
_GITHUB_REPO = "UNCTAD-eRegistrations/MCP_eRegistrations"
_GITHUB_API_URL = f"https://api.github.com/repos/{_GITHUB_REPO}/releases/latest"

# Version cache: (latest_version_str, timestamp)
_version_cache: tuple[str, float] | None = None
_VERSION_CACHE_TTL = 3600  # 1 hour

mcp = FastMCP(
    "eregistrations-bpa",
    instructions=(
        "BPA (Business Process Analyzer) tools for designing eRegistrations services. "
        "Covers services, forms, determinants, roles, bots, mappings, classifications, "
        "costs, print documents, and workflows. "
        "Audited write operations with rollback. "
        "Use auth_login to authenticate, service_list to discover services."
    ),
)

# Register BPA tools
register_service_tools(mcp)
register_registration_tools(mcp)
register_registration_institution_tools(mcp)
register_field_tools(mcp)
register_form_tools(mcp)
register_form_cache_tools(mcp)
register_determinant_tools(mcp)
register_action_tools(mcp)
register_behaviour_tools(mcp)
register_bot_tools(mcp)
register_bot_mapping_tools(mcp)
register_external_service_tools(mcp)
register_classification_tools(mcp)
register_notification_tools(mcp)
register_print_document_tools(mcp)
register_message_tools(mcp)
register_role_tools(mcp)
register_role_form_tools(mcp)
register_role_status_tools(mcp)
register_role_unit_tools(mcp)
register_document_requirement_tools(mcp)
register_cost_tools(mcp)
register_analysis_tools(mcp)
register_audit_tools(mcp)
register_rollback_tools(mcp)
register_export_tools(mcp)
register_service_insights_tools(mcp)
register_workflow_tools(mcp)
register_debug_tools(mcp)

from mcp_eregistrations_common.tools.instances import (  # noqa: E402
    register_instance_tools as register_common_instance_tools,
)

register_common_instance_tools(mcp)

# Token manager registry: one TokenManager per instance_id
_token_managers: dict[str, TokenManager] = {}


# --- CAS auth (TODO: remove when Cuba migrates to Keycloak) ---


async def _auth_login_cas(
    *,
    config: Config,
    inst_name: str,
    username: str | None = None,
    password: str | None = None,
    persist_credentials: bool = True,
) -> dict[str, object]:
    """CAS authentication — browser login only (no password grant).

    CAS backend only supports authorization_code + refresh_token grants.
    This function handles the full CAS login flow:
    1. Try browser login (opens CAS login page)
    2. On success, persist tokens for cross-server sharing (GDB/DS)
    3. Optionally store credentials for display purposes

    TODO: Remove this function when Cuba migrates to Keycloak.
    """
    from mcp_eregistrations_bpa.auth.credentials import store_refresh_context
    from mcp_eregistrations_bpa.auth.oidc import is_browser_available

    if not is_browser_available():
        return {
            "action_required": "browser_required",
            "message": (
                "CAS instances require browser login. "
                "Run auth_login in an environment with a browser."
            ),
            "instance": inst_name,
        }

    from mcp_eregistrations_bpa.auth.permissions import browser_login
    from mcp_eregistrations_bpa.exceptions import AuthenticationError

    try:
        result = await browser_login(config=config)
    except AuthenticationError as e:
        raise ToolError(f"CAS browser login failed: {e}") from e

    if result.get("success"):
        tm = get_token_manager(config.instance_id, instance_name=inst_name)
        _persist_to_auth_store(inst_name, tm, config, username, password)
        ctx = tm.refresh_context
        if ctx:
            store_refresh_context(config.instance_id, ctx)
        if persist_credentials and username and password:
            store_credentials(config.instance_id, username, password)

    return result


# --- End CAS auth ---


def _persist_to_auth_store(
    instance_name: str,
    token_manager: TokenManager,
    config: object,
    username: str | None = None,
    password: str | None = None,
) -> None:
    """Write credentials + refresh token to the unified auth store.

    This is what GDB/DS processes read via get_or_refresh_token().
    All data stored under the profile name (e.g., "jamaica") so every
    MCP server resolves the same key.
    """
    try:
        from mcp_eregistrations_common.auth_store import (
            store_credentials as auth_store_creds,
        )
        from mcp_eregistrations_common.auth_store import (
            store_refresh_context as auth_store_refresh,
        )

        if username and password:
            auth_store_creds(
                instance_name,
                username,
                password,
                keycloak_url=config.keycloak_url,  # type: ignore[attr-defined]
                keycloak_realm=config.keycloak_realm,  # type: ignore[attr-defined]
                client_id=config.keycloak_client_id,  # type: ignore[attr-defined]
            )
        ctx = token_manager.refresh_context
        if ctx:
            auth_store_refresh(
                instance_name,
                refresh_token=ctx["refresh_token"],
                token_endpoint=ctx["token_endpoint"],
                client_id=ctx["client_id"],
                cas_client_secret=ctx.get("cas_client_secret"),
                redirect_uri=ctx.get("redirect_uri"),
            )
    except Exception:
        pass  # Non-fatal — BPA auth still works without sharing


def get_token_manager(
    instance_id: str | None = None, instance_name: str | None = None
) -> TokenManager:
    """Get or create a TokenManager for the given instance.

    Args:
        instance_id: Instance slug. None falls back to current env-var instance.
        instance_name: Instance profile name (for sharing tokens with GDB/DS).

    Returns:
        TokenManager for the given instance.
    """
    key = instance_id or get_current_instance_id() or "default"
    if key not in _token_managers:
        _token_managers[key] = TokenManager(instance_name=instance_name)
    elif instance_name and not _token_managers[key]._instance_name:
        # Backfill instance_name (perform_password_login creates TM before
        # auth_login provides the name) and re-share tokens with GDB/DS.
        tm = _token_managers[key]
        tm._instance_name = instance_name
        tm._share_with_other_servers()
    return _token_managers[key]


@mcp.tool(
    annotations={"readOnlyHint": False, "destructiveHint": False, "openWorldHint": True}
)
async def auth_login(
    username: str | None = None,
    password: str | None = None,
    persist_credentials: bool = True,
    instance: str | None = None,
) -> dict[str, object]:
    """Authenticate with BPA. Supports browser and password grant.

    When called without credentials in a non-interactive environment,
    returns an ask_credentials response for the AI agent to handle.

    Args:
        username: BPA username (optional; enables password grant).
        password: BPA password (optional; required if username given).
        persist_credentials: Save credentials to system keyring (default: True).
        instance: Instance profile name (from instance_list).

    Returns:
        dict with success/ask_credentials status, user info, session duration.
    """
    from mcp_eregistrations_bpa.auth.credentials import store_refresh_context
    from mcp_eregistrations_bpa.exceptions import AuthenticationError

    # 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.")

    config = resolve_instance_config(instance)

    # Resolve instance name for cross-server token sharing (GDB, DS).
    # When instance is None and auto-selected from a single profile,
    # we must use the profile name (e.g., "jamaica"), not the slug
    # (e.g., "jamaica-a1b2c3"), so GDB/DS find the same auth.json key.
    if instance is None:
        from mcp_eregistrations_bpa.config import load_profiles

        profiles = load_profiles()
        if len(profiles) == 1:
            instance = next(iter(profiles))
    _inst_name = instance or config.instance_id

    # --- CAS path (browser-only — CAS has no password grant) ---
    # TODO: Remove this block when Cuba migrates to Keycloak.
    from mcp_eregistrations_bpa.config import AuthProvider

    if config.auth_provider == AuthProvider.CAS:
        return await _auth_login_cas(
            config=config,
            inst_name=_inst_name,
            username=username,
            password=password,
            persist_credentials=persist_credentials,
        )

    # --- Keycloak path (password grant + browser fallback) ---

    # Branch A: Credentials provided
    if username and password:
        result = await password_login(username, password, config=config)
        if result.get("success"):
            if persist_credentials:
                stored = store_credentials(config.instance_id, username, password)
                if not stored:
                    result["credential_storage_warning"] = (
                        "Keyring unavailable; credentials not saved."
                    )
            tm = get_token_manager(config.instance_id, instance_name=_inst_name)
            _persist_to_auth_store(_inst_name, tm, config, username, password)
            ctx = tm.refresh_context
            if ctx:
                store_refresh_context(config.instance_id, ctx)
        return result

    # Branch B: No credentials, try automatic methods

    # Try keyring
    creds = get_credentials(config.instance_id)
    if creds:
        try:
            result = await password_login(creds[0], creds[1], config=config)
            if result.get("success"):
                tm = get_token_manager(config.instance_id, instance_name=_inst_name)
                _persist_to_auth_store(_inst_name, tm, config, creds[0], creds[1])
                ctx = tm.refresh_context
                if ctx:
                    store_refresh_context(config.instance_id, ctx)
                return result
        except AuthenticationError:
            pass
        delete_credentials(config.instance_id)

    # Try browser
    if is_browser_available():
        try:
            result = await browser_login(config=config)
            if result.get("success"):
                tm = get_token_manager(config.instance_id, instance_name=_inst_name)
                _persist_to_auth_store(_inst_name, tm, config)
                ctx = tm.refresh_context
                if ctx:
                    store_refresh_context(config.instance_id, ctx)
                return result
        except AuthenticationError:
            pass

    # All methods exhausted
    return {
        "action_required": "ask_credentials",
        "message": (
            "Cannot open browser for authentication. "
            "Please provide your BPA credentials."
        ),
        "fields": [
            {
                "name": "username",
                "type": "text",
                "label": "BPA Username (email)",
                "required": True,
            },
            {
                "name": "password",
                "type": "password",
                "label": "BPA Password",
                "required": True,
            },
            {
                "name": "persist_credentials",
                "type": "boolean",
                "label": "Remember credentials",
                "required": False,
            },
        ],
        "retry_tool": "auth_login",
        "instance_url": config.bpa_instance_url,
    }


def _get_github_token() -> str | None:
    """Get a GitHub token from env vars or gh CLI."""
    token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
    if token:
        return token
    try:
        import subprocess

        result = subprocess.run(  # noqa: S603, S607
            ["gh", "auth", "token"],
            capture_output=True,
            text=True,
            timeout=3,
        )
        if result.returncode == 0 and result.stdout.strip():
            return result.stdout.strip()
    except Exception:
        pass
    return None


async def _check_latest_version() -> str | None:
    """Check GitHub for the latest release version (cached 1h).

    Returns:
        Latest version string (e.g. "0.14.1"), or None if check fails.
    """
    global _version_cache  # noqa: PLW0603

    # Return cached value if fresh
    if _version_cache is not None:
        cached_version, cached_at = _version_cache
        if time.monotonic() - cached_at < _VERSION_CACHE_TTL:
            return cached_version

    try:
        import httpx

        headers = {"Accept": "application/vnd.github+json"}
        # Private repo: use GitHub token if available
        gh_token = _get_github_token()
        if gh_token:
            headers["Authorization"] = f"Bearer {gh_token}"

        async with httpx.AsyncClient(timeout=5.0) as client:
            resp = await client.get(_GITHUB_API_URL, headers=headers)
            resp.raise_for_status()
            tag: str = resp.json().get("tag_name", "")
            latest = tag.lstrip("v")
            _version_cache = (latest, time.monotonic())
            return latest
    except Exception:
        logger.debug("Version check failed", exc_info=True)
        return None


def _version_info(latest: str | None) -> dict[str, object]:
    """Build version info dict comparing current vs latest.

    Args:
        latest: Latest version string from GitHub, or None.

    Returns:
        dict with version, latest_version, update_available, update_message.
    """
    info: dict[str, object] = {"version": __version__}

    if latest is None:
        return info

    info["latest_version"] = latest

    try:
        current = tuple(int(x) for x in __version__.split("."))
        remote = tuple(int(x) for x in latest.split("."))
        if remote > current:
            info["update_available"] = True
            info["update_message"] = (
                f"Update available: v{latest}. "
                f"See https://github.com/{_GITHUB_REPO}/releases/tag/v{latest}"
            )
        else:
            info["update_available"] = False
    except (ValueError, TypeError):
        pass

    return info


async def get_connection_status(instance: str | None = None) -> dict[str, object]:
    """Get current BPA connection status (internal implementation).

    Returns connection state, authenticated user, permissions, and session info.
    This is a read-only operation that does not trigger token refresh.

    Args:
        instance: Instance profile name (from instance_list).

    Returns:
        dict: Connection status with instance URL, instance_id, user, permissions,
        and expiry.
    """
    config = resolve_instance_config(instance)
    token_manager = get_token_manager(config.instance_id)
    latest = await _check_latest_version()
    version = _version_info(latest)

    # Check if not authenticated or expired — include auto-login hints
    if not token_manager.is_authenticated() or token_manager.is_token_expired():
        from mcp_eregistrations_bpa.auth.credentials import get_credentials

        has_stored_creds = get_credentials(config.instance_id) is not None
        auto_login_available = has_stored_creds

        if auto_login_available:
            message = (
                "Not authenticated yet. Stored credentials found — "
                "auto-login will activate on first tool call."
            )
        elif not token_manager.is_authenticated():
            message = "Not authenticated. Run auth_login to connect."
        else:
            message = "Session expired. Run auth_login to reconnect."

        result: dict[str, object] = {
            "connected": False,
            "instance_id": config.instance_id,
            "instance_url": config.bpa_instance_url,
            "auto_login_available": auto_login_available,
            "message": message,
            **version,
        }
        if config.ds_url:
            result["ds_url"] = config.ds_url
        return result

    # Return full authenticated status
    result = {
        "connected": True,
        "instance_id": config.instance_id,
        "instance_url": config.bpa_instance_url,
        "user": token_manager.user_email,
        "permissions": token_manager.permissions,
        "session_expires_in": f"{token_manager.expires_in_minutes} minutes",
        **version,
    }
    if config.ds_url:
        result["ds_url"] = config.ds_url
    return result


@mcp.tool(
    annotations={"readOnlyHint": True, "destructiveHint": False, "openWorldHint": True}
)
async def connection_status(instance: str | None = None) -> dict[str, object]:
    """View current BPA connection status.

    Returns connection state, authenticated user, permissions, and session info.
    This is a read-only operation that does not trigger token refresh.

    Args:
        instance: Instance profile name (from instance_list).

    Returns:
        dict: Connection status with instance URL, ds_url (applicant and agency portal),
        user, permissions, and expiry.
    """
    return await get_connection_status(instance)


@mcp.tool(
    annotations={"readOnlyHint": True, "destructiveHint": False, "openWorldHint": False}
)
async def server_info() -> dict[str, object]:
    """Get MCP server version and configured BPA instances. No authentication required.

    Returns:
        dict with mcp_version, and either env_instance (from BPA_INSTANCE_URL)
        or profiles (from instance profiles), or both.
    """
    from mcp_eregistrations_bpa.exceptions import ConfigurationError
    from mcp_eregistrations_common.config import load_profiles

    info: dict[str, object] = {"mcp_version": __version__}

    # Try env-var config (legacy single-instance mode)
    try:
        config = load_config()
        env_instance: dict[str, object] = {
            "bpa_instance_url": config.bpa_instance_url,
            "instance_id": config.instance_id,
            "auth_provider": config.auth_provider.value,
        }
        if config.ds_url:
            env_instance["ds_url"] = config.ds_url
        if config.keycloak_realm:
            env_instance["keycloak_realm"] = config.keycloak_realm
        info["env_instance"] = env_instance
    except ConfigurationError:
        pass

    # Always include profiles
    profiles = load_profiles()
    if profiles:
        info["profiles"] = {
            name: profile.get("bpa_url", profile.get("bpa_instance_url", ""))
            for name, profile in profiles.items()
        }
        info["profile_count"] = len(profiles)

    return info
