"""Pydantic v2 configuration models."""

from __future__ import annotations

import json
import os
import re
from enum import StrEnum
from pathlib import Path
from typing import Any

from pydantic import BaseModel, Field, model_validator


class ClientType(StrEnum):
    OPENAI = "openai"
    GEMINI = "gemini"
    ANTHROPIC = "anthropic"


class ResetSchedule(StrEnum):
    DAILY_UTC = "daily_utc"
    ROLLING_WINDOW = "rolling_window"


class TokenQuotaConfig(BaseModel):
    limit: int = Field(gt=0)
    reset: ResetSchedule
    window_seconds: int | None = None

    @model_validator(mode="after")
    def _validate_rolling_window(self) -> TokenQuotaConfig:
        if self.reset == ResetSchedule.ROLLING_WINDOW and self.window_seconds is None:
            raise ValueError("window_seconds is required when reset is 'rolling_window'")
        return self


class RequestQuotaConfig(BaseModel):
    limit: int = Field(gt=0)
    reset: ResetSchedule
    window_seconds: int | None = None

    @model_validator(mode="after")
    def _validate_rolling_window(self) -> RequestQuotaConfig:
        if self.reset == ResetSchedule.ROLLING_WINDOW and self.window_seconds is None:
            raise ValueError("window_seconds is required when reset is 'rolling_window'")
        return self


class KeyConfig(BaseModel):
    token: str
    alias: str
    request_quota: RequestQuotaConfig | None = None


class ModelGroupConfig(BaseModel):
    name: str
    tier: int = Field(ge=1)
    models: list[str] = Field(min_length=1)
    token_quota: TokenQuotaConfig | None = None


class ServerErrorRetryConfig(BaseModel):
    max_attempts: int = Field(default=1, ge=0)
    delay_seconds: float = Field(default=0.5, ge=0.0)


class ProviderConfig(BaseModel):
    name: str
    client_type: ClientType
    priority: int = Field(ge=1)
    models: list[str] | None = None
    model_groups: list[ModelGroupConfig] | None = None
    keys: list[KeyConfig] = Field(min_length=1)
    server_error_retry: ServerErrorRetryConfig = Field(default_factory=ServerErrorRetryConfig)
    base_url: str | None = None

    @model_validator(mode="after")
    def _validate_models_xor_groups(self) -> ProviderConfig:
        has_models = self.models is not None
        has_groups = self.model_groups is not None
        if has_models and has_groups:
            raise ValueError("Specify either models or model_groups, not both")
        if not has_models and not has_groups:
            raise ValueError("Must specify either models or model_groups")
        return self

    @model_validator(mode="after")
    def _set_default_base_url(self) -> ProviderConfig:
        if self.base_url is None and self.client_type == ClientType.OPENAI:
            self.base_url = "https://api.openai.com/v1"
        return self


class RotatorConfig(BaseModel):
    providers: list[ProviderConfig] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_unique_priorities(self) -> RotatorConfig:
        priorities = [p.priority for p in self.providers]
        if len(priorities) != len(set(priorities)):
            raise ValueError("Duplicate provider priority values are not allowed")
        return self

    @model_validator(mode="after")
    def _sort_by_priority(self) -> RotatorConfig:
        self.providers = sorted(self.providers, key=lambda p: p.priority)
        return self

    @classmethod
    def from_json(cls, path: str | Path) -> RotatorConfig:
        """Load configuration from a JSON file with env variable substitution.

        Supports env variable patterns in string values:
        - ``$VAR`` or ``${VAR}`` — substitutes from os.environ, raises ValueError if missing
        - ``${VAR:-default}`` — substitutes from os.environ, uses default if missing
        """
        file_path = Path(path)
        if not file_path.exists():
            raise FileNotFoundError(f"Config file not found: {file_path}")

        text = file_path.read_text(encoding="utf-8")
        try:
            data = json.loads(text)
        except json.JSONDecodeError as e:
            raise ValueError(f"Invalid JSON in {file_path}: {e}") from e

        _substitute_env_vars(data)
        return cls.model_validate(data)


# --- Env variable substitution ---

_ENV_PATTERN = re.compile(
    r"\$\{([^}:]+):-([^}]*)\}"  # ${VAR:-default}
    r"|\$\{([^}]+)\}"           # ${VAR}
    r"|\$([A-Za-z_][A-Za-z0-9_]*)"  # $VAR
)


def _resolve_env_match(match: re.Match) -> str:
    """Resolve a single env variable match."""
    # ${VAR:-default}
    if match.group(1) is not None:
        var_name = match.group(1)
        default = match.group(2)
        return os.environ.get(var_name, default)

    # ${VAR}
    if match.group(3) is not None:
        var_name = match.group(3)
        value = os.environ.get(var_name)
        if value is None:
            raise ValueError(
                f"Environment variable '{var_name}' is not set "
                f"(referenced as '${{{var_name}}}')"
            )
        return value

    # $VAR
    var_name = match.group(4)
    value = os.environ.get(var_name)
    if value is None:
        raise ValueError(
            f"Environment variable '{var_name}' is not set "
            f"(referenced as '${var_name}')"
        )
    return value


def _substitute_env_vars(data: Any) -> None:
    """Recursively substitute env variables in all string values."""
    if isinstance(data, dict):
        for key, value in data.items():
            if isinstance(value, str) and "$" in value:
                data[key] = _ENV_PATTERN.sub(_resolve_env_match, value)
            elif isinstance(value, (dict, list)):
                _substitute_env_vars(value)
    elif isinstance(data, list):
        for i, item in enumerate(data):
            if isinstance(item, str) and "$" in item:
                data[i] = _ENV_PATTERN.sub(_resolve_env_match, item)
            elif isinstance(item, (dict, list)):
                _substitute_env_vars(item)
