"""Permission enforcement for MCP tools.

This module provides guards and utility functions to enforce
BPA permissions before tool execution. All permission checks
are server-side and based on JWT token claims.

Usage in MCP tools:
    @mcp.tool()
    async def service_create(name: str) -> dict[str, object]:
        # Ensure write permission before proceeding
        access_token = await ensure_write_permission()
        # ... use access_token for BPA API call
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from fastmcp.exceptions import ToolError

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
    from mcp_eregistrations_bpa.auth.token_manager import TokenManager
    from mcp_eregistrations_bpa.config import Config

__all__ = [
    "PERMISSION_VIEWER",
    "PERMISSION_SERVICE_DESIGNER",
    "WRITE_PERMISSIONS",
    "browser_login",
    "check_permission",
    "ensure_authenticated",
    "ensure_write_permission",
    "password_login",
]

# Permission constants
PERMISSION_VIEWER = "viewer"
PERMISSION_SERVICE_DESIGNER = "service_designer"

# Roles that allow write operations
WRITE_PERMISSIONS: list[str] = [PERMISSION_SERVICE_DESIGNER]


def get_token_manager(instance_id: str | None = None) -> TokenManager:
    """Get the token manager for the given instance.

    Uses late import to avoid circular dependency with server.py.

    Args:
        instance_id: Instance slug. None falls back to current env-var instance.

    Returns:
        TokenManager for the given instance.
    """
    from mcp_eregistrations_bpa.server import get_token_manager as _get_tm

    return _get_tm(instance_id)


async def password_login(
    username: str,
    password: str,
    instance_id: str | None = None,
    config: Config | None = None,
) -> dict[str, object]:
    """Dispatch password grant to correct provider (Keycloak or CAS).

    Args:
        username: BPA username.
        password: BPA password.
        instance_id: Instance slug (ignored if config provided).
        config: Pre-resolved Config object. Falls back to resolve via instance_id.

    Returns:
        Auth result dict (same contract as perform_password_login).
    """
    from mcp_eregistrations_bpa.config import AuthProvider

    if config is None:
        from mcp_eregistrations_bpa.config import load_config

        config = load_config()
    if config.auth_provider == AuthProvider.CAS:
        from mcp_eregistrations_bpa.auth.cas import perform_cas_password_login

        return await perform_cas_password_login(username, password, config=config)
    else:
        from mcp_eregistrations_bpa.auth.oidc import perform_password_login

        return await perform_password_login(username, password, config=config)


async def browser_login(
    instance_id: str | None = None,
    config: Config | None = None,
) -> dict[str, object]:
    """Dispatch browser login to correct provider (Keycloak or CAS).

    Args:
        instance_id: Instance slug (ignored if config provided).
        config: Pre-resolved Config object. Falls back to load_config().

    Returns:
        Auth result dict.
    """
    from mcp_eregistrations_bpa.config import AuthProvider

    if config is None:
        from mcp_eregistrations_bpa.config import load_config

        config = load_config()
    if config.auth_provider == AuthProvider.CAS:
        from mcp_eregistrations_bpa.auth.cas import perform_cas_browser_login

        return await perform_cas_browser_login(config=config)
    else:
        from mcp_eregistrations_bpa.auth.oidc import perform_browser_login

        return await perform_browser_login(config=config)


async def ensure_authenticated(
    instance_id: str | None = None, config: Config | None = None
) -> str:
    """Ensure user is authenticated and return access token.

    Priority chain:
    1. Valid cached token -> return immediately
    2. In-memory refresh token -> attempt refresh
    3. Stored refresh token (AuthStore) -> attempt refresh
    4. Stored credentials (AuthStore) -> password grant
    5. Browser available -> browser login
    6. Raise ToolError with instructions

    Args:
        instance_id: Instance slug for token isolation. None = env-var default.
        config: Pre-resolved Config. Avoids slug-vs-profile lookup issues.

    Returns:
        Valid access token.

    Raises:
        ToolError: If all authentication methods fail.
    """
    from mcp_eregistrations_bpa.auth.credentials import (
        delete_credentials,
        delete_refresh_context,
        get_credentials,
        get_refresh_context,
        store_refresh_context,
    )
    from mcp_eregistrations_bpa.exceptions import AuthenticationError

    token_manager = get_token_manager(instance_id)
    logger.debug("Checking authentication status...")

    # Step 1: Valid cached token
    if token_manager.is_authenticated() and not token_manager.is_token_expired():
        logger.debug("User authenticated: %s", token_manager.user_email)
        return await token_manager.get_access_token()

    # Step 2: In-memory refresh token
    if token_manager.is_authenticated():
        logger.info("Token expired or near-expiry, attempting refresh")
        try:
            token = await token_manager.get_access_token()  # triggers _refresh()
            # Persist rotated refresh token
            _persist_refresh_context(token_manager, config, instance_id)
            return token
        except AuthenticationError:
            logger.info("Token refresh failed, falling through to re-auth")

    if config is None:
        from mcp_eregistrations_bpa.config import load_config

        config = load_config()

    # Step 3: Stored refresh token (keyring)
    stored_ctx = get_refresh_context(config.instance_id)
    if stored_ctx:
        logger.info("Found stored refresh context, attempting token refresh")
        success = await token_manager.try_stored_refresh(stored_ctx)
        if success:
            # Persist updated context (token may have rotated)
            ctx = token_manager.refresh_context
            if ctx:
                store_refresh_context(config.instance_id, ctx)
            return await token_manager.get_access_token()
        # Stored refresh token is stale
        logger.info("Stored refresh token failed, removing stale entry")
        delete_refresh_context(config.instance_id)

    # Step 4: Stored credentials (AuthStore)
    try:
        creds = get_credentials(config.instance_id)
    except Exception:
        creds = None
        logger.debug("Credential read failed, continuing")
    if creds:
        logger.info("Found stored credentials, attempting password grant")
        try:
            result = await password_login(creds[0], creds[1], config=config)
            if result.get("success"):
                _persist_refresh_context(token_manager, config, instance_id)
                return await token_manager.get_access_token()
        except AuthenticationError:
            pass
        # Stored creds are stale - delete them
        logger.info("Stored credentials failed, removing stale entry")
        delete_credentials(config.instance_id)

    # Step 5: Browser available
    from mcp_eregistrations_bpa.auth.oidc import is_browser_available

    if is_browser_available():
        logger.info("Browser available, attempting browser login")
        try:
            result = await browser_login(config=config)
            if result.get("success"):
                _persist_refresh_context(token_manager, config, instance_id)
                return await token_manager.get_access_token()
            if result.get("error"):
                logger.error("Browser login failed: %s", result.get("message"))
        except AuthenticationError as e:
            logger.error("Browser login error: %s", e)

    # Step 6: All methods exhausted
    inst = config.instance_id if config else (instance_id or "unknown")
    raise ToolError(
        f"Not authenticated for '{inst}'. "
        f'Call auth_login(instance="{inst}") with credentials.'
    )


def _persist_refresh_context(
    token_manager: TokenManager,
    config: Config | None,
    instance_id: str | None,
) -> None:
    """Persist refresh context to auth store.

    Includes CAS-specific fields (client_secret, redirect_uri) when the
    instance uses CAS auth, enabling cross-server token refresh for GDB/DS.
    """
    from mcp_eregistrations_bpa.auth.credentials import store_refresh_context

    ctx = token_manager.refresh_context
    if not ctx:
        return
    # Add CAS redirect_uri for cross-server refresh
    if config and config.cas_url:
        ctx["redirect_uri"] = f"http://127.0.0.1:{config.cas_callback_port}/callback"
    iid = (config.instance_id if config else None) or instance_id
    if iid:
        store_refresh_context(iid, ctx)


async def ensure_write_permission(
    instance_id: str | None = None, config: Config | None = None
) -> str:
    """Ensure user has write permission and return access token.

    Combines authentication check with write permission verification.
    Checks if user has any role in WRITE_PERMISSIONS.

    Args:
        instance_id: Instance slug for token isolation.
        config: Pre-resolved Config.

    Returns:
        Valid access token for BPA API calls.

    Raises:
        ToolError: If not authenticated or lacks write permission.
    """
    # First ensure authenticated (raises if not)
    token = await ensure_authenticated(instance_id=instance_id, config=config)

    # Check for write permission
    token_manager = get_token_manager(instance_id)
    user_permissions = set(token_manager.permissions)

    if not user_permissions.intersection(WRITE_PERMISSIONS):
        raise ToolError(
            "Permission denied: You don't have write access. "
            "Contact your administrator."
        )

    return token


def check_permission(required_permission: str) -> None:
    """Check if user has a specific permission.

    This is a synchronous check that also validates authentication status.
    Raises ToolError if not authenticated or if permission is missing.

    Args:
        required_permission: The permission role required.

    Raises:
        ToolError: If not authenticated or user lacks the required permission.
    """
    token_manager = get_token_manager()

    # Verify authentication first (sync check - doesn't refresh)
    if not token_manager.is_authenticated():
        raise ToolError("Not authenticated. Run auth_login first.")

    if token_manager.is_token_expired():
        raise ToolError("Session expired. Please run auth_login again.")

    if required_permission not in token_manager.permissions:
        raise ToolError(
            f"Permission denied: You need '{required_permission}' permission. "
            "Contact your administrator."
        )
