"""Application configuration.

Every value documented in Section 11 of the build plan is read from the
environment via pydantic-settings. Defaults are development-safe; production must
override the secrets.
"""

from __future__ import annotations

import base64
import binascii
from functools import lru_cache

from pydantic_settings import BaseSettings, SettingsConfigDict


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

    # Core
    legate_env: str = "development"
    legate_secret_key: str = "dev-insecure-secret-change-me"
    legate_master_key: str = ""  # base64 32-byte key for the credential vault (Phase 4)
    api_base_url: str = "http://localhost:8000"
    web_base_url: str = "http://localhost:3000"
    # Comma-separated allowed CORS origins for the dashboard. Defaults to web_base_url.
    cors_origins: str = ""

    @property
    def cors_origin_list(self) -> list[str]:
        raw = self.cors_origins or self.web_base_url
        return [o.strip() for o in raw.split(",") if o.strip()]

    # Database / infra
    database_url: str = "postgresql+psycopg://legate:legate@postgres:5432/legate"
    redis_url: str = "redis://redis:6379/0"
    qdrant_url: str = "http://qdrant:6333"

    # Auth
    jwt_access_ttl_min: int = 15
    jwt_refresh_ttl_days: int = 30
    jwt_algorithm: str = "HS256"

    # LLM provider (provider-agnostic)
    llm_provider: str = "openrouter"
    llm_api_key: str = ""
    llm_default_model: str = ""
    openrouter_base_url: str = "https://openrouter.ai/api/v1"
    ollama_base_url: str = "http://localhost:11434"
    openai_base_url: str = "https://api.openai.com/v1"
    embedding_provider: str = "openai"  # openai | hashing
    embedding_model: str = "text-embedding-3-small"
    embedding_dim: int = 256  # dimensionality for the offline hashing embedder

    # Vector store (RAG)
    vector_store_backend: str = "qdrant"  # qdrant | memory

    # Object storage
    storage_backend: str = "local"
    storage_local_dir: str = "./data/storage"
    s3_endpoint: str = ""
    s3_bucket: str = ""
    s3_access_key: str = ""
    s3_secret_key: str = ""

    # Limits and safety
    run_max_steps: int = 25
    run_max_tokens: int = 200_000
    run_max_wallclock_sec: int = 600
    # Outbound requests to connector / MCP URLs block private, loopback, and
    # link-local targets to prevent SSRF. Trusted single-tenant deployments that
    # need internal endpoints can opt back in.
    allow_private_connector_urls: bool = False

    # When true, POST /agents/{id}/run executes the agent loop inline before
    # responding instead of scheduling it in the background. Used by tests for
    # deterministic runs; production leaves this false.
    agent_run_inline: bool = False
    # When true, document ingestion runs inline instead of via a Celery task.
    kb_ingest_inline: bool = False
    # When true, workflow runs execute inline instead of in the background.
    workflow_run_inline: bool = False

    # How long a pending approval stays valid before it auto-expires (0 = never).
    approval_ttl_hours: int = 24

    # Billing & plans (Phase 8). When disabled, plan limits are not enforced.
    billing_enabled: bool = False
    default_plan: str = "free"
    stripe_api_key: str = ""
    stripe_webhook_secret: str = ""

    # SSO via OIDC (Phase 8).
    oidc_enabled: bool = False
    oidc_client_id: str = ""
    oidc_client_secret: str = ""
    oidc_redirect_uri: str = "http://localhost:8000/auth/oidc/callback"
    oidc_authorization_endpoint: str = ""
    oidc_token_endpoint: str = ""
    oidc_userinfo_endpoint: str = ""
    oidc_scopes: str = "openid email profile"

    # Rate limiting (Phase 8). Off by default so it never surprises small self-hosts.
    rate_limit_enabled: bool = False
    rate_limit_runs_per_minute: int = 30

    # Observability (Phase 8).
    metrics_enabled: bool = True
    otel_exporter_otlp_endpoint: str = ""
    otel_service_name: str = "legate-api"

    @property
    def is_production(self) -> bool:
        return self.legate_env.lower() in {"production", "prod"}


class UnsafeProductionConfig(RuntimeError):
    """Raised at startup when production would run with insecure default secrets."""


# Substrings that mark a secret as a shipped development placeholder.
_INSECURE_SECRET_MARKERS = ("change-me", "changeme", "insecure")


def check_production_safety(settings: Settings) -> None:
    """Refuse to boot in production with development-default secrets.

    This guards against the most common self-host footgun: deploying with the
    shipped dev defaults, which would make JWTs forgeable, leave the credential
    vault unusable, and expose the database with well-known credentials. It is a
    no-op outside production so local development stays frictionless.
    """
    if not settings.is_production:
        return

    problems: list[str] = []

    secret = settings.legate_secret_key.strip()
    if not secret or any(marker in secret.lower() for marker in _INSECURE_SECRET_MARKERS):
        problems.append(
            "LEGATE_SECRET_KEY is unset or still a development default; "
            "set it to a long, random secret."
        )

    master = settings.legate_master_key.strip()
    if not master:
        problems.append(
            "LEGATE_MASTER_KEY is not set; the credential vault cannot operate. "
            "Set it to a base64-encoded 32-byte key."
        )
    else:
        try:
            if len(base64.b64decode(master, validate=True)) != 32:
                raise ValueError
        except (ValueError, binascii.Error):
            problems.append("LEGATE_MASTER_KEY must decode to exactly 32 bytes of base64.")

    if "legate:legate@" in settings.database_url:
        problems.append(
            "DATABASE_URL still uses the default 'legate:legate' credentials; "
            "use a strong database password in production."
        )

    if problems:
        bullet = "\n  - "
        raise UnsafeProductionConfig(
            f"Refusing to start in production (LEGATE_ENV={settings.legate_env}) with "
            f"insecure configuration:{bullet}{bullet.join(problems)}"
        )


@lru_cache
def get_settings() -> Settings:
    """Cached settings accessor used across the app and as a FastAPI dependency."""
    return Settings()
