"""Web App authentication (authorization_code grant).

Use this for server-side applications that can securely store a client secret.
"""

from __future__ import annotations

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 ._token_manager import DEFAULT_REFRESH_BUFFER_SECONDS, TokenManager
from ._token_manager_async import AsyncTokenManager
from ._validation import _validate_redirect_uri_scheme


class WebAppAuth(BaseAuth):
    """Authenticate using the OAuth 2.0 authorization_code grant.

    Example::

        auth = WebAppAuth(
            client_id="...",
            client_secret="...",
            redirect_uri="https://myapp.com/callback",
        )
        url = auth.get_authorization_url(state="random-csrf-token")
        # redirect user to url ...
        auth.exchange_code(code="CODE_FROM_CALLBACK")
        client = Frameio(token=auth.get_token)

    Args:
        client_id: Adobe IMS OAuth client ID.
        client_secret: Adobe IMS OAuth client secret.
        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,
        client_secret: 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 client_secret:
            raise ConfigurationError("client_secret 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 = client_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) -> str:
        """Build the Adobe IMS authorization URL.

        Args:
            state: An opaque CSRF/state value that will be echoed back.

        Returns:
            The full authorization URL to redirect the user to.
        """
        params = {
            "client_id": self._client_id,
            "redirect_uri": self._redirect_uri,
            "scope": self._scopes,
            "response_type": "code",
            "state": state,
        }
        return f"{self._authorize_url}?{urlencode(params)}"

    def exchange_code(self, code: str) -> dict[str, Any]:
        """Exchange an authorization code for access and refresh tokens.

        Args:
            code: The authorization code from the OAuth callback.

        Returns:
            Token response dict.
        """
        if self._client_secret is None:
            raise ConfigurationError("client_secret is required")
        result = token_request(
            {
                "grant_type": "authorization_code",
                "client_id": self._client_id,
                "client_secret": self._client_secret,
                "code": code,
                "redirect_uri": self._redirect_uri,
            },
            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.

        Returns:
            Token response dict.
        """
        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."""
        refresh_tok = self._token_manager.refresh_token
        if not refresh_tok:
            raise ConfigurationError(
                "No refresh token available. Call exchange_code() first."
            )
        if self._client_secret is None:
            raise ConfigurationError("client_secret is required")
        return token_request(
            {
                "grant_type": "refresh_token",
                "client_id": self._client_id,
                "client_secret": self._client_secret,
                "refresh_token": refresh_tok,
            },
            timeout=self._timeout,
            max_retries=self._max_retries,
            client=self._http_client,
            token_url=self._token_url,
        )


class AsyncWebAppAuth(AsyncBaseAuth):
    """Async Web App auth (authorization_code grant). Use with async Frame.io SDK."""

    def __init__(
        self,
        *,
        client_id: str,
        client_secret: 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 client_secret:
            raise ConfigurationError("client_secret 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 = client_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 = AsyncTokenManager(
            refresh_func=self._refresh_token,
            refresh_buffer=refresh_buffer,
            on_token_refreshed=on_token_refreshed,
        )

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

    async def exchange_code(self, code: str) -> dict[str, Any]:
        """Exchange an authorization code for access and refresh tokens."""
        if self._client_secret is None:
            raise ConfigurationError("client_secret is required")
        result = await token_request_async(
            {
                "grant_type": "authorization_code",
                "client_id": self._client_id,
                "client_secret": self._client_secret,
                "code": code,
                "redirect_uri": self._redirect_uri,
            },
            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."
            )
        if self._client_secret is None:
            raise ConfigurationError("client_secret is required")
        return await token_request_async(
            {
                "grant_type": "refresh_token",
                "client_id": self._client_id,
                "client_secret": self._client_secret,
                "refresh_token": refresh_tok,
            },
            timeout=self._timeout,
            max_retries=self._max_retries,
            client=self._http_client,
            token_url=self._token_url,
        )
