"""
Security scheme models for OpenAPI 3.1.

References:
- https://spec.openapis.org/oas/v3.1.0#security-scheme-object
- https://spec.openapis.org/oas/v3.1.0#oauth-flows-object
"""

import re
from typing import Any, Dict, List, Literal, Set

from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator


class OAuth2Flow(BaseModel):
    """
    OAuth 2.0 flow configuration.

    OpenAPI Reference: https://spec.openapis.org/oas/v3.1.0#oauth-flow-object
    """

    model_config = ConfigDict(populate_by_name=True, extra="forbid")

    authorization_url: str | None = Field(None, alias="authorizationUrl")
    token_url: str | None = Field(None, alias="tokenUrl")
    refresh_url: str | None = Field(None, alias="refreshUrl")
    scopes: Dict[str, str] = Field(default_factory=dict)


class OAuth2Flows(BaseModel):
    """
    Collection of OAuth 2.0 flows.

    OpenAPI Reference: https://spec.openapis.org/oas/v3.1.0#oauth-flows-object
    """

    model_config = ConfigDict(populate_by_name=True, extra="forbid")

    implicit: OAuth2Flow | None = None
    password: OAuth2Flow | None = None
    client_credentials: OAuth2Flow | None = Field(None, alias="clientCredentials")
    authorization_code: OAuth2Flow | None = Field(None, alias="authorizationCode")


class AuthConfigFieldSpec(BaseModel):
    """
    Specification for a user-facing authentication config field.

    This defines a single input field that users provide for authentication.
    """

    model_config = ConfigDict(populate_by_name=True, extra="forbid")

    type: Literal["string", "integer", "boolean", "number"] = "string"
    title: str | None = None
    description: str | None = None
    format: str | None = None  # e.g., "email", "uri"
    pattern: str | None = None  # Regex validation
    default: Any | None = None


class AirbyteAuthConfig(BaseModel):
    """
    Airbyte auth configuration extension (x-airbyte-auth-config).

    Defines user-facing authentication configuration and how it maps to
    the underlying OpenAPI security scheme.

    For multiple authentication methods, define separate security schemes
    rather than using oneOf within a single scheme.
    """

    model_config = ConfigDict(populate_by_name=True, extra="forbid")

    title: str | None = None
    description: str | None = None
    type: Literal["object"] | None = None
    required: List[str] | None = None
    properties: Dict[str, AuthConfigFieldSpec] | None = None
    auth_mapping: Dict[str, str] | None = None

    # Replication connector auth mapping
    replication_auth_key_mapping: Dict[str, str] | None = Field(
        None,
        description="Mapping from source config paths (e.g., 'credentials.api_key') to auth config keys for direct connectors",
    )

    # Additional headers to inject alongside OAuth2 Bearer token
    additional_headers: Dict[str, str] | None = Field(
        None,
        description=(
            "Extra headers to inject with auth. Values support Jinja2 {{ variable }} template syntax "
            "to reference secrets. Example: {'Amazon-Advertising-API-ClientId': '{{ client_id }}'}"
        ),
    )

    # Replication connector auth constants
    replication_auth_key_constants: Dict[str, Any] | None = Field(
        None,
        description="Constant values to always inject at source config paths (e.g., 'credentials.auth_type': 'OAuth2.0')",
    )

    @model_validator(mode="after")
    def validate_config_structure(self) -> "AirbyteAuthConfig":
        """Validate that required fields are provided for auth config."""
        has_config = self.type is not None or self.properties is not None or self.auth_mapping is not None

        if not has_config:
            raise ValueError("Auth config must have type, properties, and auth_mapping")

        # Validate required fields
        if self.type != "object":
            raise ValueError("Auth config must have type='object'")
        if not self.properties:
            raise ValueError("Auth config must have properties")
        if not self.auth_mapping:
            raise ValueError("Auth config must have auth_mapping")

        # Validate replication_auth_key_mapping targets exist in properties
        if self.replication_auth_key_mapping and self.properties:
            self._validate_replication_auth_key_mapping(self.replication_auth_key_mapping, self.properties, context="x-airbyte-auth-config")

        # Validate all properties are used in auth_mapping or additional_headers
        if self.properties and self.auth_mapping:
            self._validate_properties_used_in_auth_mapping(
                self.properties,
                self.auth_mapping,
                context="x-airbyte-auth-config",
                additional_headers=self.additional_headers,
            )

        return self

    @staticmethod
    def _validate_replication_auth_key_mapping(mapping: Dict[str, str], properties: Dict[str, AuthConfigFieldSpec], context: str) -> None:
        """Validate that replication_auth_key_mapping target keys exist in properties.

        Args:
            mapping: The replication_auth_key_mapping dict (airbyte_path -> our_key)
            properties: The properties dict from x-airbyte-auth-config
            context: Context string for error messages
        """
        property_names = set(properties.keys())
        for airbyte_path, our_key in mapping.items():
            if our_key not in property_names:
                available = ", ".join(sorted(property_names)) if property_names else "(none)"
                raise ValueError(
                    f"replication_auth_key_mapping in {context}: target key '{our_key}' "
                    f"(mapped from '{airbyte_path}') not found in properties. Available: {available}"
                )

    @staticmethod
    def _validate_properties_used_in_auth_mapping(
        properties: Dict[str, AuthConfigFieldSpec],
        auth_mapping: Dict[str, str],
        context: str,
        additional_headers: Dict[str, str] | None = None,
    ) -> None:
        """Validate that all properties are referenced in auth_mapping or additional_headers.

        Auth mapping values use ${field_name} syntax to reference properties.
        Additional headers values use {{ field_name }} Jinja2 syntax.
        All defined properties must be used somewhere in auth_mapping or additional_headers.

        Args:
            properties: The properties dict from x-airbyte-auth-config
            auth_mapping: The auth_mapping dict (param_name -> template string)
            context: Context string for error messages
            additional_headers: Optional additional_headers dict with Jinja2 templates
        """
        # Extract all ${field_name} references from auth_mapping values
        referenced_fields: Set[str] = set()
        for template_value in auth_mapping.values():
            matches = re.findall(r"\$\{(\w+)\}", template_value)
            referenced_fields.update(matches)

        # Extract {{ field_name }} references from additional_headers values
        if additional_headers:
            for template_value in additional_headers.values():
                matches = re.findall(r"\{\{\s*(\w+)\s*\}\}", template_value)
                referenced_fields.update(matches)

        property_names: Set[str] = set(properties.keys())
        unused_properties = property_names - referenced_fields

        if unused_properties:
            raise ValueError(
                f"Properties in {context} not used in auth_mapping or additional_headers: {', '.join(sorted(unused_properties))}. "
                f"All properties must be referenced in auth_mapping using ${{field_name}} syntax "
                f"or in additional_headers using {{{{ field_name }}}} syntax."
            )


class AirbyteOAuthCredentials(BaseModel):
    """
    Airbyte OAuth credentials extension (x-airbyte-oauth-credentials).

    Describes what OAuth app credentials an organization needs to provide
    to override the default Airbyte-managed OAuth app credentials.
    """

    model_config = ConfigDict(populate_by_name=True, extra="forbid")

    title: str | None = None
    description: str | None = None
    properties: Dict[str, AuthConfigFieldSpec] | None = None
    required: List[str] | None = None


class SecurityScheme(BaseModel):
    """
    Security scheme definition.

    OpenAPI Reference: https://spec.openapis.org/oas/v3.1.0#security-scheme-object

    Supported Types:
    - apiKey: API key in header/query/cookie
    - http: HTTP authentication (basic, bearer, digest, etc.)
    - oauth2: OAuth 2.0 flows

    Extensions:
    - x-airbyte-token-path: JSON path to extract token from auth response (Airbyte extension)
    - x-airbyte-token-refresh: OAuth2 token refresh configuration (dict with auth_style, body_format)
    - x-airbyte-auth-config: User-facing authentication configuration (Airbyte extension)
    - x-airbyte-oauth-credentials: OAuth app credential override specification (Airbyte extension)

    Future extensions (not yet active):
    - x-grant-type: OAuth grant type for refresh tokens
    - x-refresh-endpoint: Custom refresh endpoint URL
    """

    model_config = ConfigDict(populate_by_name=True, extra="allow")

    # Standard OpenAPI fields
    type: Literal["apiKey", "http", "oauth2", "openIdConnect"]
    description: str | None = None

    # apiKey specific
    name: str | None = None
    in_: Literal["query", "header", "cookie"] | None = Field(None, alias="in")

    # http specific
    scheme: str | None = None  # e.g., "basic", "bearer", "digest"
    bearer_format: str | None = Field(None, alias="bearerFormat")

    # oauth2 specific
    flows: OAuth2Flows | None = None

    # openIdConnect specific
    open_id_connect_url: str | None = Field(None, alias="openIdConnectUrl")

    # Airbyte extensions
    x_token_path: str | None = Field(None, alias="x-airbyte-token-path")
    x_token_refresh: Dict[str, Any] | None = Field(None, alias="x-airbyte-token-refresh")
    x_airbyte_auth_config: AirbyteAuthConfig | None = Field(None, alias="x-airbyte-auth-config")
    x_airbyte_token_extract: List[str] | None = Field(
        None,
        alias="x-airbyte-token-extract",
        description="List of fields to extract from OAuth2 token responses and use as server variables",
    )
    x_airbyte_oauth_credentials: AirbyteOAuthCredentials | None = Field(
        None,
        alias="x-airbyte-oauth-credentials",
        description="Describes what OAuth app credentials an organization needs to provide for credential override",
    )
    x_airbyte_untested: bool = Field(
        False,
        alias="x-airbyte-untested",
        description="Mark this auth scheme as untested to skip cassette coverage validation",
    )

    @field_validator("x_airbyte_token_extract", mode="after")
    @classmethod
    def validate_token_extract(cls, v: List[str] | None) -> List[str] | None:
        """Validate x-airbyte-token-extract has no duplicates."""
        if v is not None:
            if len(v) != len(set(v)):
                duplicates = [x for x in v if v.count(x) > 1]
                raise ValueError(f"x-airbyte-token-extract contains duplicate fields: {set(duplicates)}")
        return v

    # Future extensions (commented out, defined for future use)
    # x_grant_type: Optional[Literal["refresh_token", "client_credentials"]] = Field(None, alias="x-grant-type")
    # x_refresh_endpoint: Optional[str] = Field(None, alias="x-refresh-endpoint")

    @model_validator(mode="after")
    def validate_security_scheme(self) -> "SecurityScheme":
        """Validate that required fields are present based on security type."""
        if self.type == "apiKey":
            if not self.name or not self.in_:
                raise ValueError("apiKey type requires 'name' and 'in' fields")
        elif self.type == "http":
            if not self.scheme:
                raise ValueError("http type requires 'scheme' field")
        elif self.type == "oauth2":
            if not self.flows:
                raise ValueError("oauth2 type requires 'flows' field")
        elif self.type == "openIdConnect":
            if not self.open_id_connect_url:
                raise ValueError("openIdConnect type requires 'openIdConnectUrl' field")
        return self


# SecurityRequirement is a dict mapping security scheme name to list of scopes
SecurityRequirement = Dict[str, List[str]]
