"""Typed application settings: the only reader of environment variables."""

from enum import StrEnum

from pydantic_settings import BaseSettings, SettingsConfigDict


class AppMode(StrEnum):
    """Runtime mode: live Spotify + Anthropic, or fixture-replay demo."""

    LIVE = "live"
    DEMO = "demo"


class Settings(BaseSettings):
    """Application settings, loaded from the environment or a .env file."""

    model_config = SettingsConfigDict(env_file=".env", extra="ignore")

    app_mode: AppMode = AppMode.DEMO

    # Spotify application and OAuth. The redirect URI must exactly match a
    # URI registered in the Spotify developer dashboard, port included.
    spotify_client_id: str = ""
    spotify_redirect_uri: str = "http://127.0.0.1:8888/callback"
    spotify_api_base_url: str = "https://api.spotify.com/v1"
    # Set behind TLS so the session cookie is never sent over plain HTTP.
    session_cookie_secure: bool = False
    # A pre-authorized refresh token installs a session at startup, so a
    # hosted instance works without an interactive login.
    spotify_seed_refresh_token: str = ""

    # Spotify transport. A Retry-After above the cap fails the request
    # instead of silently holding it open for seconds.
    spotify_timeout_seconds: float = 10.0
    spotify_retry_after_cap_seconds: float = 5.0

    # LLM provider. Effort steers reasoning depth per call: intent is a
    # recall task, reranking benefits from more deliberation.
    anthropic_api_key: str = ""
    llm_model: str = "claude-sonnet-5"
    intent_effort: str = "low"
    rerank_effort: str = "medium"
    # Bound provider calls independently from the grounding deadline.
    llm_timeout_seconds: float = 120.0
    # Ceilings include adaptive thinking tokens, which is why they sit far
    # above the size of the structured output itself.
    intent_max_tokens: int = 16384
    rerank_max_tokens: int = 16384

    # Pipeline shape. candidate_count is the main call-1 latency lever and
    # the hallucination budget: at "new to you" familiarity a large share of
    # proposed tracks fails verification, so breadth keeps the pool filled.
    # The buffer gives the reranker real choices beyond the shown count.
    candidate_count: int = 35
    rerank_count: int = 15
    rerank_pool_buffer: int = 5

    # Grounding. Search is capped at 10 results per call, so resolving is a
    # fan-out; concurrency 6 stays far under the limiter (40 wide drew no
    # 429s when measured). Below the floor the response is an honest error
    # instead of a thin list. The similarity threshold rejects wrong tracks
    # while tolerating punctuation and edition noise.
    grounding_concurrency: int = 6
    grounding_floor: int = 8
    title_similarity_threshold: float = 0.82
    request_deadline_seconds: float = 25.0

    # In-process caches, single instance by design; a shared store is the
    # first production step. Resolution entries are small, so the bound is
    # generous; taste rarely shifts within a session.
    resolution_cache_ttl_seconds: float = 3600.0
    resolution_cache_max_entries: int = 2048
    taste_profile_ttl_seconds: float = 900.0

    # Taste profile fetch bounds: enough signal to describe a listener
    # without paging through an entire library on session start.
    top_items_limit: int = 50
    saved_tracks_limit: int = 100

    # Created playlists carry a fixed prefix so they can be found and
    # removed in bulk afterwards.
    playlist_name_prefix: str = "discovery-by-llm"


settings = Settings()
