"""Configuration dataclasses and the OAuthStateStore protocol for the unified
user OAuth 2.1 authorization code flow.

Design notes
------------
- :class:`OAuthFlowConfig` intentionally has **no** ``client_secret`` field.
  The actual secret is held in a vault and referenced by a vault URI string
  (``client_secret_ref``).  This makes the config object safe to log, cache,
  and pass around freely — it contains no sensitive values.
- :class:`OAuthStateStore` is a :class:`typing.Protocol` so the SDK stays
  Redis-agnostic; each consuming service provides a thin Redis implementation.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Protocol, runtime_checkable


# ---------------------------------------------------------------------------
# EnrichmentStep
# ---------------------------------------------------------------------------


@dataclass
class EnrichmentStep:
    """A single post-token-exchange API call to fetch provider metadata.

    After the authorization code is exchanged for tokens, some providers
    require additional API calls to obtain data that is not included in the
    token response.  For example, Atlassian's token response contains an
    ``access_token`` but the ``cloud_id`` needed to construct API URLs requires
    a separate call to ``/oauth/token/accessible-resources``.

    Steps are executed sequentially by :class:`~empowernow_common.oauth.user_flow.enrichment.MetadataEnrichmentService`.

    Attributes:
        url:         API endpoint to call (absolute URL).
        method:      HTTP method — default ``"GET"``.
        auth:        Authentication to attach to the enrichment request.
                     ``"bearer"`` injects ``Authorization: Bearer <access_token>``;
                     ``"none"`` sends no Authorization header.
        inject_into: Key under which the JSON response is stored in
                     ``token_data`` — default ``"enrichment_data"``.
        required:    If ``True``, a failure raises
                     :class:`~empowernow_common.oauth.user_flow.exceptions.OAuthEnrichmentError`
                     and the OAuth flow is aborted.  If ``False`` (default),
                     the failure is logged as a warning and the flow continues.

    Example (Atlassian)::

        EnrichmentStep(
            url="https://api.atlassian.com/oauth/token/accessible-resources",
            inject_into="resources",
            required=False,
        )
    """

    url: str
    method: str = "GET"
    auth: str = "bearer"
    inject_into: str = "enrichment_data"
    required: bool = False


# ---------------------------------------------------------------------------
# OAuthFlowConfig
# ---------------------------------------------------------------------------


@dataclass
class OAuthFlowConfig:
    """Provider-agnostic OAuth 2.1 client configuration.

    Both the MCP Gateway and the CRUD Service convert their respective config
    sources (OpenBAO / YAML) into this unified dataclass before delegating to
    :class:`~empowernow_common.oauth.user_flow.orchestrator.UnifiedOAuthFlow`.

    Security
    --------
    This config contains **no secret values** — only a vault URI reference
    (``client_secret_ref``).  The reference is safe to log, cache, and
    serialize.  The actual secret is resolved just-in-time during token
    exchange via the SDK's existing
    :class:`~empowernow_common.vault.base.ReadableVaultProvider`.

    Attributes:
        client_id:            OAuth client identifier.
        client_secret_ref:    Vault URI pointing to the client secret, e.g.
                              ``"openbao+kv2://secret/oauth-clients/jira_cloud#client_secret"``.
                              **Never the secret value itself.**
        authorization_url:    Provider's authorization endpoint.
        token_url:            Provider's token endpoint.
        redirect_uri:         Callback URL.  Required; CRUD Service sets it
                              per-system from YAML, MCP Gateway falls back to
                              a global redirect URI.
        scopes:               OAuth scopes to request — default ``[]``.
        pkce_required:        Whether to include PKCE challenge params in the
                              authorization URL and code_verifier in the token
                              exchange — default ``True``.
        client_auth_method:   How to send client credentials to the token
                              endpoint: ``"client_secret_post"`` (default,
                              sends ``client_secret`` in the POST body) or
                              ``"client_secret_basic"`` (sends base64-encoded
                              ``client_id:secret`` in the Authorization header).
        audience:             Optional ``audience`` parameter for the
                              authorization URL (e.g. Auth0).
        provider_type:        Optional provider hint passed to
                              :class:`~empowernow_common.vault.user_oauth.metadata.OAuthMetadataExtractor`
                              for provider-specific metadata extraction.
                              Falls back to ``system_key`` when not set.
        metadata_enrichment:  Optional ordered list of
                              :class:`EnrichmentStep` API calls to execute
                              after token exchange.
        static_metadata:      Optional non-dynamic metadata merged into the
                              extracted metadata dict (e.g.
                              ``{"base_url": "https://github.example.com"}``
                              for GitHub Enterprise).
        extra_auth_params:    Optional provider-specific parameters appended
                              to the authorization URL query string (e.g.
                              ``{"prompt": "consent"}``).  Replaces hardcoded
                              ``if/elif`` provider logic in the services.
    """

    # Required fields
    client_id: str
    client_secret_ref: str  # vault URI — NOT the secret value
    authorization_url: str
    token_url: str
    redirect_uri: str

    # Optional fields with sensible defaults
    scopes: list[str] = field(default_factory=list)
    pkce_required: bool = True
    client_auth_method: str = "client_secret_post"
    audience: str | None = None
    provider_type: str | None = None
    metadata_enrichment: list[EnrichmentStep] | None = None
    static_metadata: dict[str, Any] | None = None
    extra_auth_params: dict[str, str] | None = None


# ---------------------------------------------------------------------------
# OAuthStateStore — Protocol (Redis-agnostic)
# ---------------------------------------------------------------------------


@runtime_checkable
class OAuthStateStore(Protocol):
    """Interface for OAuth state persistence.

    In production both the MCP Gateway and the CRUD Service provide a thin
    Redis implementation of this protocol.  The SDK itself is Redis-agnostic.

    Implementation requirements
    ---------------------------
    - ``consume_state`` **must** atomically retrieve *and* delete the key to
      prevent replay attacks.  Use a Redis pipeline or Lua script.
    - ``store_state`` must set a TTL so state expires even if never consumed.
    - ``check_rate_limit`` must atomically increment the counter and compare it
      with *max_count* within *window_seconds*.
    """

    async def store_state(
        self, key: str, data: dict[str, Any], ttl_seconds: int
    ) -> None:
        """Persist *data* under *key* with a TTL.

        Args:
            key:         Storage key (e.g. ``"oauth_state:<token>"``).
            data:        Arbitrary JSON-serialisable state dict.
            ttl_seconds: Time-to-live in seconds.
        """
        ...

    async def consume_state(self, key: str) -> dict[str, Any] | None:
        """Atomically retrieve and delete state (one-time use).

        Args:
            key: Storage key to consume.

        Returns:
            The stored data dict, or ``None`` if the key does not exist /
            has already been consumed.
        """
        ...

    async def check_rate_limit(
        self, key: str, max_count: int, window_seconds: int
    ) -> bool:
        """Increment a counter and return whether the caller is within the limit.

        Args:
            key:            Rate-limit counter key (e.g. ``"oauth_rate:<arn>"``).
            max_count:      Maximum allowed increments within the window.
            window_seconds: Sliding window duration in seconds.

        Returns:
            ``True`` if the current count is within *max_count*, ``False`` if
            the limit has been exceeded.
        """
        ...
