"""Resolve which WorkIQ endpoint to use, with consent-driven Graph fallback.

The CLI prefers the direct WorkIQ A2A endpoint. When token acquisition fails
with a consent / service-principal error (the tenant is not provisioned for the
A2A service), this module transparently falls back to the Microsoft Graph
gateway — switching both the endpoint and the OAuth scopes and re-acquiring a
token — mirroring the WorkIQ CLI's ``ResolveEndpointWithFallbackAsync``.

The returned ``AuthHandler`` always carries the *resolved* scopes, so a later
401 refresh re-acquires against the same scopes that produced the working token.
"""

from dataclasses import dataclass
from typing import Callable, Optional

from auth.auth_handler import AuthHandler
from auth.consent_errors import extract_error_text, is_consent_or_sp_error
from env_validator import ALLOWED_ENDPOINTS, validate_endpoint_url
from cli_logging.cli_logger import emit_structured_log
from cli_logging.logging_utils import Operation


@dataclass(frozen=True)
class ResolvedAuth:
    """Outcome of endpoint resolution.

    Attributes:
        endpoint: The endpoint the agent client should target.
        scopes_str: The scopes used to acquire ``access_token``.
        auth_handler: The handler bound to ``scopes_str`` (used for refresh).
        access_token: A valid access token for ``endpoint``.
        used_fallback: True when the Graph gateway fallback was taken.
    """

    endpoint: str
    scopes_str: str
    auth_handler: AuthHandler
    access_token: str
    used_fallback: bool


def _acquire_token(handler: AuthHandler) -> tuple:
    """Acquire a token, returning (access_token, error_text).

    Distinguishes the two ways the broker reports failure: a returned result
    dict with no ``access_token`` (error fields populated) and a raised
    exception. On success ``error_text`` is None; on failure ``access_token``
    is an empty string.
    """
    result = handler.acquire_token_interactive() or {}
    token = result.get("access_token") or ""
    if token:
        return token, None
    return "", extract_error_text(result)


def resolve_endpoint_and_token(
    *,
    primary_endpoint: str,
    primary_scopes_str: str,
    graph_endpoint: str,
    graph_scopes_str: str,
    client_id: str,
    tenant_id: str,
    account_hint: Optional[str] = None,
    make_handler: Callable[..., AuthHandler] = AuthHandler,
    log: Callable[..., None] = emit_structured_log,
) -> ResolvedAuth:
    """Acquire a token for the primary endpoint, falling back to Graph.

    Args:
        primary_endpoint: The configured (A2A) endpoint.
        primary_scopes_str: Scopes for the primary endpoint.
        graph_endpoint: The Graph gateway fallback endpoint.
        graph_scopes_str: Scopes for the Graph gateway.
        client_id: App registration client id (shared by both paths).
        tenant_id: Directory/tenant id.
        account_hint: Optional account (email/UPN) to scope authentication to a
            specific cached MSAL account. Forwarded to every ``AuthHandler``.
        make_handler: Factory for ``AuthHandler`` (injectable for tests).
        log: Structured-log callable (injectable for tests).

    Returns:
        A ``ResolvedAuth`` describing the endpoint, scopes, handler and token.

    Raises:
        RuntimeError: When a non-consent auth failure occurs or the Graph
            fallback also fails to yield a token.
        Exception: Propagated from the broker on non-consent exceptions.
    """
    handler = make_handler(
        client_id=client_id,
        tenant_id=tenant_id,
        scopes_str=primary_scopes_str,
        account_hint=account_hint,
    )

    try:
        token, error_text = _acquire_token(handler)
    except Exception as exc:  # broker raised
        if is_consent_or_sp_error(extract_error_text(exc)):
            return _fallback_to_graph(
                graph_endpoint=graph_endpoint,
                graph_scopes_str=graph_scopes_str,
                client_id=client_id,
                tenant_id=tenant_id,
                account_hint=account_hint,
                make_handler=make_handler,
                log=log,
            )
        raise

    if token:
        return ResolvedAuth(
            endpoint=primary_endpoint,
            scopes_str=primary_scopes_str,
            auth_handler=handler,
            access_token=token,
            used_fallback=False,
        )

    # Broker returned a result dict without a token.
    if is_consent_or_sp_error(error_text):
        return _fallback_to_graph(
            graph_endpoint=graph_endpoint,
            graph_scopes_str=graph_scopes_str,
            client_id=client_id,
            tenant_id=tenant_id,
            account_hint=account_hint,
            make_handler=make_handler,
            log=log,
        )

    raise RuntimeError("Failed to acquire A2A access token")


def _fallback_to_graph(
    *,
    graph_endpoint: str,
    graph_scopes_str: str,
    client_id: str,
    tenant_id: str,
    account_hint: Optional[str] = None,
    make_handler: Callable[..., AuthHandler],
    log: Callable[..., None],
) -> ResolvedAuth:
    """Acquire a token against the Graph gateway after an A2A consent error."""
    log(
        "info",
        "Tenant is not provisioned for the WorkIQ A2A endpoint; falling back "
        f"to the Microsoft Graph gateway ({graph_endpoint}).",
        operation=Operation.AUTHENTICATE,
    )
    validate_endpoint_url(graph_endpoint, ALLOWED_ENDPOINTS)
    handler = make_handler(
        client_id=client_id,
        tenant_id=tenant_id,
        scopes_str=graph_scopes_str,
        account_hint=account_hint,
    )
    token, _ = _acquire_token(handler)
    if not token:
        raise RuntimeError(
            "Failed to acquire Graph-gateway token after A2A consent fallback."
        )
    return ResolvedAuth(
        endpoint=graph_endpoint,
        scopes_str=graph_scopes_str,
        auth_handler=handler,
        access_token=token,
        used_fallback=True,
    )
