from __future__ import annotations

import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any, ClassVar

from pydantic import AliasChoices, Field, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

from cleveragents.domain.models.core.retry_policy import RetryStrategy

# pydantic v2: use string for extra mode
EXTRA_IGNORE = "ignore"
STRUCTLOG_ENABLED = False


@dataclass(slots=True)
class ProviderDefaults:
    provider: str | None
    provider_source: str
    model: str | None
    model_source: str


class Settings(BaseSettings):
    """Application runtime configuration backed by environment variables."""

    model_config = SettingsConfigDict(
        env_prefix="cleveragents_", case_sensitive=False, extra="ignore"
    )

    _instance: ClassVar[Settings | None] = None
    _PROVIDER_ENV_MAP: ClassVar[dict[str, tuple[str, ...]]] = {
        "openai_api_key": ("OPENAI_API_KEY",),
        "anthropic_api_key": ("ANTHROPIC_API_KEY",),
        "google_api_key": ("GOOGLE_API_KEY", "GOOGLE_GENAI_API_KEY"),
        "azure_api_key": ("AZURE_OPENAI_API_KEY", "AZURE_API_KEY"),
        "openrouter_api_key": ("OPENROUTER_API_KEY",),
        "gemini_api_key": ("GEMINI_API_KEY", "GOOGLE_GEMINI_API_KEY"),
        "hf_token": ("HF_TOKEN", "HUGGINGFACEHUB_API_TOKEN", "HUGGING_FACE_HUB_TOKEN"),
        "cohere_api_key": ("COHERE_API_KEY",),
        "perplexity_api_key": ("PERPLEXITY_API_KEY",),
        "groq_api_key": ("GROQ_API_KEY",),
        "together_api_key": ("TOGETHER_API_KEY",),
    }
    _PROVIDER_ALIAS: ClassVar[dict[str, str]] = {
        "openai": "openai_api_key",
        "anthropic": "anthropic_api_key",
        "google": "google_api_key",
        "azure": "azure_api_key",
        "openrouter": "openrouter_api_key",
        "router": "openrouter_api_key",
        "gemini": "gemini_api_key",
        "huggingface": "hf_token",
        "hf": "hf_token",
        "cohere": "cohere_api_key",
        "perplexity": "perplexity_api_key",
        "groq": "groq_api_key",
        "together": "together_api_key",
    }
    _PROVIDER_FALLBACK_ORDER: ClassVar[tuple[str, ...]] = (
        "openai",
        "anthropic",
        "google",
        "azure",
        "openrouter",
        "groq",
        "together",
        "cohere",
        "gemini",
    )
    _PROVIDER_DEFAULT_MODELS: ClassVar[dict[str, str]] = {
        "openai": "gpt-4o",
        "anthropic": "claude-sonnet-4-20250514",
        "google": "gemini-2.0-flash",
        "gemini": "gemini-2.0-flash",
        "azure": "gpt-4o",
        "openrouter": "anthropic/claude-sonnet-4-20250514",
        "cohere": "command-r-plus",
        "groq": "llama-3.1-70b-versatile",
        "together": "meta-llama/Llama-3.1-70B-Instruct-Turbo",
        "mock": "mock-gpt",
    }

    # Automation profile (the only automation control)
    default_automation_profile: str = Field(
        default="",
        validation_alias=AliasChoices(
            "CLEVERAGENTS_AUTOMATION_PROFILE",
        ),
        description=(
            "Default automation profile name (e.g. 'manual', 'auto', 'full-auto')."
        ),
    )

    # Runtime/server configuration
    env: str = Field(
        default="development",
        validation_alias=AliasChoices("CLEVERAGENTS_ENV"),
    )
    server_host: str = Field(
        default="0.0.0.0",  # nosec B104
        validation_alias=AliasChoices("CLEVERAGENTS_SERVER_HOST"),
    )
    server_port: int = Field(
        default=8080,
        validation_alias=AliasChoices("CLEVERAGENTS_SERVER_PORT"),
    )
    server_reload: bool = Field(
        default=False,
        validation_alias=AliasChoices("CLEVERAGENTS_SERVER_RELOAD"),
    )
    server_url: str | None = Field(
        default=None,
        validation_alias=AliasChoices("CLEVERAGENTS_SERVER_URL"),
        description=(
            "URL for the CleverAgents server endpoint, enabling server mode. "
            "Maps to the spec's server.url config key."
        ),
    )
    server_token: str | None = Field(
        default=None,
        validation_alias=AliasChoices("CLEVERAGENTS_SERVER_TOKEN"),
        description=(
            "Authentication token for connecting to the CleverAgents server. "
            "Maps to the spec's server.token config key."
        ),
    )
    debug_enabled: bool = Field(
        default=False,
        validation_alias=AliasChoices("CLEVERAGENTS_DEBUG_ENABLED"),
    )

    # Core output and estimation configuration
    format: str | None = Field(
        default=None,
        validation_alias=AliasChoices("CLEVERAGENTS_FORMAT"),
        description=(
            "Default output format (e.g. 'json', 'markdown', 'text'). "
            "Overrides the core.format config key when set."
        ),
    )
    default_estimation_actor: str | None = Field(
        default=None,
        validation_alias=AliasChoices("CLEVERAGENTS_DEFAULT_ESTIMATION_ACTOR"),
        description=(
            "Default actor used for estimation tasks. "
            "Maps to the spec's core.default_estimation_actor config key."
        ),
    )

    # Logging/paths
    log_level: str = Field(
        default="INFO",
        validation_alias=AliasChoices("CLEVERAGENTS_LOG_LEVEL"),
    )
    debug_log_level: str = Field(
        default="INFO",
        validation_alias=AliasChoices("CLEVERAGENTS_DEBUG_LOG_LEVEL"),
    )
    log_dir: Path = Field(
        default_factory=lambda: Path("logs"),
        validation_alias=AliasChoices("CLEVERAGENTS_LOG_DIR"),
    )
    data_dir: Path = Field(
        default_factory=lambda: Path.home() / ".cleveragents",
        validation_alias=AliasChoices("CLEVERAGENTS_DATA_DIR"),
    )
    storage_base_path: Path = Field(
        default_factory=lambda: Path("data"),
        validation_alias=AliasChoices("CLEVERAGENTS_STORAGE_BASE_PATH"),
    )

    # Cleanup / retention policies (CONC3)
    cleanup_sandbox_max_age_hours: int = Field(
        default=48,
        ge=1,
        validation_alias=AliasChoices("CLEVERAGENTS_CLEANUP_SANDBOX_MAX_AGE_HOURS"),
        description="Max age (hours) before stale sandboxes are eligible for cleanup.",
    )
    cleanup_checkpoint_max_per_plan: int = Field(
        default=50,
        ge=2,
        validation_alias=AliasChoices("CLEVERAGENTS_CHECKPOINT_MAX"),
        description=(
            "Max checkpoints per plan; oldest pruned first (keep first + most recent). "
            "Minimum 2 to always preserve the first and most recent checkpoints."
        ),
    )
    cleanup_session_inactivity_days: int = Field(
        default=30,
        ge=1,
        validation_alias=AliasChoices("CLEVERAGENTS_CLEANUP_SESSION_INACTIVITY_DAYS"),
        description="Days of inactivity before a session is eligible for cleanup.",
    )
    cleanup_log_retention_days: int = Field(
        default=30,
        ge=1,
        validation_alias=AliasChoices("CLEVERAGENTS_LOG_RETENTION_DAYS"),
        description="Days to retain log files before automatic cleanup.",
    )
    cleanup_backup_retention_days: int = Field(
        default=7,
        ge=1,
        validation_alias=AliasChoices("CLEVERAGENTS_BACKUP_RETENTION_DAYS"),
        description="Days to retain backup snapshots before automatic cleanup.",
    )
    cleanup_schedule: str = Field(
        default="manual",
        validation_alias=AliasChoices("CLEVERAGENTS_CLEANUP_SCHEDULE"),
        description="Cleanup schedule: 'manual' (MVP default) or 'auto'.",
    )

    # Audit logging retention (SEC7)
    audit_retention_days: int = Field(
        default=0,
        ge=0,
        validation_alias=AliasChoices("CLEVERAGENTS_AUDIT_RETENTION_DAYS"),
        description=(
            "Days to retain audit log entries before pruning. "
            "0 means keep indefinitely (spec default for compliance)."
        ),
    )

    # Async audit recording (issue #718)
    audit_async: bool = Field(
        default=True,
        validation_alias=AliasChoices("CLEVERAGENTS_AUDIT_ASYNC"),
        description=(
            "When True (default), audit entries are written asynchronously "
            "via a write-behind queue on a background thread, so that "
            "AuditService.record() does not block the calling domain "
            "operation.  Set to False to restore synchronous behaviour "
            "(useful for debugging or when strict ordering is required)."
        ),
    )

    audit_queue_maxsize: int = Field(
        default=10_000,
        ge=1,
        validation_alias=AliasChoices("CLEVERAGENTS_AUDIT_QUEUE_MAXSIZE"),
        description=(
            "Maximum number of pending audit entries in the write-behind "
            "queue.  When the queue is full, record() blocks until space "
            "is available, providing back-pressure.  Increase this value "
            "for very high-throughput workloads."
        ),
    )

    # Per-session and per-org cost budgets (M6+ #584)
    session_max_cost_usd: float | None = Field(
        default=None,
        ge=0.0,
        validation_alias=AliasChoices("CLEVERAGENTS_SESSION_MAX_COST_USD"),
        description=(
            "Maximum USD spend per session across all plans. "
            "None (default) means unlimited."
        ),
    )
    org_max_cost_usd: float | None = Field(
        default=None,
        ge=0.0,
        validation_alias=AliasChoices("CLEVERAGENTS_ORG_MAX_COST_USD"),
        description=(
            "Maximum USD spend per organisation across all sessions. "
            "None (default) means unlimited."
        ),
    )
    budget_warning_threshold: float = Field(
        default=0.8,
        ge=0.0,
        le=1.0,
        validation_alias=AliasChoices("CLEVERAGENTS_BUDGET_WARNING_THRESHOLD"),
        description=(
            "Utilisation ratio (0.0-1.0) at which a BUDGET_WARNING event "
            "is emitted.  Default 0.8 (80%)."
        ),
    )

    # Cost controls (M4)
    budget_per_plan: float | None = Field(
        default=None,
        ge=0.0,
        validation_alias=AliasChoices("CLEVERAGENTS_BUDGET_PER_PLAN"),
        description=(
            "Maximum USD spend per plan execution. None (default) means unlimited."
        ),
    )
    budget_per_day: float | None = Field(
        default=None,
        ge=0.0,
        validation_alias=AliasChoices("CLEVERAGENTS_BUDGET_PER_DAY"),
        description=(
            "Maximum USD spend per calendar day across all plans. "
            "None (default) means unlimited."
        ),
    )
    fallback_providers: list[str] = Field(
        default_factory=list,
        validation_alias=AliasChoices("CLEVERAGENTS_FALLBACK_PROVIDERS"),
        description=(
            "Ordered list of provider names to try when the primary "
            "provider is unavailable or over budget. "
            "Empty list uses the default fallback order."
        ),
    )

    # Persistence
    database_url: str = Field(
        default_factory=lambda: (
            f"sqlite:///{Path.home() / '.cleveragents' / 'cleveragents.db'}"
        ),
        validation_alias=AliasChoices("CLEVERAGENTS_DATABASE_URL"),
    )
    test_database_url: str = Field(
        default_factory=lambda: (
            f"sqlite:///{Path.home() / '.cleveragents' / 'cleveragents_test.db'}"
        ),
        validation_alias=AliasChoices("CLEVERAGENTS_TEST_DATABASE_URL"),
    )

    # Context tier budgets (ACMS #208)
    context_max_tokens_hot: int = Field(
        default=8000,
        ge=0,
        validation_alias=AliasChoices("CLEVERAGENTS_CONTEXT_MAX_TOKENS_HOT"),
        description="Maximum total tokens in the hot context tier.",
    )
    context_max_decisions_warm: int = Field(
        default=500,
        ge=0,
        validation_alias=AliasChoices("CLEVERAGENTS_CONTEXT_MAX_DECISIONS_WARM"),
        description="Maximum fragments in the warm context tier.",
    )
    context_max_decisions_cold: int = Field(
        default=5000,
        ge=0,
        validation_alias=AliasChoices("CLEVERAGENTS_CONTEXT_MAX_DECISIONS_COLD"),
        description="Maximum fragments in the cold context tier.",
    )

    # Context tier runtime settings (ACMS #821)
    context_tier_promotion_threshold: int = Field(
        default=5,
        ge=1,
        validation_alias=AliasChoices("CLEVERAGENTS_CONTEXT_TIER_PROMOTION_THRESHOLD"),
        description="Number of accesses before a fragment is auto-promoted.",
    )
    context_tier_hot_ttl_hours: int = Field(
        default=24,
        ge=1,
        validation_alias=AliasChoices(
            "CLEVERAGENTS_CONTEXT_TIER_HOT_TTL_HOURS",
            "CLEVERAGENTS_CTX_HOT_HOURS",
        ),
        description="Hours without access before hot fragment is demoted to warm.",
    )
    context_tier_warm_ttl_hours: int = Field(
        default=24,
        ge=1,
        validation_alias=AliasChoices(
            "CLEVERAGENTS_CONTEXT_TIER_WARM_TTL_HOURS",
            "CLEVERAGENTS_CTX_WARM_HOURS",
        ),
        description="Hours without access before warm fragment is demoted to cold.",
    )

    # Vector store configuration
    vector_store_enabled: bool = Field(
        default=False,
        validation_alias=AliasChoices("CLEVERAGENTS_VECTOR_STORE_ENABLED"),
    )
    vector_store_backend: str = Field(
        default="faiss",
        validation_alias=AliasChoices("CLEVERAGENTS_VECTOR_STORE_BACKEND"),
    )
    vector_store_path: Path = Field(
        default_factory=lambda: Path(".cleveragents") / "vector_store",
        validation_alias=AliasChoices("CLEVERAGENTS_VECTOR_STORE_PATH"),
    )
    vector_embeddings_provider: str = Field(
        default="fake",
        validation_alias=AliasChoices("CLEVERAGENTS_VECTOR_EMBEDDINGS_PROVIDER"),
    )
    vector_embeddings_model: str | None = Field(
        default=None,
        validation_alias=AliasChoices("CLEVERAGENTS_VECTOR_EMBEDDINGS_MODEL"),
    )
    vector_embeddings_dimension: int = Field(
        default=1536,
        validation_alias=AliasChoices("CLEVERAGENTS_VECTOR_EMBEDDINGS_DIMENSION"),
    )

    # Provider defaults
    default_provider: str | None = Field(
        default=None,
        validation_alias=AliasChoices("CLEVERAGENTS_DEFAULT_PROVIDER"),
    )
    default_model: str | None = Field(
        default=None,
        validation_alias=AliasChoices("CLEVERAGENTS_DEFAULT_MODEL"),
    )

    # LangSmith
    langsmith_enabled: bool = Field(
        default=False,
        validation_alias=AliasChoices(
            "CLEVERAGENTS_LANGSMITH_ENABLED",
            "CLEVERAGENTS_LANGSMITH_TRACING_V2",
            "LANGSMITH_TRACING_V2",
            "LANGCHAIN_TRACING_V2",
        ),
    )
    langsmith_api_key: str | None = Field(
        default=None,
        validation_alias=AliasChoices(
            "CLEVERAGENTS_LANGSMITH_API_KEY",
            "LANGSMITH_API_KEY",
            "LANGCHAIN_API_KEY",
        ),
    )
    langsmith_project: str | None = Field(
        default=None,
        validation_alias=AliasChoices(
            "CLEVERAGENTS_LANGSMITH_PROJECT",
            "LANGSMITH_PROJECT",
            "LANGCHAIN_PROJECT",
        ),
    )
    langsmith_endpoint: str | None = Field(
        default=None,
        validation_alias=AliasChoices(
            "CLEVERAGENTS_LANGSMITH_ENDPOINT",
            "LANGSMITH_ENDPOINT",
            "LANGCHAIN_ENDPOINT",
        ),
    )
    langsmith_user_id: str | None = Field(
        default=None,
        validation_alias=AliasChoices(
            "CLEVERAGENTS_LANGSMITH_USER_ID",
            "LANGSMITH_USER_ID",
        ),
    )
    langsmith_tags: list[str] = Field(
        default_factory=list,
        validation_alias=AliasChoices("CLEVERAGENTS_LANGSMITH_TAGS"),
    )

    # Provider keys (populated from both prefixed and provider env vars)
    openai_api_key: str | None = Field(default=None)
    anthropic_api_key: str | None = Field(default=None)
    google_api_key: str | None = Field(default=None)
    azure_api_key: str | None = Field(default=None)
    openrouter_api_key: str | None = Field(default=None)
    gemini_api_key: str | None = Field(default=None)
    hf_token: str | None = Field(default=None)
    cohere_api_key: str | None = Field(default=None)
    perplexity_api_key: str | None = Field(default=None)
    groq_api_key: str | None = Field(default=None)
    together_api_key: str | None = Field(default=None)
    azure_openai_endpoint: str | None = Field(
        default=None,
        validation_alias=AliasChoices(
            "AZURE_OPENAI_ENDPOINT",
            "AZURE_API_BASE",
            "CLEVERAGENTS_AZURE_OPENAI_ENDPOINT",
        ),
    )
    azure_openai_api_version: str | None = Field(
        default=None,
        validation_alias=AliasChoices(
            "AZURE_OPENAI_API_VERSION",
            "AZURE_API_VERSION",
            "CLEVERAGENTS_AZURE_OPENAI_API_VERSION",
        ),
    )
    azure_openai_deployment: str | None = Field(
        default=None,
        validation_alias=AliasChoices(
            "AZURE_OPENAI_DEPLOYMENT",
            "AZURE_OPENAI_CHAT_DEPLOYMENT",
            "CLEVERAGENTS_AZURE_OPENAI_DEPLOYMENT",
        ),
    )
    openrouter_organization: str | None = Field(
        default=None,
        validation_alias=AliasChoices(
            "OPENROUTER_ORGANIZATION",
            "CLEVERAGENTS_OPENROUTER_ORGANIZATION",
        ),
    )

    # Secrets visibility (SEC5)
    show_secrets: bool = Field(
        default=False,
        validation_alias=AliasChoices("CLEVERAGENTS_SHOW_SECRETS"),
        description="When True, secrets are shown in CLI output and logs.",
    )

    # Planner decomposition (M6 - large-project decomposition)
    planner_max_depth: int = Field(
        default=4,
        ge=1,
        validation_alias=AliasChoices("CLEVERAGENTS_PLANNER_MAX_DEPTH"),
        description="Maximum decomposition depth for large projects.",
    )
    planner_max_files_per_subplan: int = Field(
        default=500,
        ge=1,
        validation_alias=AliasChoices("CLEVERAGENTS_PLANNER_MAX_FILES_PER_SUBPLAN"),
        description="Maximum files per decomposition subplan.",
    )
    planner_max_tokens_per_subplan: int = Field(
        default=100_000,
        ge=1,
        validation_alias=AliasChoices("CLEVERAGENTS_PLANNER_MAX_TOKENS_PER_SUBPLAN"),
        description="Maximum estimated tokens per decomposition subplan.",
    )
    planner_min_files_per_subplan: int = Field(
        default=10,
        ge=1,
        validation_alias=AliasChoices("CLEVERAGENTS_PLANNER_MIN_FILES_PER_SUBPLAN"),
        description="Minimum files below which decomposition is skipped.",
    )

    # Async job execution (M6 - #312)
    async_enabled: bool = Field(
        default=False,
        validation_alias=AliasChoices("CLEVERAGENTS_ASYNC_ENABLED"),
        description="Enable asynchronous job execution for plan phases.",
    )
    async_max_workers: int = Field(
        default=4,
        ge=1,
        validation_alias=AliasChoices("CLEVERAGENTS_ASYNC_MAX_WORKERS"),
        description="Maximum concurrent async workers.",
    )
    async_poll_interval: float = Field(
        default=1.0,
        gt=0,
        validation_alias=AliasChoices("CLEVERAGENTS_ASYNC_POLL_INTERVAL"),
        description="Polling interval in seconds for async workers.",
    )
    async_job_timeout: int = Field(
        default=3600,
        ge=1,
        validation_alias=AliasChoices("CLEVERAGENTS_ASYNC_JOB_TIMEOUT"),
        description="Job timeout in seconds (mark failed after no heartbeat).",
    )
    async_job_ttl: int = Field(
        default=86400,
        ge=1,
        validation_alias=AliasChoices("CLEVERAGENTS_ASYNC_JOB_TTL"),
        description="Completed job retention in seconds before cleanup.",
    )

    # Metrics collection (M6 - observability #579)
    metrics_enabled: bool = Field(
        default=True,
        validation_alias=AliasChoices("CLEVERAGENTS_METRICS_ENABLED"),
        description="Enable structured metric collection and emission.",
    )
    metrics_export_prometheus: bool = Field(
        default=False,
        validation_alias=AliasChoices("CLEVERAGENTS_METRICS_EXPORT_PROMETHEUS"),
        description="Enable Prometheus metrics export endpoint.",
    )

    # Retry policy configuration (M6 - #313)
    retry_max_attempts: int = Field(
        default=3,
        ge=1,
        le=50,
        validation_alias=AliasChoices("CLEVERAGENTS_RETRY_MAX_ATTEMPTS"),
        description="Default maximum retry attempts for service operations.",
    )
    retry_base_delay: float = Field(
        default=1.0,
        ge=0.0,
        le=300.0,
        validation_alias=AliasChoices("CLEVERAGENTS_RETRY_BASE_DELAY"),
        description="Default base delay in seconds between retries.",
    )
    retry_max_delay: float = Field(
        default=60.0,
        ge=0.0,
        le=3600.0,
        validation_alias=AliasChoices("CLEVERAGENTS_RETRY_MAX_DELAY"),
        description="Default maximum delay in seconds between retries.",
    )
    retry_jitter: bool = Field(
        default=True,
        validation_alias=AliasChoices("CLEVERAGENTS_RETRY_JITTER"),
        description="Whether to add random jitter to retry delays.",
    )
    retry_backoff_strategy: RetryStrategy = Field(
        default=RetryStrategy.EXPONENTIAL,
        validation_alias=AliasChoices("CLEVERAGENTS_RETRY_BACKOFF_STRATEGY"),
        description=(
            "Default backoff strategy: exponential, linear, fixed, jitter, none."
        ),
    )

    # Circuit breaker configuration (M6 - #313)
    circuit_breaker_failure_threshold: int = Field(
        default=5,
        ge=1,
        validation_alias=AliasChoices("CLEVERAGENTS_CB_FAILURE_THRESHOLD"),
        description="Consecutive failures before a circuit breaker opens.",
    )
    circuit_breaker_recovery_timeout: float = Field(
        default=60.0,
        ge=1.0,
        validation_alias=AliasChoices("CLEVERAGENTS_CB_RECOVERY_TIMEOUT"),
        description="Seconds before an open circuit breaker enters half-open.",
    )
    circuit_breaker_cooldown: float = Field(
        default=30.0,
        ge=0.0,
        validation_alias=AliasChoices("CLEVERAGENTS_CB_COOLDOWN"),
        description="Minimum seconds between half-open recovery attempts.",
    )
    half_open_max_successes: int = Field(
        default=1,
        ge=1,
        le=20,
        validation_alias=AliasChoices("CLEVERAGENTS_CB_HALF_OPEN_MAX_SUCCESSES"),
        description=(
            "Consecutive successes in half-open state required to close the circuit."
        ),
    )

    # Per-service retry overrides (M6 - #313) — JSON string mapping
    retry_service_overrides: str = Field(
        default="{}",
        max_length=10000,
        validation_alias=AliasChoices("CLEVERAGENTS_RETRY_SERVICE_OVERRIDES"),
        description=(
            "JSON string mapping service names to retry policy overrides. "
            'Example: \'{"plan_service": {"retry": {"max_attempts": 5}}}\''
        ),
    )

    @model_validator(mode="after")
    def _max_delay_ge_base_delay(self) -> Settings:
        """Ensure retry_max_delay >= retry_base_delay."""
        if self.retry_max_delay < self.retry_base_delay:
            raise ValueError(
                f"retry_max_delay ({self.retry_max_delay}) must be >= "
                f"retry_base_delay ({self.retry_base_delay})"
            )
        return self

    # Mock providers flag (M4 - provider fixes)
    mock_providers: bool = Field(
        default=False,
        validation_alias=AliasChoices("CLEVERAGENTS_MOCK_PROVIDERS"),
        description=(
            "Enable mock providers for testing. Must not be used in production."
        ),
    )

    def validate_provider_availability(self) -> None:
        """Check that at least one provider is configured or mock_providers is on.

        Raises:
            ValueError: When no provider is available and mock_providers is False.
        """
        import logging as _logging

        if self.mock_providers:
            if self.env.lower() not in ("test", "testing", "development"):
                _log = _logging.getLogger(__name__)
                _log.warning(
                    "mock_providers is enabled outside of test/development mode. "
                    "Do not use mock providers in production."
                )
            return

        if not self.has_provider_configured():
            raise ValueError(
                "No AI providers configured and mock_providers is disabled. "
                "Set a provider API key (e.g. OPENAI_API_KEY) or set "
                "CLEVERAGENTS_MOCK_PROVIDERS=true for testing."
            )

    def __repr__(self) -> str:
        """Safe repr that masks sensitive field values."""
        from cleveragents.shared.redaction import REDACTED, is_sensitive_key

        parts: list[str] = []
        for field_name in self.model_fields:
            value = getattr(self, field_name, None)
            if is_sensitive_key(field_name) and value is not None:
                parts.append(f"{field_name}={REDACTED!r}")
            else:
                parts.append(f"{field_name}={value!r}")
        return f"Settings({', '.join(parts)})"

    def model_post_init(self, __context: Any) -> None:  # type: ignore[override]
        # pydantic v1 compatibility: BaseSettings may not define model_post_init
        maybe_super = getattr(super(), "model_post_init", None)
        if callable(maybe_super):
            maybe_super(__context)
        self._langsmith_validation_errors: list[str] = []
        self._apply_external_env_overrides()
        # Prime LangSmith state so environment mirrors configuration
        _ = self.is_langsmith_enabled

    # ------------------------------------------------------------------
    # Singleton helpers
    # ------------------------------------------------------------------
    @classmethod
    def get_settings(cls: type[Settings]) -> Settings:
        """Return the cached singleton instance (creating it if needed)."""
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

    @classmethod
    def reset(cls: type[Settings]) -> None:
        """Reset the cached singleton instance.

        Ensures the next ``get_settings()`` call constructs a fresh
        instance instead of reusing stale process-global state.

        .. warning:: **Test use only.** Do not call in production code
           paths — resetting the singleton mid-flight can cause
           inconsistent configuration state.
        """
        cls._instance = None

    # ------------------------------------------------------------------
    # Derived properties & helpers
    # ------------------------------------------------------------------
    @property
    def environment(self) -> str:
        """Backwards compatible alias for ``env``."""
        return self.env

    @environment.setter
    def environment(self, value: str) -> None:
        self.env = value

    @property
    def storage_path(self) -> Path:
        """Absolute path backing runtime storage."""
        base = self.storage_base_path
        if not base.is_absolute():
            base = Path.cwd() / base
        return base.resolve()

    def get_storage_base_path(self, storage_type: str | None = None) -> Path:
        """Return the base storage path optionally scoped by type."""
        if storage_type:
            storage_type = storage_type.strip()
            if storage_type:
                return self.storage_path / storage_type
        return self.storage_path

    def resolve_vector_store_path(self) -> Path:
        """Return the absolute path for persisted vector store data."""
        base = self.vector_store_path
        if not base.is_absolute():
            base = Path.cwd() / base
        return base.resolve()

    def get_database_url(self, *, test: bool = False) -> str:
        """Return the primary or test database URL."""
        if test:
            if self.test_database_url:
                return self.test_database_url
            return self._derive_test_database_url(self.database_url)
        return self.database_url

    def is_production(self) -> bool:
        """Whether the runtime matches production settings."""
        return (
            not self.debug_enabled
            and not self.server_reload
            and self.env.lower() == "production"
        )

    @property
    def is_production_mode(self) -> bool:
        """Convenience alias for :meth:`is_production`."""
        return self.is_production()

    @property
    def langsmith_validation_errors(self) -> list[str]:
        """Latest LangSmith configuration errors (if any)."""
        return list(getattr(self, "_langsmith_validation_errors", []))

    @property
    def is_langsmith_enabled(self) -> bool:
        """Check if LangSmith tracing is enabled and valid."""
        enabled, errors = self._evaluate_langsmith_configuration()
        self._langsmith_validation_errors = errors
        if enabled:
            self._synchronize_langsmith_environment()
        return enabled

    def validate_langsmith_configuration(self) -> tuple[bool, list[str]]:
        """Return the current LangSmith configuration status."""
        return self._evaluate_langsmith_configuration()

    def build_langsmith_config(
        self,
        *,
        tags: list[str] | None = None,
        metadata: dict[str, Any] | None = None,
        run_name: str | None = None,
    ) -> dict[str, Any] | None:
        """Build the config for LangSmith tracing."""
        if not self.is_langsmith_enabled:
            return None

        base_tags: list[str] = []
        if self.langsmith_tags:
            base_tags.extend(self.langsmith_tags)
        if tags:
            base_tags.extend(tags)

        merged_metadata: dict[str, Any] = {}
        user_id = self._get_langsmith_user_id()
        if user_id:
            merged_metadata.setdefault("user_id", user_id)
        project_name = self._get_langsmith_project()
        if project_name:
            merged_metadata.setdefault("langsmith_project", project_name)
        if metadata:
            merged_metadata.update(metadata)

        config: dict[str, Any] = {
            "tags": base_tags,
            "metadata": merged_metadata,
        }
        if run_name:
            config["run_name"] = run_name

        return config

    def _evaluate_langsmith_configuration(self) -> tuple[bool, list[str]]:
        """Determine whether LangSmith should be active and capture errors."""
        errors: list[str] = []
        requested = bool(self.langsmith_enabled) or (
            os.getenv("LANGCHAIN_TRACING_V2", "false").lower() == "true"
        )
        if not requested:
            return False, errors

        if not self._get_langsmith_api_key():
            errors.append(
                "LangSmith API key is required "
                "(set CLEVERAGENTS_LANGSMITH_API_KEY or LANGCHAIN_API_KEY)."
            )
        if not self._get_langsmith_project():
            errors.append(
                "LangSmith project name is required "
                "(set CLEVERAGENTS_LANGSMITH_PROJECT or LANGCHAIN_PROJECT)."
            )
        return (len(errors) == 0, errors)

    def has_provider_configured(self, provider: str | None = None) -> bool:
        """Return True if any (or a specific) provider API key is configured."""

        if provider is None:
            return any(self._provider_value(attr) for attr in self._PROVIDER_ENV_MAP)

        attr = self._resolve_provider_attribute(provider)
        if not attr:
            return False
        return bool(self._provider_value(attr))

    def _synchronize_langsmith_environment(self) -> None:
        """Ensure LangChain environment variables mirror CleverAgents config."""
        os.environ["LANGCHAIN_TRACING_V2"] = "true"
        api_key = self._get_langsmith_api_key()
        if api_key:
            os.environ.setdefault("LANGCHAIN_API_KEY", api_key)
        project = self._get_langsmith_project()
        if project:
            os.environ.setdefault("LANGCHAIN_PROJECT", project)
        endpoint = self._get_langsmith_endpoint()
        if endpoint:
            os.environ.setdefault("LANGCHAIN_ENDPOINT", endpoint)

    def _get_langsmith_api_key(self) -> str | None:
        return (
            self.langsmith_api_key
            or os.getenv("CLEVERAGENTS_LANGSMITH_API_KEY")
            or os.getenv("LANGSMITH_API_KEY")
            or os.getenv("LANGCHAIN_API_KEY")
        )

    def _get_langsmith_project(self) -> str | None:
        return (
            self.langsmith_project
            or os.getenv("CLEVERAGENTS_LANGSMITH_PROJECT")
            or os.getenv("LANGSMITH_PROJECT")
            or os.getenv("LANGCHAIN_PROJECT")
        )

    def _get_langsmith_endpoint(self) -> str | None:
        return (
            self.langsmith_endpoint
            or os.getenv("CLEVERAGENTS_LANGSMITH_ENDPOINT")
            or os.getenv("LANGSMITH_ENDPOINT")
            or os.getenv("LANGCHAIN_ENDPOINT")
        )

    def _get_langsmith_user_id(self) -> str | None:
        return (
            self.langsmith_user_id
            or os.getenv("CLEVERAGENTS_LANGSMITH_USER_ID")
            or os.getenv("LANGSMITH_USER_ID")
        )

    def configured_provider_names(self) -> list[str]:
        """Return sorted provider names with configured credentials."""

        providers: list[str] = []
        for attr in self._PROVIDER_ENV_MAP:
            if self._provider_value(attr):
                normalized = attr.replace("_api_key", "").replace("_token", "")
                providers.append(normalized)
        return sorted(set(providers))

    def provider_expected_env_vars(self) -> list[str]:
        """Return the flattened list of provider-related env vars."""

        env_vars: set[str] = set()
        for names in self._PROVIDER_ENV_MAP.values():
            env_vars.update(names)
        return sorted(env_vars)

    def _provider_specific_configuration(self) -> dict[str, Any]:
        """Return non-secret provider-specific configuration details."""

        return {
            "azure": {
                "api_key_present": bool(self.azure_api_key),
                "endpoint": self.azure_openai_endpoint or "",
                "api_version": self.azure_openai_api_version or "",
                "deployment": self.azure_openai_deployment or "",
            },
            "openrouter": {
                "api_key_present": bool(self.openrouter_api_key),
                "organization": self.openrouter_organization or "",
            },
        }

    def resolve_provider_defaults(self) -> ProviderDefaults:
        """Return the resolved default provider/model metadata."""

        env_provider = self._normalize_provider_name(
            os.getenv("CLEVERAGENTS_DEFAULT_PROVIDER")
        )
        provider_source = "env" if env_provider else "none"
        provider_value = env_provider

        if provider_value is None:
            configured_setting = self._normalize_provider_name(self.default_provider)
            if configured_setting:
                provider_value = configured_setting
                provider_source = "settings"

        if provider_value is None:
            configured = self.configured_provider_names()
            for candidate in self._PROVIDER_FALLBACK_ORDER:
                if candidate in configured:
                    provider_value = candidate
                    provider_source = "auto"
                    break
            if provider_value is None and configured:
                provider_value = configured[0]
                provider_source = "auto"

        env_model_raw = os.getenv("CLEVERAGENTS_DEFAULT_MODEL")
        trimmed_model = env_model_raw.strip() if env_model_raw else ""
        model_value = trimmed_model or None
        model_source = "env" if model_value else "none"

        if not model_value and self.default_model:
            trimmed_default = self.default_model.strip()
            if trimmed_default:
                model_value = trimmed_default
                model_source = "settings"
        elif not model_value and provider_value:
            model_value = self._PROVIDER_DEFAULT_MODELS.get(provider_value)
            if model_value:
                model_source = "provider-default"

        return ProviderDefaults(
            provider=provider_value,
            provider_source=provider_source,
            model=model_value,
            model_source=model_source,
        )

    def provider_configuration_diagnostics(self) -> dict[str, Any]:
        """Summarize provider configuration for user-facing diagnostics."""

        defaults = self.resolve_provider_defaults()
        configured = self.configured_provider_names()
        is_valid, errors = self.validate_provider_configuration(defaults.provider)

        diagnostics: dict[str, Any] = {
            "configured_providers": configured,
            "default_provider": defaults.provider or "",
            "default_model": defaults.model or "",
            "defaults": {
                "provider": defaults.provider or "",
                "provider_source": defaults.provider_source,
                "model": defaults.model or "",
                "model_source": defaults.model_source,
            },
            "expected_env_vars": self.provider_expected_env_vars(),
            "providers": self._provider_specific_configuration(),
            "validation_passed": is_valid,
            "validation_errors": errors,
        }
        return diagnostics

    def validate_provider_configuration(
        self, provider: str | None = None
    ) -> tuple[bool, list[str]]:
        """Validate provider configuration and return actionable errors."""

        normalized = self._normalize_provider_name(provider)
        configured = self.configured_provider_names()
        errors: list[str] = []

        if normalized is None:
            if not configured:
                errors.append(
                    "No AI providers configured. Set a provider API key such as "
                    "OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_API_KEY."
                )
            return (len(errors) == 0, errors)

        attr = self._resolve_provider_attribute(normalized)
        if attr and not self._provider_value(attr):
            env_names = ", ".join(self._PROVIDER_ENV_MAP.get(attr, ()))
            env_hint = env_names or attr.upper()
            errors.append(f"{normalized} provider requires {env_hint} to be set.")

        if normalized == "azure":
            if not self.azure_openai_endpoint:
                errors.append(
                    "Azure provider requires AZURE_OPENAI_ENDPOINT or "
                    "CLEVERAGENTS_AZURE_OPENAI_ENDPOINT."
                )
            if not self.azure_openai_deployment:
                errors.append(
                    "Azure provider requires AZURE_OPENAI_DEPLOYMENT or "
                    "CLEVERAGENTS_AZURE_OPENAI_DEPLOYMENT."
                )
        return (len(errors) == 0, errors)

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------
    def _normalize_provider_name(self, provider: str | None) -> str | None:
        """Normalize provider names and aliases to canonical registry names."""

        if not provider:
            return None
        normalized = provider.strip().lower().replace(" ", "").replace("-", "")
        if not normalized:
            return None
        attr = self._resolve_provider_attribute(normalized)
        if attr:
            return self._provider_name_from_attr(attr)
        return normalized

    def _provider_name_from_attr(self, attr: str) -> str:
        """Derive provider name from the settings attribute storing its key."""

        name = attr
        for suffix in ("_api_key", "_token"):
            if name.endswith(suffix):
                name = name[: -len(suffix)]
                break
        return name

    def _apply_external_env_overrides(self) -> None:
        """Allow provider/native env vars to override defaults."""
        for attr, env_names in self._PROVIDER_ENV_MAP.items():
            if getattr(self, attr):
                continue
            for env_name in env_names:
                value = os.getenv(env_name)
                if value:
                    setattr(self, attr, value)
                    break

    def _provider_value(self, attr: str) -> str | None:
        """Return the configured value for a provider attr (env fallback)."""
        value = getattr(self, attr, None)
        if value:
            return value
        for env_name in self._PROVIDER_ENV_MAP.get(attr, ()):  # type: ignore[arg-type]
            env_value = os.getenv(env_name)
            if env_value:
                return env_value
        return None

    def _resolve_provider_attribute(self, provider: str) -> str | None:
        """Normalize provider name to the internal attribute storing its key."""

        normalized = provider.strip().lower()
        normalized = normalized.replace(" ", "").replace("-", "")

        for suffix in ("_api_key", "apikey", "_apikey"):
            if normalized.endswith(suffix):
                normalized = normalized[: -len(suffix)]
                break

        attr = self._PROVIDER_ALIAS.get(normalized)
        if attr:
            return attr

        candidate = f"{normalized}_api_key"
        if candidate in self._PROVIDER_ENV_MAP:
            return candidate
        if normalized in self._PROVIDER_ENV_MAP:
            return normalized
        return None

    @staticmethod
    def _derive_test_database_url(url: str) -> str:
        if url.startswith("sqlite") and url.endswith(".db"):
            return url[:-3] + "_test.db"
        return url


def get_settings() -> Settings:
    """Get the application settings."""
    return Settings.get_settings()
