"""
Central application configuration (spec §57 — every threshold/weight configurable; §67 env
management). Everything sensitive comes from the environment, never hard-coded, never committed
(.env.example documents every variable with a safe placeholder).
"""
from __future__ import annotations

from functools import lru_cache
from typing import Literal, Optional

from pydantic import model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

# AUDIT (StockLab overhaul, security audit -- JWT secret strength): the placeholder default below
# is intentionally obvious/guessable so a forgotten override is loud in dev, not silently "secure
# enough." _validate_production_safety() below refuses to start the app if this placeholder (or
# anything shorter than a reasonable minimum) reaches production -- see that validator for why.
_INSECURE_DEFAULT_JWT_SECRET = "CHANGE_ME_INSECURE_DEV_ONLY"
_MIN_PRODUCTION_JWT_SECRET_LENGTH = 32


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

    ENVIRONMENT: Literal["development", "staging", "production"] = "development"
    DEBUG: bool = False

    # --- Database ---
    DATABASE_URL: str = "postgresql+psycopg://stocklab:stocklab@localhost:5432/stocklab"

    # --- Redis ---
    REDIS_URL: str = "redis://localhost:6379/0"

    # --- Auth ---
    JWT_SECRET_KEY: str = _INSECURE_DEFAULT_JWT_SECRET
    JWT_ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
    REFRESH_TOKEN_EXPIRE_DAYS: int = 14

    # --- Data providers (see docs/DATA_SOURCES.md) ---
    PROVIDER_PRIMARY: Literal["FMP", "EODHD", "DEMO"] = "DEMO"
    PROVIDER_SECONDARY: Literal["FMP", "EODHD", "NONE"] = "NONE"
    FMP_API_KEY: Optional[str] = None
    FMP_BASE_URL: str = "https://financialmodelingprep.com/api/v3"
    EODHD_API_KEY: Optional[str] = None
    EODHD_BASE_URL: str = "https://eodhd.com/api"
    PROVIDER_REQUEST_TIMEOUT_SECONDS: float = 20.0
    PROVIDER_MAX_RETRIES: int = 3

    # --- Reporting basis (final engineering pass, Part A9 — docs/AUDIT_TTM_A9.md) ---
    # docs/AUDIT_METRICS.md cross-cutting finding #2: every metric this platform labels "TTM" is
    # really "most recent completed fiscal year", because only annual periods are ingested and
    # build_snapshot_from_db() filters on period_type == "FY". These two settings are the opt-in
    # path to a real trailing-twelve-month basis. BOTH default to the existing behaviour, so a
    # deployment that changes nothing behaves exactly as before.
    INGEST_QUARTERLY_PERIODS: bool = False
    # When true, ingestion additionally fetches period="quarter" statements and stores them as
    # Q1..Q4 FinancialPeriod rows alongside the annual ones. Costs one extra provider call per
    # statement type per security, which is why it is opt-in.
    FINANCIAL_BASIS: Literal["ANNUAL", "TTM"] = "ANNUAL"
    # "TTM" makes build_snapshot_from_db() aggregate the four most recent quarters via
    # app/engines/ttm.py for the CURRENT period. Requires INGEST_QUARTERLY_PERIODS=true to have
    # any data to aggregate; with no usable four-quarter window the snapshot falls back to the
    # annual basis and the reason is logged rather than silently swallowed.

    # --- Valuation assumptions (ASSUMPTION-tagged, never invented per company — VALUATION.md §1) ---
    DEFAULT_RISK_FREE_RATE: float = 0.045
    DEFAULT_EQUITY_RISK_PREMIUM: float = 0.045
    DEFAULT_BETA_IF_MISSING: float = 1.0
    MIN_COST_OF_DEBT: float = 0.01
    # AUDIT (StockLab overhaul Part 9/11): these four were bare literals inline in
    # app/workers/recompute.py before this pass (tax_rate=0.21, growth fallback=0.04,
    # reinvestment fallback=0.05, terminal_growth=0.025) — moved to configurable settings so
    # they're visible/overridable rather than buried in worker code, per spec §57.
    DEFAULT_CORPORATE_TAX_RATE: float = 0.21  # used only when the metrics engine's own effective/fallback tax rate is unavailable
    DEFAULT_DCF_REVENUE_GROWTH_FALLBACK: float = 0.04  # used only when the security has no 3Y revenue CAGR
    DEFAULT_DCF_REINVESTMENT_RATE_FALLBACK: float = 0.05  # used only when capex/revenue is unavailable
    DEFAULT_DCF_TERMINAL_GROWTH: float = 0.025  # capped at DEFAULT_RISK_FREE_RATE by cap_terminal_growth_at_risk_free()
    # --- DCF trajectory / scenario derivation (final engineering pass, Part B1) ---
    DCF_GROWTH_FADE: bool = True
    # When true, the near-term revenue growth rate fades linearly toward DEFAULT_DCF_TERMINAL_GROWTH
    # over DCF_GROWTH_FADE_YEARS instead of being held flat for all 10 explicit years. Holding a
    # 3Y-CAGR-derived growth rate flat for a decade is the single least defensible assumption in
    # the previous DCF; "growth fades to the terminal rate" is the standard convention.
    DCF_GROWTH_FADE_YEARS: int = 10
    DCF_SCENARIO_MODE: Literal["hybrid", "additive_pp", "multiplicative"] = "hybrid"
    # "hybrid" (default, Part B1): Bear/Bull shift by max(floor_pp, fraction x base_rate), which
    # measurably gives mature businesses a narrower fair-value band and hypergrowth a wider one.
    # "additive_pp": fixed percentage-point shifts. "multiplicative": the pre-B1 fixed-factor
    # behaviour, retained so a deployment can reproduce older valuations. The measured spreads for
    # all three are tabulated in docs/AUDIT_DCF_B1.md.

    # --- Database connection pooling (spec: PostgreSQL production optimization) ---
    DB_POOL_SIZE: int = 5
    DB_MAX_OVERFLOW: int = 10
    DB_POOL_TIMEOUT_SECONDS: int = 30
    DB_POOL_RECYCLE_SECONDS: int = 1800
    DB_POOL_PRE_PING: bool = True

    # --- Celery worker tuning (spec: Redis/Celery production review — Part 25) ---
    # Conservative defaults sized for a small single-server deployment (docs/DEPLOYMENT.md §9),
    # not a large cluster — override via env for a bigger box.
    CELERY_WORKER_CONCURRENCY: int = 2
    CELERY_WORKER_PREFETCH_MULTIPLIER: int = 1  # don't let one worker hoard tasks off the queue
    CELERY_TASK_ACKS_LATE: bool = True           # requeue a task if the worker dies mid-execution
    CELERY_TASK_TIME_LIMIT_SECONDS: int = 900     # hard kill after 15 min (a stuck task shouldn't run forever)
    CELERY_TASK_SOFT_TIME_LIMIT_SECONDS: int = 780  # soft warning 2 min before the hard limit

    # --- Scoring defaults (spec §17, overridable per screener) ---
    SCORE_WEIGHT_QUALITY: float = 0.25
    SCORE_WEIGHT_FINANCIAL_HEALTH: float = 0.20
    SCORE_WEIGHT_GROWTH: float = 0.20
    SCORE_WEIGHT_COMPETITIVE_ADVANTAGE: float = 0.15
    SCORE_WEIGHT_VALUATION: float = 0.20
    MIN_PEER_GROUP_SIZE: int = 8
    # --- Peer groups and industry reference multiples (final engineering pass, Part B2) ---
    INDUSTRY_MULTIPLE_MIN_GROUP_SIZE: int = 5
    # An industry needs at least this many companies reporting a given multiple before its median
    # is published as an INDUSTRY_MEDIAN reference. A "median P/E" from two companies is not an
    # industry reference; industries below the threshold are simply absent and the caller keeps
    # its self-historical fallback. See docs/AUDIT_PEER_GROUPS_B2.md.
    MULTIPLES_REFERENCE_PREFERENCE: Literal["industry_then_self", "self_then_industry", "self_only"] = "industry_then_self"
    # Which reference anchors a multiples fair value when both are available. "self_only"
    # reproduces the pre-B2 behaviour exactly.

    # --- Margin of safety defaults (VALUATION.md §5) ---
    DEFAULT_STRONG_BUY_MOS: float = 0.35
    DEFAULT_BUY_MOS: float = 0.20
    DEFAULT_OVERVALUED_PREMIUM: float = 0.15

    # --- Rate limiting (SECURITY.md) ---
    RATE_LIMIT_PER_MINUTE: int = 120
    # AUDIT FIX (StockLab overhaul, final engineering pass, Part A3): three more, deliberately
    # separate from the general default above and from each other -- each protects a route with a
    # genuinely different risk/benefit shape (docs/AUDIT_SECURITY_A3.md has the full reasoning for
    # every route, including the ones left undecorated). All three configurable per spec §57, same
    # as RATE_LIMIT_PER_MINUTE.
    SCREENER_RATE_LIMIT_PER_MINUTE: int = 30
    # Tighter than the general default: /v1/screeners/run is unauthenticated, and unlike a fixed
    # read its cost scales with the number of filters a caller supplies (each ScreenFilter compiles
    # to its own correlated EXISTS subquery, app/engines/screening/executor.py) -- a small number of
    # concurrent callers with maximal filter lists is a real DoS-shaped risk in a way a simple GET
    # isn't.
    SEARCH_RATE_LIMIT_PER_MINUTE: int = 30
    # Same tier as screener, same reason: /v1/search is unauthenticated and its `ILIKE '%q%'`
    # leading-wildcard pattern (app/api/v1/search.py) cannot use a plain btree index -- a real,
    # already-documented scan-cost concern (docs/AUDIT_PERFORMANCE.md finding #4) that a rate limit
    # mitigates until pg_trgm is adopted.
    WATCHLIST_WRITE_RATE_LIMIT_PER_MINUTE: int = 60
    # Looser than screener/search: watchlist add/remove is authenticated (get_current_user) and
    # scoped to the caller's own rows only -- the risk is DB write/commit churn from a scripted
    # caller, not an unauthenticated amplification vector, so a generous limit is enough.

    # --- CORS ---
    CORS_ALLOWED_ORIGINS: str = "http://localhost:3000"

    # --- Screener cache ---
    SCREENER_CACHE_TTL_SECONDS: int = 900

    @property
    def cors_origins_list(self) -> list[str]:
        return [o.strip() for o in self.CORS_ALLOWED_ORIGINS.split(",") if o.strip()]

    @model_validator(mode="after")
    def _validate_production_safety(self) -> "Settings":
        """AUDIT FIX (StockLab overhaul, security audit): before this pass, ENVIRONMENT=production
        with a forgotten/default JWT_SECRET_KEY started successfully and silently signed tokens
        with a public, well-known string -- anyone could forge a valid access token for any user
        ID. This is a fail-closed startup check, not a runtime request check: it raises once, at
        process start, rather than degrading security silently. Only applies when
        ENVIRONMENT == "production" -- development/staging keep the friendly placeholder so local
        setup doesn't require generating a real secret first."""
        if self.ENVIRONMENT == "production":
            if self.JWT_SECRET_KEY == _INSECURE_DEFAULT_JWT_SECRET:
                raise ValueError(
                    "JWT_SECRET_KEY is still the insecure default placeholder. Generate a real "
                    "secret before running in production, e.g.: "
                    "python3 -c \"import secrets; print(secrets.token_urlsafe(48))\""
                )
            if len(self.JWT_SECRET_KEY) < _MIN_PRODUCTION_JWT_SECRET_LENGTH:
                raise ValueError(
                    f"JWT_SECRET_KEY is only {len(self.JWT_SECRET_KEY)} characters -- production "
                    f"requires at least {_MIN_PRODUCTION_JWT_SECRET_LENGTH}. Generate a real "
                    "secret, e.g.: python3 -c \"import secrets; print(secrets.token_urlsafe(48))\""
                )
            if self.DEBUG:
                raise ValueError(
                    "DEBUG=true is not allowed when ENVIRONMENT=production (would risk verbose "
                    "error responses). Set DEBUG=false."
                )
        return self


@lru_cache
def get_settings() -> Settings:
    return Settings()
