"""Single Page App authentication (authorization_code + PKCE).

Use this for browser-based apps that cannot store a client secret.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Callable, Optional
from urllib.parse import urlencode

import httpx
from ._base import BaseAuth
from ._base_async import AsyncBaseAuth
from ._errors import ConfigurationError
from ._http import (
    DEFAULT_IMS_BASE_URL,
    DEFAULT_MAX_RETRIES,
    DEFAULT_SCOPES,
    DEFAULT_TIMEOUT,
    _build_urls,
    token_request,
    token_request_async,
)
from ._pkce import generate_code_challenge, generate_code_verifier
from ._token_manager import DEFAULT_REFRESH_BUFFER_SECONDS, TokenManager
from ._token_manager_async import AsyncTokenManager
from ._validation import _validate_redirect_uri_scheme


@dataclass(frozen=True)
class AuthorizationUrlResult:
    """Holds the authorization URL and the PKCE code verifier."""

    url: str
    code_verifier: str


class SPAAuth(BaseAuth):
    """Authenticate using authorization_code + PKCE (no client secret).

    Example::

        auth = SPAAuth(client_id="...", redirect_uri="https://myapp.com/cb")
        result = auth.get_authorization_url(state="random")
        # redirect user to result.url, store result.code_verifier
        auth.exchange_code(code="CODE", code_verifier=result.code_verifier)
        client = Frameio(token=auth.get_token)

    Args:
        client_id: Adobe IMS OAuth client ID.
        redirect_uri: Registered redirect URI.
        scopes: Space-separated scopes (OAuth 2.0 RFC 6749).
        ims_base_url: IMS base URL for staging/alternative environments. Defaults to production.
        http_client: Optional httpx.Client for proxy, TLS, or connection pooling.
        on_token_refreshed: Optional callback fired after every token refresh.
        timeout: HTTP request timeout in seconds. Defaults to 30.
        max_retries: Maximum number of retries for transient failures. Defaults to 2.
        refresh_buffer: Seconds before expiry to trigger proactive refresh. Defaults to 60.
    """

    def __init__(
        self,
        *,
        client_id: str,
        redirect_uri: str,
        scopes: str = DEFAULT_SCOPES,
        ims_base_url: str = DEFAULT_IMS_BASE_URL,
        http_client: httpx.Client | None = None,
        on_token_refreshed: Optional[Callable[[dict[str, Any]], None]] = None,
        timeout: float = DEFAULT_TIMEOUT,
        max_retries: int = DEFAULT_MAX_RETRIES,
        refresh_buffer: int = DEFAULT_REFRESH_BUFFER_SECONDS,
    ) -> None:
        if not client_id:
            raise ConfigurationError("client_id is required")
        if not redirect_uri:
            raise ConfigurationError("redirect_uri is required")
        _validate_redirect_uri_scheme(redirect_uri)

        self._client_id = client_id
        self._client_secret = None  # SPA has no secret
        self._redirect_uri = redirect_uri
        self._scopes = scopes
        self._timeout = timeout
        self._max_retries = max_retries
        self._http_client = http_client
        self._authorize_url, self._token_url, self._revoke_url = _build_urls(ims_base_url)

        self._token_manager = TokenManager(
            refresh_func=self._refresh_token,
            refresh_buffer=refresh_buffer,
            on_token_refreshed=on_token_refreshed,
        )

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def get_authorization_url(self, state: str) -> AuthorizationUrlResult:
        """Build the Adobe IMS authorization URL with PKCE challenge.

        Args:
            state: An opaque CSRF/state value.

        Returns:
            :class:`AuthorizationUrlResult` with ``url`` and ``code_verifier``.
        """
        code_verifier = generate_code_verifier()
        code_challenge = generate_code_challenge(code_verifier)

        params = {
            "client_id": self._client_id,
            "redirect_uri": self._redirect_uri,
            "scope": self._scopes,
            "response_type": "code",
            "state": state,
            "code_challenge": code_challenge,
            "code_challenge_method": "S256",
        }
        url = f"{self._authorize_url}?{urlencode(params)}"
        return AuthorizationUrlResult(url=url, code_verifier=code_verifier)

    def exchange_code(self, code: str, code_verifier: str) -> dict[str, Any]:
        """Exchange an authorization code + PKCE verifier for tokens.

        Args:
            code: The authorization code from the OAuth callback.
            code_verifier: The code verifier generated alongside the auth URL.

        Returns:
            Token response dict.
        """
        result = token_request(
            {
                "grant_type": "authorization_code",
                "client_id": self._client_id,
                "code": code,
                "redirect_uri": self._redirect_uri,
                "code_verifier": code_verifier,
            },
            timeout=self._timeout,
            max_retries=self._max_retries,
            client=self._http_client,
            token_url=self._token_url,
        )
        self._store_tokens(result)
        return result

    def refresh(self) -> dict[str, Any]:
        """Manually trigger a token refresh."""
        result = self._refresh_token()
        self._store_tokens(result)
        return result

    # ------------------------------------------------------------------
    # Internal
    # ------------------------------------------------------------------

    def _refresh_token(self) -> dict[str, Any]:
        """Refresh the access token using the refresh token (no secret)."""
        refresh_tok = self._token_manager.refresh_token
        if not refresh_tok:
            raise ConfigurationError(
                "No refresh token available. Call exchange_code() first."
            )
        return token_request(
            {
                "grant_type": "refresh_token",
                "client_id": self._client_id,
                "refresh_token": refresh_tok,
            },
            timeout=self._timeout,
            max_retries=self._max_retries,
            client=self._http_client,
            token_url=self._token_url,
        )


class AsyncSPAAuth(AsyncBaseAuth):
    """Async SPA auth (authorization_code + PKCE). Use with async Frame.io SDK."""

    def __init__(
        self,
        *,
        client_id: str,
        redirect_uri: str,
        scopes: str = DEFAULT_SCOPES,
        ims_base_url: str = DEFAULT_IMS_BASE_URL,
        http_client: httpx.AsyncClient | None = None,
        on_token_refreshed: Optional[Callable[[dict[str, Any]], None]] = None,
        timeout: float = DEFAULT_TIMEOUT,
        max_retries: int = DEFAULT_MAX_RETRIES,
        refresh_buffer: int = DEFAULT_REFRESH_BUFFER_SECONDS,
    ) -> None:
        if not client_id:
            raise ConfigurationError("client_id is required")
        if not redirect_uri:
            raise ConfigurationError("redirect_uri is required")
        _validate_redirect_uri_scheme(redirect_uri)

        self._client_id = client_id
        self._client_secret = None
        self._redirect_uri = redirect_uri
        self._scopes = scopes
        self._timeout = timeout
        self._max_retries = max_retries
        self._http_client = http_client
        self._authorize_url, self._token_url, self._revoke_url = _build_urls(ims_base_url)

        self._token_manager = AsyncTokenManager(
            refresh_func=self._refresh_token,
            refresh_buffer=refresh_buffer,
            on_token_refreshed=on_token_refreshed,
        )

    def get_authorization_url(self, state: str) -> AuthorizationUrlResult:
        """Build the Adobe IMS authorization URL with PKCE (sync, no I/O)."""
        code_verifier = generate_code_verifier()
        code_challenge = generate_code_challenge(code_verifier)
        params = {
            "client_id": self._client_id,
            "redirect_uri": self._redirect_uri,
            "scope": self._scopes,
            "response_type": "code",
            "state": state,
            "code_challenge": code_challenge,
            "code_challenge_method": "S256",
        }
        url = f"{self._authorize_url}?{urlencode(params)}"
        return AuthorizationUrlResult(url=url, code_verifier=code_verifier)

    async def exchange_code(self, code: str, code_verifier: str) -> dict[str, Any]:
        """Exchange an authorization code + PKCE verifier for tokens."""
        result = await token_request_async(
            {
                "grant_type": "authorization_code",
                "client_id": self._client_id,
                "code": code,
                "redirect_uri": self._redirect_uri,
                "code_verifier": code_verifier,
            },
            timeout=self._timeout,
            max_retries=self._max_retries,
            client=self._http_client,
            token_url=self._token_url,
        )
        self._store_tokens(result)
        return result

    async def refresh(self) -> dict[str, Any]:
        """Manually trigger a token refresh."""
        result = await self._refresh_token()
        self._store_tokens(result)
        return result

    async def _refresh_token(self) -> dict[str, Any]:
        refresh_tok = self._token_manager.refresh_token
        if not refresh_tok:
            raise ConfigurationError(
                "No refresh token available. Call exchange_code() first."
            )
        return await token_request_async(
            {
                "grant_type": "refresh_token",
                "client_id": self._client_id,
                "refresh_token": refresh_tok,
            },
            timeout=self._timeout,
            max_retries=self._max_retries,
            client=self._http_client,
            token_url=self._token_url,
        )
