"""
Extension models for connector configuration.

Provides Pydantic models for OpenAPI x-airbyte-* extensions:
- RetryConfig: retry strategy with exponential backoff
- CacheConfig / CacheEntityConfig / CacheFieldConfig: cache mapping for api_search
- ReplicationConfig: replication settings for MULTI mode connectors
- EntityRelationshipConfig: entity relationship declarations
- ScopingParamConfig: scoping parameter resolution from config
"""

from typing import Literal

from pydantic import BaseModel, ConfigDict, Field, model_validator


class RetryConfig(BaseModel):
    """
    Configuration for retry strategy with exponential backoff.

    Used to configure automatic retries for transient errors (429, 5xx, timeouts, network errors).
    Can be specified at the connector level via x-airbyte-retry-config in the OpenAPI spec's info section.

    By default, retries are enabled with max_attempts=3. To disable retries, set max_attempts=1
    in your connector's x-airbyte-retry-config.

    Example YAML usage:
        info:
          title: My API
          x-airbyte-retry-config:
            max_attempts: 5
            initial_delay_seconds: 2.0
            retry_after_header: "X-RateLimit-Reset"
            retry_after_format: "unix_timestamp"
    """

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

    # Core retry settings (max_attempts=3 enables retries by default)
    max_attempts: int = 3
    initial_delay_seconds: float = 1.0
    max_delay_seconds: float = 60.0
    exponential_base: float = 2.0
    jitter: bool = True

    # Which errors to retry
    retry_on_status_codes: list[int] = [429, 500, 502, 503, 504]
    retry_on_timeout: bool = True
    retry_on_network_error: bool = True

    # Header-based delay extraction
    retry_after_header: str = "Retry-After"
    retry_after_format: Literal["seconds", "milliseconds", "unix_timestamp"] = "seconds"


class CacheFieldProperty(BaseModel):
    """
    Nested property definition for object-type cache fields.

    Supports recursive nesting to represent complex nested schemas in cache field definitions.
    Used when a cache field has type 'object' and needs to define its internal structure.

    Example YAML usage:
        - name: collaboration
          type: ['null', 'object']
          description: "Collaboration data"
          properties:
            brief:
              type: ['null', 'string']
            comments:
              type: ['null', 'array']
    """

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

    type: str | list[str]
    properties: dict[str, "CacheFieldProperty"] | None = None


class CacheFieldConfig(BaseModel):
    """
    Field configuration for cache mapping.

    Defines a single field in a cache entity, with optional name aliasing
    to map between user-facing field names and cache storage names.

    For object-type fields, supports nested properties to define the internal structure
    of complex nested schemas.

    Used in x-airbyte-cache extension for api_search operations.
    """

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

    name: str
    x_airbyte_name: str | None = Field(default=None, alias="x-airbyte-name")
    type: str | list[str]
    description: str
    properties: dict[str, CacheFieldProperty] | None = None

    @property
    def cache_name(self) -> str:
        """Return cache name, falling back to name if alias not specified."""
        return self.x_airbyte_name or self.name


class CacheEntityConfig(BaseModel):
    """
    Entity configuration for cache mapping.

    Defines a cache-enabled entity with its fields and optional name aliasing
    to map between user-facing entity names and cache storage names.

    Used in x-airbyte-cache extension for api_search operations.
    """

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

    entity: str
    x_airbyte_name: str | None = Field(default=None, alias="x-airbyte-name")
    fields: list[CacheFieldConfig]

    @property
    def cache_name(self) -> str:
        """Return cache entity name, falling back to entity if alias not specified."""
        return self.x_airbyte_name or self.entity


class ReplicationConfigPropertyItems(BaseModel):
    """
    Items definition for array-type replication configuration fields.

    Defines the schema for items in an array-type replication config property.
    """

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

    type: str


class ReplicationConfigProperty(BaseModel):
    """
    Property definition for replication configuration fields.

    Defines a single field in the replication configuration with its type,
    description, and optional default value. Supports both simple types
    (string, integer, boolean) and array types.

    Example YAML usage:
        x-airbyte-replication-config:
          properties:
            start_date:
              type: string
              title: Start Date
              description: UTC date and time from which to replicate data
              format: date-time
            account_ids:
              type: array
              title: Account IDs
              description: List of account IDs to replicate
              items:
                type: string
    """

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

    type: str
    title: str | None = None
    description: str | None = None
    format: str | None = None
    default: str | int | float | bool | list | None = None
    enum: list[str] | None = None
    items: ReplicationConfigPropertyItems | None = None


class ReplicationConfig(BaseModel):
    """
    Replication configuration extension (x-airbyte-replication-config).

    Defines replication-specific settings for MULTI mode connectors that need
    to configure the underlying replication connector. This allows users who
    use the direct-style API (credentials + environment) to also specify
    replication settings like start_date, lookback_window, etc.

    This extension is added to the Info model and provides field definitions
    for replication configuration that gets merged into the source config
    when creating sources.

    Example YAML usage:
        info:
          title: HubSpot API
          x-airbyte-replication-config:
            title: Replication Configuration
            description: Settings for data replication
            properties:
              start_date:
                type: string
                title: Start Date
                description: UTC date and time from which to replicate data
                format: date-time
            required:
              - start_date
            replication_config_key_mapping:
              start_date: start_date
    """

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

    title: str | None = None
    description: str | None = None
    properties: dict[str, ReplicationConfigProperty] = Field(default_factory=dict)
    required: list[str] = Field(default_factory=list)
    replication_config_key_mapping: dict[str, str] = Field(
        default_factory=dict,
        alias="replication_config_key_mapping",
        description="Mapping from replication_config field names to source_config field names",
    )

    @model_validator(mode="after")
    def validate_replication_config_key_mapping(self) -> "ReplicationConfig":
        """Validate that replication_config_key_mapping keys exist in properties.

        The mapping is: {local_key: airbyte_path}
        We validate that local_key exists in our properties.
        """
        if self.replication_config_key_mapping and self.properties:
            property_names = set(self.properties.keys())
            for local_key, airbyte_path in self.replication_config_key_mapping.items():
                if local_key not in property_names:
                    available = ", ".join(sorted(property_names)) if property_names else "(none)"
                    raise ValueError(
                        f"replication_config_key_mapping: local key '{local_key}' "
                        f"(mapped to '{airbyte_path}') not found in properties. Available: {available}"
                    )
        return self


class CacheConfig(BaseModel):
    """
    Cache configuration extension (x-airbyte-cache).

    Defines cache-enabled entities and their field mappings for api_search operations.
    Supports optional name aliasing via x-airbyte-name for both entities and fields,
    enabling bidirectional mapping between user-facing names and cache storage names.

    This extension is added to the Info model and provides field-level mapping for
    search operations that use cached data.

    Example YAML usage:
        info:
          title: Stripe API
          x-airbyte-cache:
            entities:
              - entity: customers
                stream: customers
                fields:
                  - name: email
                    type: ["null", "string"]
                    description: "Customer email address"
                  - name: customer_name
                    x-airbyte-name: name
                    type: ["null", "string"]
                    description: "Customer full name"
    """

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

    entities: list[CacheEntityConfig]
    disable_compaction: bool = Field(
        default=False,
        alias="disable_compaction",
        description="When true, Athena compaction (OPTIMIZE + VACUUM) is skipped for this connector type.",
    )

    def get_entity_mapping(self, user_entity: str) -> CacheEntityConfig | None:
        """
        Get entity config by user-facing name.

        Args:
            user_entity: User-facing entity name to look up

        Returns:
            CacheEntityConfig if found, None otherwise
        """
        for entity in self.entities:
            if entity.entity == user_entity:
                return entity
        return None


class EntityRelationshipConfig(BaseModel):
    """
    Entity relationship declaration for cross-entity navigation.

    Defines a foreign-key relationship between two entities, enabling
    the runtime to resolve parent-child dependencies and provide
    relationship metadata to agents.

    Used in x-airbyte-entity-relationships extension in the Info object.

    Example YAML usage:
        info:
          title: My API
          x-airbyte-entity-relationships:
            - source_entity: contacts
              target_entity: accounts
              foreign_key: account_id
              cardinality: many_to_one
              description: "Contact belongs to an account"
    """

    model_config = ConfigDict(extra="forbid")

    source_entity: str = Field(description="Entity that holds the foreign key")
    target_entity: str = Field(description="Entity being referenced")
    foreign_key: str = Field(description="Field on source_entity that references target_entity")
    target_key: str = Field(default="id", description="Field on target_entity being referenced")
    cardinality: Literal["one_to_one", "one_to_many", "many_to_one", "many_to_many"] | None = Field(
        None, description="Optional relationship cardinality"
    )
    description: str | None = Field(
        None, description="Human-readable description of the relationship"
    )

    def format_line(self) -> str:
        """Format as a human-readable line for tool descriptions."""
        line = f"{self.source_entity} -> {self.target_entity} (via {self.foreign_key}"
        if self.cardinality:
            line += f", {self.cardinality.replace('_', '-')}"
        line += ")"
        if self.description:
            line += f" -- {self.description}"
        return line


class ScopingParamConfig(BaseModel):
    """
    Scoping parameter resolution from connector configuration.

    Declares a path parameter that should be resolved from the connector's
    config values at runtime, rather than being supplied per-request.

    Used in x-airbyte-scoping extension in the Info object.

    Example YAML usage:
        info:
          title: My API
          x-airbyte-scoping:
            - param: account_id
              config_key: account_id
    """

    model_config = ConfigDict(extra="forbid")

    param: str = Field(description="Path parameter name to resolve from config")
    config_key: str | None = Field(
        None, description="Config key to read. Defaults to param name if omitted."
    )
