"""Device authorization flow for Maeris AI.

Standard device auth (RFC 8628-style):
  POST /cli/device/start  → {device_code, verification_url, expires_in, interval}
  POST /cli/device/poll   → {access_token, plan}   — authenticated
                          → {pending: true}          — still waiting
                          → {error: "..."}           — denied / expired

No browser redirect — user opens the URL themselves and enters the displayed code.
The CLI polls until auth completes or the code expires.
"""

from __future__ import annotations

import time
from dataclasses import dataclass, field

import httpx

# Defaults — the server's interval field takes precedence if provided
DEFAULT_POLL_INTERVAL = 5     # seconds between poll requests
DEFAULT_POLL_TIMEOUT = 300    # seconds before giving up
_REQUEST_TIMEOUT = 15.0       # httpx socket timeout


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


@dataclass
class DeviceCodeResponse:
    """Response from POST /cli/device/start."""

    device_code: str
    verification_url: str
    expires_in: int = 300
    interval: int = DEFAULT_POLL_INTERVAL


@dataclass
class MaerisAIToken:
    """Successful token returned by POST /cli/device/poll."""

    access_token: str
    plan: str = "developer"
    token_type: str = "Bearer"
    expires_at: str = ""     # ISO datetime, empty if not provided by server


# ---------------------------------------------------------------------------
# Errors
# ---------------------------------------------------------------------------


class DeviceFlowError(Exception):
    """Device auth failed: access denied, code expired, or server error."""


# ---------------------------------------------------------------------------
# Flow
# ---------------------------------------------------------------------------


class DeviceFlow:
    """Two-phase Maeris AI device authorization flow.

    Usage::

        flow = DeviceFlow(api_url)

        # Phase 1 — get and display device code
        code = flow.get_device_code()
        print(f"Open: {code.verification_url}")
        print(f"Code: {code.device_code}")

        # Phase 2 — poll until the user completes browser auth
        token = flow.wait_for_token(code.device_code, code.expires_in, code.interval)
    """

    def __init__(self, api_url: str) -> None:
        self.api_url = api_url.rstrip("/")

    # ------------------------------------------------------------------
    # Phase 1
    # ------------------------------------------------------------------

    def get_device_code(self) -> DeviceCodeResponse:
        """Request a device code from the Maeris backend.

        Raises:
            DeviceFlowError: If the request fails.
        """
        try:
            resp = httpx.post(
                f"{self.api_url}/cli/device/start",
                timeout=_REQUEST_TIMEOUT,
            )
            resp.raise_for_status()
            data = resp.json()
        except httpx.HTTPStatusError as e:
            raise DeviceFlowError(
                f"Failed to start device auth (HTTP {e.response.status_code})"
            ) from e
        except httpx.RequestError as e:
            raise DeviceFlowError(f"Network error reaching Maeris: {e}") from e
        except Exception as e:
            raise DeviceFlowError(f"Unexpected error: {e}") from e

        try:
            return DeviceCodeResponse(
                device_code=data["device_code"],
                verification_url=data["verification_url"],
                expires_in=int(data.get("expires_in", 300)),
                interval=int(data.get("interval", DEFAULT_POLL_INTERVAL)),
            )
        except KeyError as e:
            raise DeviceFlowError(f"Invalid server response (missing field: {e})") from e

    # ------------------------------------------------------------------
    # Phase 2 — single poll
    # ------------------------------------------------------------------

    def poll_once(self, device_code: str) -> MaerisAIToken | None:
        """Poll once for a completed token.

        Returns:
            MaerisAIToken if auth is complete, None if still pending.

        Raises:
            DeviceFlowError: If the code was denied, expired, or the server
                             returned an unexpected error.
        """
        try:
            resp = httpx.post(
                f"{self.api_url}/cli/device/poll",
                json={"device_code": device_code},
                timeout=_REQUEST_TIMEOUT,
            )
        except httpx.RequestError as e:
            raise DeviceFlowError(f"Network error during poll: {e}") from e

        # 200 — parse body for success / pending / error
        if resp.status_code == 200:
            try:
                data = resp.json()
            except Exception:
                return None

            if "access_token" in data:
                return MaerisAIToken(
                    access_token=data["access_token"],
                    plan=data.get("plan", "developer"),
                    token_type=data.get("token_type", "Bearer"),
                    expires_at=data.get("expires_at", ""),
                )

            if data.get("pending"):
                return None

            if err := data.get("error"):
                raise DeviceFlowError(f"Authorization failed: {err}")

            return None

        # 202 Accepted — still pending (alternative server convention)
        if resp.status_code == 202:
            return None

        # 400 — denied or expired
        if resp.status_code == 400:
            try:
                err = resp.json().get("error", "Authorization denied or code expired")
            except Exception:
                err = "Authorization denied or code expired"
            raise DeviceFlowError(err)

        raise DeviceFlowError(
            f"Unexpected response from Maeris during poll (HTTP {resp.status_code})"
        )

    # ------------------------------------------------------------------
    # Phase 2 — full polling loop
    # ------------------------------------------------------------------

    def wait_for_token(
        self,
        device_code: str,
        expires_in: int = DEFAULT_POLL_TIMEOUT,
        interval: int = DEFAULT_POLL_INTERVAL,
        progress_callback=None,
    ) -> MaerisAIToken:
        """Poll until the user completes browser auth or the code expires.

        Args:
            device_code: Code returned by get_device_code().
            expires_in: How long (seconds) before the code expires.
            interval: Seconds to wait between polls.
            progress_callback: Optional callable() called before each poll
                               (useful for printing a progress dot).

        Returns:
            MaerisAIToken on success.

        Raises:
            DeviceFlowError: If auth was denied or the server returned an error.
            TimeoutError: If the code expired before the user authenticated.
        """
        deadline = time.monotonic() + expires_in
        effective_interval = max(1, interval)

        while time.monotonic() < deadline:
            time.sleep(effective_interval)

            if progress_callback:
                progress_callback()

            token = self.poll_once(device_code)
            if token is not None:
                return token

        raise TimeoutError(
            "Device code expired before authentication completed. "
            "Run 'maeris login' to try again."
        )
