"""OAuth 2.0 Device Authorization Grant (RFC 8628) for the Sika CLI.

Implements the device code flow for headless environments (SSH, Docker,
WSL, CI containers) where a localhost callback server cannot work.

Flow:

1. CLI requests a device code from the authorization server.
2. CLI displays a user code and verification URL to the user.
3. User visits the URL in any browser (on any device) and enters the code.
4. CLI polls the token endpoint until the user approves or the code expires.
5. On approval, tokens are exchanged for a long-lived API token.

**Backend dependency:** The Cognito app client must be configured to
support the ``urn:ietf:params:oauth:grant-type:device_code`` grant type.
If not yet configured, the CLI-side code is ready and tests use mocked
responses.  See ``XR-9`` in the sika-core-platform roadmap.
"""

from __future__ import annotations

import time
from dataclasses import dataclass

import httpx

from sika_cli.auth import AuthFlowError, TokenResponse

# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------

_DEVICE_AUTH_TIMEOUT = 30.0


@dataclass(frozen=True)
class DeviceCodeResponse:
    """Response from the device authorization endpoint (RFC 8628 Section 3.2).

    Attributes:
        device_code: The device verification code (opaque to the CLI).
        user_code: The short code the user must enter at the verification URL.
        verification_uri: The URL the user visits to authorize.
        verification_uri_complete: Optional URL with the user code pre-filled.
        expires_in: Lifetime of the device code in seconds.
        interval: Minimum polling interval in seconds.
    """

    device_code: str
    user_code: str
    verification_uri: str
    verification_uri_complete: str | None
    expires_in: int
    interval: int


# ---------------------------------------------------------------------------
# Error classes
# ---------------------------------------------------------------------------


class DeviceCodeExpiredError(AuthFlowError):
    """Raised when the device code expires before the user approves."""


class DeviceCodeDeniedError(AuthFlowError):
    """Raised when the user explicitly denies the authorization request."""


# ---------------------------------------------------------------------------
# Device authorization request
# ---------------------------------------------------------------------------


def request_device_code(
    cognito_domain: str,
    client_id: str,
    *,
    scopes: str = "openid email profile",
) -> DeviceCodeResponse:
    """Request a device code from the authorization server.

    Calls the device authorization endpoint (RFC 8628 Section 3.1).

    Args:
        cognito_domain: Cognito custom domain
            (e.g. ``auth.sikasecurity.com``).
        client_id: OAuth client ID for the CLI.
        scopes: OAuth scopes to request.

    Returns:
        A :class:`DeviceCodeResponse` with the user code and
        verification URL.

    Raises:
        AuthFlowError: If the device authorization request fails.
    """
    url = f"https://{cognito_domain}/oauth2/device_authorization"
    try:
        response = httpx.post(
            url,
            data={
                "client_id": client_id,
                "scope": scopes,
            },
            headers={"Content-Type": "application/x-www-form-urlencoded"},
            timeout=_DEVICE_AUTH_TIMEOUT,
        )
        response.raise_for_status()
    except httpx.HTTPStatusError as exc:
        raise AuthFlowError(
            f"Device authorization failed (HTTP {exc.response.status_code})"
        ) from None
    except httpx.RequestError as exc:
        raise AuthFlowError(
            f"Device authorization failed: {type(exc).__name__}"
        ) from None

    data = response.json()
    return DeviceCodeResponse(
        device_code=data["device_code"],
        user_code=data["user_code"],
        verification_uri=data["verification_uri"],
        verification_uri_complete=data.get("verification_uri_complete"),
        expires_in=data["expires_in"],
        interval=data.get("interval", 5),
    )


# ---------------------------------------------------------------------------
# Token polling
# ---------------------------------------------------------------------------


def poll_for_tokens(
    cognito_domain: str,
    client_id: str,
    device_code: str,
    *,
    interval: int = 5,
    timeout: float = 300.0,
) -> TokenResponse:
    """Poll the token endpoint until the user approves or the code expires.

    Implements the polling loop described in RFC 8628 Section 3.3--3.5.
    Handles the standard error responses:

    - ``authorization_pending``: Continue polling.
    - ``slow_down``: Increase the polling interval by 5 seconds.
    - ``expired_token``: Raise :class:`DeviceCodeExpiredError`.
    - ``access_denied``: Raise :class:`DeviceCodeDeniedError`.

    Args:
        cognito_domain: Cognito custom domain.
        client_id: OAuth client ID.
        device_code: The device code from :func:`request_device_code`.
        interval: Initial polling interval in seconds.
        timeout: Maximum time to wait in seconds (default 5 minutes).

    Returns:
        A :class:`~sika_cli.auth.TokenResponse` with OAuth tokens.

    Raises:
        DeviceCodeExpiredError: If the device code expires or the
            timeout is reached.
        DeviceCodeDeniedError: If the user denies the request.
        AuthFlowError: On unexpected errors.
    """
    token_url = f"https://{cognito_domain}/oauth2/token"
    current_interval = interval
    elapsed = 0.0

    while elapsed < timeout:
        time.sleep(current_interval)
        elapsed += current_interval

        try:
            response = httpx.post(
                token_url,
                data={
                    "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
                    "client_id": client_id,
                    "device_code": device_code,
                },
                headers={"Content-Type": "application/x-www-form-urlencoded"},
                timeout=_DEVICE_AUTH_TIMEOUT,
            )
        except httpx.RequestError as exc:
            raise AuthFlowError(
                f"Token polling failed: {type(exc).__name__}"
            ) from None

        # Success — user approved
        if response.status_code == 200:
            data = response.json()
            return TokenResponse(
                access_token=data["access_token"],
                id_token=data["id_token"],
                refresh_token=data["refresh_token"],
                expires_in=data["expires_in"],
            )

        # Polling errors (RFC 8628 Section 3.5)
        if response.status_code == 400:
            data = response.json()
            error = data.get("error", "")

            if error == "authorization_pending":
                continue
            if error == "slow_down":
                current_interval += 5
                continue
            if error == "expired_token":
                raise DeviceCodeExpiredError(
                    "Device code expired. Please try again."
                )
            if error == "access_denied":
                raise DeviceCodeDeniedError(
                    "Authorization denied by the user."
                )
            raise AuthFlowError(f"Device code flow failed: {error}")

        # Unexpected status code
        raise AuthFlowError(
            f"Device code flow failed (HTTP {response.status_code})"
        )

    raise DeviceCodeExpiredError(
        "Device code flow timed out waiting for user approval."
    )
