"""Server-to-Server authentication (client_credentials grant).

Use this for backend services and scripts that need Frame.io access
without user interaction.
"""

from __future__ import annotations

from typing import Any, Callable, Optional

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_TIMEOUT,
    S2S_SCOPES,
    _build_urls,
    token_request,
    token_request_async,
)
from ._token_manager import DEFAULT_REFRESH_BUFFER_SECONDS, TokenManager
from ._token_manager_async import AsyncTokenManager


class ServerToServerAuth(BaseAuth):
    """Authenticate using the OAuth 2.0 client_credentials grant.

    This flow does **not** involve a user and does **not** return a refresh
    token.  When the access token expires the package simply requests a new
    one using the client credentials.

    Example::

        from frameio import Frameio
        from frameio.auth import ServerToServerAuth

        auth = ServerToServerAuth(client_id="...", client_secret="...")
        client = Frameio(token=auth.get_token)

    Args:
        client_id: Adobe IMS OAuth client ID.
        client_secret: Adobe IMS OAuth client secret.
        scopes: Space-separated scopes (OAuth 2.0 RFC 6749). Defaults to ``openid AdobeID frame.s2s.all``.
        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 fetch.
        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,
        scopes: str = S2S_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")

        self._client_id = client_id
        self._client_secret = client_secret
        self._scopes = scopes
        self._timeout = timeout
        self._max_retries = max_retries
        self._http_client = http_client
        _, self._token_url, self._revoke_url = _build_urls(ims_base_url)

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

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

    def authenticate(self) -> dict[str, Any]:
        """Explicitly fetch a new access token.

        Returns:
            Token response dict with ``access_token``, ``expires_in``, etc.
        """
        result = self._fetch_token()
        self._store_tokens(result)
        return result

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

    def _fetch_token(self) -> dict[str, Any]:
        """Request a new token using client_credentials."""
        if self._client_secret is None:
            raise ConfigurationError("client_secret is required")
        return token_request(
            {
                "grant_type": "client_credentials",
                "client_id": self._client_id,
                "client_secret": self._client_secret,
                "scope": self._scopes,
            },
            timeout=self._timeout,
            max_retries=self._max_retries,
            client=self._http_client,
            token_url=self._token_url,
        )


class AsyncServerToServerAuth(AsyncBaseAuth):
    """Async Server-to-Server auth (client_credentials). Use with async Frame.io SDK."""

    def __init__(
        self,
        *,
        client_id: str,
        client_secret: str,
        scopes: str = S2S_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")

        self._client_id = client_id
        self._client_secret = client_secret
        self._scopes = scopes
        self._timeout = timeout
        self._max_retries = max_retries
        self._http_client = http_client
        _, self._token_url, self._revoke_url = _build_urls(ims_base_url)

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

    async def authenticate(self) -> dict[str, Any]:
        """Explicitly fetch a new access token."""
        result = await self._fetch_token()
        self._store_tokens(result)
        return result

    async def _fetch_token(self) -> dict[str, Any]:
        if self._client_secret is None:
            raise ConfigurationError("client_secret is required")
        return await token_request_async(
            {
                "grant_type": "client_credentials",
                "client_id": self._client_id,
                "client_secret": self._client_secret,
                "scope": self._scopes,
            },
            timeout=self._timeout,
            max_retries=self._max_retries,
            client=self._http_client,
            token_url=self._token_url,
        )
