"""Auth resolution for CLI commands.

Provides a single entry point — resolve_auth() — that determines the
auth token for any CLI invocation. The hierarchy is explicit and fixed:

    HIERARCHY (checked in order, first match wins):
      1. Connection stored auth   — set during 'mcpbundles connect'
      2. MCPBUNDLES_API_KEY env   — ad-hoc / CI usage
      3. Global OAuth token       — from 'mcpbundles login'

Connection selection follows progressive disclosure:
  - 0 connections: use env var or global login directly
  - 1 connection:  auto-select it, no --as needed
  - N connections: require --as or an explicit --default
"""

import logging
import os
from datetime import datetime, timedelta

import requests

from mcpbundles.config import AuthError, CLISettings
from mcpbundles.connections import ConnectionInfo, ConnectionManager
from mcpbundles.exit_codes import USER_ERROR
from mcpbundles.output import render_error

logger = logging.getLogger(__name__)


class AuthResult:
    """Resolved connection target + auth token."""

    __slots__ = ("url", "endpoint", "conn_name", "token")

    def __init__(self, url: str, endpoint: str, conn_name: str | None, token: str) -> None:
        self.url = url
        self.endpoint = endpoint
        self.conn_name = conn_name
        self.token = token


def resolve_auth(settings: CLISettings, connection: str | None) -> AuthResult:
    """Resolve which connection to use and how to authenticate.

    Raises AuthError with an actionable message when no auth source
    is available. Raises SystemExit(USER_ERROR) for connection-not-found
    or ambiguous-default situations.
    """
    mgr = ConnectionManager()

    if connection:
        conn = mgr.get(connection.lstrip("@"))
        if not conn:
            render_error(f"Connection '{connection}' not found. Use 'mcpbundles connect' first.")
            raise SystemExit(USER_ERROR)
        token = _auth_for_connection(conn, mgr, settings)
        return AuthResult(conn.url, conn.endpoint, conn.name, token)

    conn = _select_connection(mgr)

    if conn:
        token = _auth_for_connection(conn, mgr, settings)
        return AuthResult(conn.url, conn.endpoint, conn.name, token)

    token = _auth_without_connection(settings)
    return AuthResult(settings.base_url, "/hub/", None, token)


def resolve_auth_for_named(
    conn: ConnectionInfo, mgr: ConnectionManager, settings: CLISettings,
) -> str:
    """Resolve auth for a specific ConnectionInfo (used by serve aggregation)."""
    return _auth_for_connection(conn, mgr, settings)


# ── Connection selection ───────────────────────────────────────────


def _select_connection(mgr: ConnectionManager) -> ConnectionInfo | None:
    """Pick the active connection using progressive disclosure rules."""
    conns = mgr.list_connections()

    if len(conns) == 0:
        return None

    if len(conns) == 1:
        return conns[0]

    explicit_default = next((c for c in conns if c.is_default), None)
    if explicit_default:
        return explicit_default

    names = ", ".join(c.name for c in conns)
    render_error(
        f"Multiple connections exist ({names}) but none is default. "
        f"Use --as <name> or run 'mcpbundles connect <name> --default'."
    )
    raise SystemExit(USER_ERROR)


# ── Auth hierarchy ─────────────────────────────────────────────────


def _auth_for_connection(
    conn: ConnectionInfo, mgr: ConnectionManager, settings: CLISettings,
) -> str:
    """Walk the auth hierarchy for a resolved connection.

    1. Connection stored auth (API key or OAuth token, with auto-refresh)
    2. MCPBUNDLES_API_KEY env var
    3. Global OAuth token from 'mcpbundles login'
    """
    stored = _get_stored_auth(conn, mgr)
    if stored:
        return stored

    env_key = os.getenv("MCPBUNDLES_API_KEY")
    if env_key:
        return env_key

    global_token = _get_global_token(settings)
    if global_token:
        return global_token

    raise AuthError(
        f"Connection '{conn.name}' has no stored credentials.\n"
        f"  Fix: re-run 'mcpbundles connect {conn.name}' to store an API key or use --oauth"
    )


def _auth_without_connection(settings: CLISettings) -> str:
    """Walk the auth hierarchy when no connection is configured.

    1. MCPBUNDLES_API_KEY env var
    2. Global OAuth token from 'mcpbundles login'
    """
    env_key = os.getenv("MCPBUNDLES_API_KEY")
    if env_key:
        return env_key

    global_token = _get_global_token(settings)
    if global_token:
        return global_token

    raise AuthError(
        "No credentials found.\n"
        "  Option 1: mcpbundles connect <name>     — set up a named connection with stored auth\n"
        "  Option 2: export MCPBUNDLES_API_KEY=...  — provide an API key via environment\n"
        "  Option 3: mcpbundles login               — authenticate via browser OAuth"
    )


# ── Auth sources ───────────────────────────────────────────────────


def _get_stored_auth(conn: ConnectionInfo, mgr: ConnectionManager) -> str | None:
    """Return the connection's stored token, refreshing OAuth if expired."""
    if conn.auth_type == "oauth":
        return _get_oauth_token(conn, mgr)
    return conn.get_auth_token()


def _get_global_token(settings: CLISettings) -> str | None:
    """Return the global OAuth token from 'mcpbundles login', or None."""
    try:
        return settings.require_auth()
    except AuthError:
        return None


# ── OAuth token lifecycle ──────────────────────────────────────────


def _get_oauth_token(conn: ConnectionInfo, mgr: ConnectionManager) -> str | None:
    """Return the OAuth access token, refreshing transparently if near expiry."""
    refresh_data = conn.get_refresh_data()
    if not refresh_data:
        return conn.get_auth_token()

    expires_at_str = refresh_data.get("expires_at")
    if expires_at_str:
        try:
            expires_at = datetime.fromisoformat(expires_at_str)
            if datetime.now() < expires_at - timedelta(minutes=5):
                return conn.get_auth_token()
        except (ValueError, TypeError):
            pass

    return _refresh_oauth_token(conn, mgr, refresh_data)


def _refresh_oauth_token(
    conn: ConnectionInfo, mgr: ConnectionManager, refresh_data: dict,
) -> str | None:
    """Exchange a refresh token for a new access token and persist it."""
    oauth_base = "http://localhost:8000" if "localhost" in conn.url else "https://api.mcpbundles.com"
    token_url = f"{oauth_base}/o/token/"

    try:
        response = requests.post(token_url, data={
            "grant_type": "refresh_token",
            "refresh_token": refresh_data["refresh_token"],
            "client_id": refresh_data["client_id"],
            "client_secret": refresh_data["client_secret"],
        })
        if response.status_code != 200:
            logger.warning(
                "OAuth refresh failed for '%s' (HTTP %d): %s",
                conn.name, response.status_code, response.text,
            )
            return conn.get_auth_token()

        new_token = response.json()
        expires_in = new_token.get("expires_in", 3600)
        new_expires = (datetime.now() + timedelta(seconds=expires_in)).isoformat()

        conn.set_auth_token(new_token["access_token"])
        conn.set_refresh_data({
            "client_id": refresh_data["client_id"],
            "client_secret": refresh_data["client_secret"],
            "refresh_token": new_token.get("refresh_token", refresh_data["refresh_token"]),
            "expires_at": new_expires,
        })
        mgr._save(conn)

        logger.info("OAuth token refreshed for connection '%s'", conn.name)
        return new_token["access_token"]

    except Exception:
        logger.warning("OAuth refresh error for '%s'", conn.name, exc_info=True)
        return conn.get_auth_token()
