"""Configuration settings for TipSharks system.

Loads configuration from environment variables using Pydantic settings.
All settings have sensible defaults and can be overridden via .env file.
"""

from typing import Literal

from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict


class TABSettings(BaseSettings):
    """TAB Affiliates API configuration.

    The TAB API is a public API - no authentication required.
    """

    base_url: str = Field(
        default="https://api.tab.co.nz/affiliates/v1",
        description="TAB Affiliates API base URL",
    )
    timeout: float = Field(default=30.0, description="Request timeout in seconds")
    max_retries: int = Field(default=3, description="Maximum retry attempts")
    mock_mode: bool = Field(
        default=False,
        description="Enable mock mode to use sample data instead of real API (for testing)",
    )
    default_category: str = Field(
        default="H",
        description="Default racing category: H (Harness), T (Thoroughbred), G (Greyhound)",
    )
    default_country: str = Field(
        default="NZ",
        description="Default country filter for API queries",
    )

    model_config = SettingsConfigDict(env_prefix="TAB_")


class DatabaseSettings(BaseSettings):
    """Database configuration."""

    url: str = Field(..., description="Database connection URL")
    pool_size: int = Field(default=5, description="Connection pool size")
    max_overflow: int = Field(default=10, description="Max overflow connections")
    pool_recycle: int = Field(
        default=3600, description="Connection recycle time in seconds"
    )
    pool_pre_ping: bool = Field(default=True, description="Enable pool pre-ping")
    echo: bool = Field(default=False, description="Echo SQL statements (debug)")
    slow_query_threshold_ms: int = Field(
        default=100,
        description="Queries exceeding this duration (ms) are logged as slow queries",
    )

    model_config = SettingsConfigDict(env_prefix="DATABASE_")


class LoggingSettings(BaseSettings):
    """Logging configuration."""

    level: str = Field(default="INFO", description="Log level")
    format: str = Field(default="json", description="Log format (json or text)")

    model_config = SettingsConfigDict(env_prefix="LOG_")


class APISettings(BaseSettings):
    """API server configuration."""

    host: str = Field(default="0.0.0.0", description="API host")
    port: int = Field(default=8000, description="API port")
    admin_token: str = Field(
        default="change_me_in_production",
        description="Admin endpoint authentication token",
    )
    cors_allow_origins: str = Field(
        default="*",
        description="CORS allowed origins (comma-separated or * for all). Use specific origins in production.",
    )

    model_config = SettingsConfigDict(env_prefix="API_")


HRNZ_ALL_CLUB_CODES = [
    "02",
    "04",
    "07",
    "09",
    "15",
    "16",
    "17",
    "18",
    "19",
    "20",
    "21",
    "23",
    "24",
    "25",
    "26",
    "27",
    "28",
    "30",
    "31",
    "32",
    "35",
    "36",
    "37",
    "38",
    "39",
    "40",
    "41",
    "42",
    "43",
    "44",
    "45",
    "46",
    "47",
    "48",
    "49",
    "50",
    "51",
    "52",
    "53",
    "54",
    "55",
    "56",
    "57",
    "63",
    "64",
    "66",
    "68",
    "69",
    "70",
    "73",
    "79",
    "80",
    "81",
    "82",
    "83",
    "84",
    "85",
    "86",
    "87",
    "88",
    "89",
    "90",
    "91",
    "92",
    "94",
    "95",
    "96",
    "97",
]


class HRNZSettings(BaseSettings):
    """HRNZ scraper configuration."""

    club_codes: str = Field(
        default="all",
        description="Comma-separated HRNZ club codes (two digits each) or 'all'",
    )
    decodo_proxy_server: str | None = Field(
        default=None,
        description="Decodo proxy server URL, e.g. http://host:port",
    )
    decodo_proxy_host: str | None = Field(
        default=None,
        description="Decodo proxy host (used when proxy server URL is not set)",
    )
    decodo_proxy_port: int | None = Field(
        default=None,
        description="Decodo proxy port (used when proxy server URL is not set)",
    )
    decodo_proxy_scheme: str = Field(
        default="http",
        description="Decodo proxy scheme (http or https)",
    )
    decodo_proxy_username: str | None = Field(
        default=None,
        description="Decodo proxy username (base credentials before session rotation)",
    )
    decodo_proxy_password: str | None = Field(
        default=None,
        description="Decodo proxy password",
    )
    decodo_rotate_each_request: bool = Field(
        default=True,
        description="Rotate Decodo session per request",
    )
    decodo_session_param: str = Field(
        default="session",
        description="Decodo session parameter name appended to username",
    )
    decodo_session_id: str | None = Field(
        default=None,
        description="Fixed Decodo session ID when rotation is disabled",
    )
    decodo_username_template: str | None = Field(
        default=None,
        description="Username template using {username} and {session} placeholders",
    )

    model_config = SettingsConfigDict(env_prefix="HRNZ_")


class RatingSettings(BaseSettings):
    """Rating engine configuration."""

    # Core Elo parameters
    elo_scale_c: float = Field(
        default=400.0,
        description="Logistic scale factor for Elo calculations",
    )
    elo_k_base: float = Field(
        default=24.0,
        description="Base K-factor for rating updates",
    )
    elo_k_min: float | None = Field(
        default=None,
        description="Optional minimum clamp for effective K-factor",
    )
    elo_k_max: float | None = Field(
        default=None,
        description="Optional maximum clamp for effective K-factor",
    )
    pairwise_normalizer: Literal["n_minus_1", "n", "comparisons"] = Field(
        default="n_minus_1",
        description="Normalization mode for pairwise sum (n-1, n, or actual comparisons)",
    )
    initial_rating: float = Field(
        default=1500.0,
        description="Starting rating for new entities",
    )
    rating_min: float | None = Field(
        default=None,
        description="Optional minimum clamp for ratings",
    )
    rating_max: float | None = Field(
        default=None,
        description="Optional maximum clamp for ratings",
    )
    initial_rd: float = Field(
        default=350.0,
        description="Initial rating deviation (uncertainty)",
    )

    # Rating deviation parameters
    rd_min: float = Field(
        default=50.0,
        description="Minimum rating deviation (fully established)",
    )
    rd_max: float = Field(
        default=350.0,
        description="Maximum rating deviation (completely uncertain)",
    )
    rd_decay_per_race: float = Field(
        default=15.0,
        description="RD decrease per race (converges toward certainty)",
    )
    rd_decay_floor: float = Field(
        default=0.0,
        description="Minimum RD decay applied per race when RD is enabled",
    )
    rd_inflation_per_day: float = Field(
        default=0.5,
        description="RD increase per day of inactivity",
    )
    rd_inflation_cap_days: int | None = Field(
        default=None,
        description="Optional cap on inactivity days used for RD inflation",
    )
    rd_scaling_mode: Literal["linear", "sqrt", "none"] = Field(
        default="linear",
        description="Scaling mode for K-factor when RD is enabled",
    )

    # Multi-entity weights
    driver_weight_alpha: float = Field(
        default=0.35,
        description="Weight for driver contribution to effective rating",
    )
    trainer_weight_beta: float = Field(
        default=0.15,
        description="Weight for trainer contribution to effective rating",
    )
    horse_k_scale: float = Field(
        default=1.0,
        description="Multiplier applied to horse rating updates",
    )
    driver_k_scale: float = Field(
        default=1.0,
        description="Multiplier applied to driver rating updates",
    )
    trainer_k_scale: float = Field(
        default=1.0,
        description="Multiplier applied to trainer rating updates",
    )

    # Condition adjustment learning
    adj_learning_rate: float = Field(
        default=0.5,
        description="Learning rate for barrier/handicap adjustments",
    )
    adj_update_scale: float = Field(
        default=1.0,
        description="Scale factor applied to adjustment deltas before learning",
    )
    adj_min_samples: int = Field(
        default=0,
        description="Minimum samples required before applying learned adjustments",
    )
    adj_clamp_min: float | None = Field(
        default=None,
        description="Optional minimum clamp for learned adjustments",
    )
    adj_clamp_max: float | None = Field(
        default=None,
        description="Optional maximum clamp for learned adjustments",
    )
    adj_global_only: bool = Field(
        default=False,
        description="If true, only update/use global adjustments (no venue-specific)",
    )
    adj_barrier_enabled: bool = Field(
        default=True,
        description="Enable barrier adjustment learning/application",
    )
    adj_handicap_enabled: bool = Field(
        default=True,
        description="Enable handicap adjustment learning/application",
    )
    place_history_limit: int = Field(
        default=8,
        description="Number of recent finishes used for place consistency",
    )
    place_prior_rate: float = Field(
        default=0.33,
        description="Prior top-3 rate for place probability smoothing",
    )
    place_prior_weight: float = Field(
        default=3.0,
        description="Weight of the top-3 prior in place probability smoothing",
    )
    place_top3_weight: float = Field(
        default=0.75,
        description="Weight applied to the smoothed top-3 rate signal",
    )
    place_consistency_weight: float = Field(
        default=0.5,
        description="Weight applied to finish consistency signal",
    )

    # Feature flags
    enable_driver: bool = Field(
        default=True,
        description="Include driver ratings in calculations",
    )
    enable_trainer: bool = Field(
        default=True,
        description="Include trainer ratings in calculations",
    )
    enable_adjustments: bool = Field(
        default=True,
        description="Learn and apply barrier/handicap adjustments",
    )
    enable_rd: bool = Field(
        default=False,
        description="Track and use rating deviation (RD)",
    )
    min_finishers: int = Field(
        default=2,
        description="Minimum finishers required to process a race",
    )
    dnf_treated_as_last: bool = Field(
        default=False,
        description="Treat DNF starters as last-place finishers",
    )
    tie_handling: Literal["ordered", "half", "skip"] = Field(
        default="ordered",
        description="Handling for tied placings (ordered, half, skip)",
    )

    # Distance buckets for condition adjustments (in meters)
    distance_buckets: list[int] = Field(
        default=[1700, 2000, 2400],
        description="Distance thresholds for bucketing (in meters)",
    )
    distance_bucket_mode: Literal["thresholds", "fixed"] = Field(
        default="thresholds",
        description="Bucket mode: thresholds list or fixed-size buckets",
    )
    distance_bucket_size: int | None = Field(
        default=None,
        description="Fixed bucket size in meters (required if mode=fixed)",
    )

    @field_validator("distance_buckets", mode="before")
    @classmethod
    def parse_distance_buckets(cls, v):
        """Parse distance buckets from comma-separated string or list."""
        if isinstance(v, str):
            return [int(x.strip()) for x in v.split(",")]
        return v

    @field_validator("distance_bucket_size")
    @classmethod
    def validate_bucket_size(cls, v, info):
        """Ensure fixed-size bucketing has a valid bucket size."""
        if info.data.get("distance_bucket_mode") == "fixed":
            if v is None or v <= 0:
                raise ValueError(
                    "DISTANCE_BUCKET_SIZE must be positive when mode is fixed"
                )
        return v

    @field_validator("rating_max")
    @classmethod
    def validate_rating_bounds(cls, v, info):
        """Ensure rating bounds are consistent."""
        rating_min = info.data.get("rating_min")
        if v is not None and rating_min is not None and v < rating_min:
            raise ValueError("RATING_MAX must be >= RATING_MIN")
        return v

    model_config = SettingsConfigDict(
        env_prefix="",
        json_schema_extra={"examples": [{"distance_buckets": "1700,2000,2400"}]},
    )


class RateLimitSettings(BaseSettings):
    """Rate limit configuration for the API.

    Each limit is a string compatible with slowapi (e.g. ``"100/minute"``).
    """

    default: str = Field(
        default="100/minute", description="Default per-user rate limit"
    )
    export: str = Field(
        default="10/minute", description="Export endpoint per-user rate limit"
    )
    predictions: str = Field(
        default="50/minute", description="Predictions endpoint per-user rate limit"
    )
    races_list: str = Field(
        default="200/minute", description="Race list endpoint per-user rate limit"
    )
    race_detail: str = Field(
        default="100/minute", description="Race detail endpoint per-user rate limit"
    )
    admin: str = Field(
        default="20/minute", description="Admin endpoint per-user rate limit"
    )

    model_config = SettingsConfigDict(env_prefix="RATE_LIMIT_")


class RedisSettings(BaseSettings):
    """Redis cache configuration."""

    url: str = Field(
        default="redis://localhost:6379/0", description="Redis connection URL"
    )
    ttl_seconds: int = Field(default=300, description="Default cache TTL in seconds")
    enabled: bool = Field(default=True, description="Enable Redis caching")

    model_config = SettingsConfigDict(env_prefix="REDIS_")


class IngestServiceSettings(BaseSettings):
    """tab-api-ingest service configuration.

    The tab-api-ingest service is a TypeScript/Express service that fetches
    racing data from the TAB and HRNZ APIs and exposes it via its own REST API.
    """

    url: str = Field(
        default="http://localhost:9090",
        description="tab-api-ingest service base URL",
    )

    model_config = SettingsConfigDict(env_prefix="INGEST_SERVICE_")


class SchedulerSettings(BaseSettings):
    """Scheduler configuration for automated background jobs."""

    enabled: bool = Field(
        default=True,
        description="Enable the background scheduler on startup",
    )
    ingest_cron: str = Field(
        default="0 2 * * *",
        description="Cron expression for daily ingestion (default: daily at 2am)",
    )
    recompute_cron: str = Field(
        default="0 3 * * *",
        description="Cron expression for daily recompute (default: daily at 3am)",
    )
    scrape_cron: str | None = Field(
        default=None,
        description="Cron expression for HRNZ scraping (default: disabled)",
    )
    eval_cron: str = Field(
        default="0 4 * * 0",
        description="Cron for weekly evaluation report (Sunday 4am)",
    )
    full_recompute_cron: str = Field(
        default="0 3 1 * *",
        description="Cron for monthly full recompute (1st of month 3am)",
    )
    email_notifications: bool = Field(
        default=False,
        description="Enable email notifications for scheduler failures",
    )
    email_smtp_host: str = Field(
        default="",
        description="SMTP host for notifications",
    )
    email_smtp_port: int = Field(
        default=587,
        description="SMTP port",
    )
    email_from: str = Field(
        default="",
        description="From address for notifications",
    )
    email_to: str = Field(
        default="",
        description="Comma-separated notification recipients",
    )
    email_username: str = Field(
        default="",
        description="SMTP username",
    )
    email_password: str = Field(
        default="",
        description="SMTP password",
    )
    timezone: str = Field(
        default="Pacific/Auckland",
        description="Timezone for scheduled jobs",
    )
    ingest_days_back: int = Field(
        default=1,
        description="Number of days back to ingest on each scheduled run",
    )
    recompute_days_back: int = Field(
        default=90,
        description="Number of days back to recompute on each scheduled run",
    )

    model_config = SettingsConfigDict(env_prefix="SCHEDULER_")


class Settings(BaseSettings):
    """Main application settings."""

    tab: TABSettings = Field(default_factory=TABSettings)
    database: DatabaseSettings = Field(default_factory=DatabaseSettings)
    logging: LoggingSettings = Field(default_factory=LoggingSettings)
    api: APISettings = Field(default_factory=APISettings)
    hrnz: HRNZSettings = Field(default_factory=HRNZSettings)
    rating: RatingSettings = Field(default_factory=RatingSettings)
    ingest_service: IngestServiceSettings = Field(default_factory=IngestServiceSettings)
    rate_limit: RateLimitSettings = Field(default_factory=RateLimitSettings)
    redis: RedisSettings = Field(default_factory=RedisSettings)
    scheduler: SchedulerSettings = Field(default_factory=SchedulerSettings)

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=False,
        extra="ignore",
    )


# Global settings instance
_settings: Settings | None = None


def get_settings() -> Settings:
    """Get or create global settings instance.

    Returns:
        Settings: Application settings loaded from environment
    """
    global _settings
    if _settings is None:
        _settings = Settings()
    return _settings


def reload_settings() -> Settings:
    """Force reload settings from environment.

    Useful for testing or dynamic configuration changes.

    Returns:
        Settings: Freshly loaded settings
    """
    global _settings
    _settings = Settings()
    return _settings
