"""Azure AD authentication flows for Navari CLI using MSAL.

Uses Microsoft Authentication Library (MSAL) for all OAuth flows,
providing automatic token caching and silent refresh via refresh tokens.
"""

import asyncio
import concurrent.futures
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse

import msal
import typer
from rich.console import Console

from navari_cli.constants import DEFAULT_REDIRECT_PORT, DEFAULT_REDIRECT_URI

console = Console()


def _build_msal_app(
    auth_config: dict, token_cache: msal.SerializableTokenCache | None = None
) -> msal.PublicClientApplication:
    """Create an MSAL PublicClientApplication from server auth config."""
    return msal.PublicClientApplication(
        client_id=auth_config["client_id"],
        authority=auth_config["authority"],
        token_cache=token_cache,
    )


def _extract_token(result: dict) -> tuple[str, str | None]:
    """Extract access token from MSAL result, raising on error."""
    if "access_token" in result:
        return result["access_token"], result.get("id_token")

    error = result.get("error", "unknown_error")
    error_desc = result.get("error_description", error)
    raise ValueError(f"Authentication failed: {error_desc}")


async def acquire_token_silent(auth_config: dict, token_cache: msal.SerializableTokenCache) -> str | None:
    """Try to silently acquire a token using cached refresh token.

    Returns the access token if refresh succeeded, None otherwise.
    """
    app = _build_msal_app(auth_config, token_cache)
    accounts = app.get_accounts()
    if not accounts:
        return None

    result = app.acquire_token_silent(
        scopes=auth_config["scopes"],
        account=accounts[0],
    )
    if result and "access_token" in result:
        return result["access_token"]
    return None


async def browser_oauth_flow(
    auth_config: dict, token_cache: msal.SerializableTokenCache | None = None
) -> tuple[str, str | None]:
    """Browser-based OAuth flow using MSAL's auth code flow with PKCE.

    Args:
        auth_config: Dict with keys: authority, client_id, scopes
        token_cache: Optional MSAL token cache for persisting refresh tokens

    Returns:
        Tuple of (access_token, id_token)
    """
    app = _build_msal_app(auth_config, token_cache)
    redirect_uri = auth_config.get("redirect_uri", DEFAULT_REDIRECT_URI)

    # Initiate auth code flow (MSAL handles PKCE internally)
    flow = app.initiate_auth_code_flow(
        scopes=auth_config["scopes"],
        redirect_uri=redirect_uri,
    )

    if "auth_uri" not in flow:
        raise ValueError(f"Failed to initiate auth flow: {flow.get('error_description', 'unknown error')}")

    # Start local HTTP server to receive the callback
    callback_params: dict = {}

    class CallbackHandler(BaseHTTPRequestHandler):
        def do_GET(self):
            nonlocal callback_params
            callback_params = dict(parse_qs(urlparse(self.path).query))
            callback_params = {k: v[0] if len(v) == 1 else v for k, v in callback_params.items()}
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            success = "code" in callback_params
            html = (
                '<html><body style="font-family:system-ui;text-align:center;padding-top:100px;">'
                + (
                    '<h1 style="color:#2e7d32;">Login Successful!</h1>'
                    '<p style="color:#666;">You can close this window and return to the CLI.</p>'
                    if success
                    else '<h1 style="color:#c62828;">Authentication Failed</h1>'
                    '<p style="color:#666;">Please try again or use device code flow.</p>'
                )
                + "</body></html>"
            )
            self.wfile.write(html.encode())

        def log_message(self, format, *args):
            pass

    try:
        server = HTTPServer(("localhost", DEFAULT_REDIRECT_PORT), CallbackHandler)
        server.socket.settimeout(1.0)
    except OSError as e:
        raise RuntimeError(
            f"Failed to start local server on port {DEFAULT_REDIRECT_PORT}: {e}. "
            "Port may be in use. Try using device code flow instead: --device-code"
        ) from e

    console.print("[bold cyan]Opening browser for login...[/bold cyan]")
    try:
        webbrowser.open(flow["auth_uri"])
    except Exception as e:
        console.print(f"[yellow]Could not open browser: {e}[/yellow]")
        console.print(f"[yellow]Please manually open: {flow['auth_uri']}[/yellow]")

    console.print("[dim]Waiting for login in browser... (Ctrl+C to cancel)[/dim]")
    try:
        for _ in range(300):
            try:
                server.handle_request()
            except OSError:
                pass
            if callback_params:
                break
            await asyncio.sleep(0.1)
        else:
            raise TimeoutError("Login timeout - no response from browser after 5 minutes")
    except KeyboardInterrupt:
        server.server_close()
        console.print("\n[yellow]Login cancelled[/yellow]")
        raise typer.Abort() from None
    finally:
        server.server_close()

    if "error" in callback_params:
        raise ValueError(f"Authentication error: {callback_params['error']}")

    # Complete the auth code flow — MSAL exchanges code for tokens and caches them
    result = app.acquire_token_by_auth_code_flow(flow, callback_params)
    return _extract_token(result)


async def password_flow(
    auth_config: dict,
    username: str,
    password: str,
    token_cache: msal.SerializableTokenCache | None = None,
) -> tuple[str, str | None]:
    """Resource Owner Password Credentials (ROPC) flow for non-interactive login.

    Useful for CI/automation when the user has no MFA and the tenant allows ROPC.

    Args:
        auth_config: Dict with keys: authority, client_id, scopes
        username: User's email or UPN
        password: User's password
        token_cache: Optional MSAL token cache for persisting refresh tokens

    Returns:
        Tuple of (access_token, id_token)
    """
    app = _build_msal_app(auth_config, token_cache)
    result = app.acquire_token_by_username_password(
        username=username,
        password=password,
        scopes=auth_config["scopes"],
    )
    return _extract_token(result)


async def device_code_flow(
    auth_config: dict, token_cache: msal.SerializableTokenCache | None = None
) -> tuple[str, str | None]:
    """Device code flow for headless environments.

    Args:
        auth_config: Dict with keys: authority, client_id, scopes
        token_cache: Optional MSAL token cache for persisting refresh tokens

    Returns:
        Tuple of (access_token, id_token)
    """
    app = _build_msal_app(auth_config, token_cache)
    flow = app.initiate_device_flow(scopes=auth_config["scopes"])

    if "user_code" not in flow:
        raise ValueError(f"Failed to initiate device flow: {flow.get('error_description', 'unknown error')}")

    console.print("\n[bold yellow]Device Code Authentication[/bold yellow]")
    console.print(f"1. Visit: [cyan]{flow['verification_uri']}[/cyan]")
    console.print(f"2. Enter code: [bold green]{flow['user_code']}[/bold green]\n")
    console.print("[dim]Waiting for authentication...[/dim]")

    # MSAL's acquire_token_by_device_flow polls synchronously; run in thread
    with concurrent.futures.ThreadPoolExecutor() as pool:
        result = await asyncio.get_event_loop().run_in_executor(pool, lambda: app.acquire_token_by_device_flow(flow))

    return _extract_token(result)
