"""Central configuration — env-driven, no secrets in code (pydantic-settings).

Every layer reads its config from here; nothing hardcodes a connection string or threshold.
Override via environment variables prefixed FXHNT_ (e.g. FXHNT_OPERATIONAL_DSN=postgresql+psycopg://...).
"""
from __future__ import annotations

from functools import lru_cache
from pathlib import Path

from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict

_DATA_DIR = Path.home() / ".fxhnt"


class GauntletSettings(BaseSettings):
    """Validation thresholds — the anti-overfitting bar. Conservative by design."""
    model_config = SettingsConfigDict(env_prefix="FXHNT_GAUNTLET_")
    dsr_min: float = 0.95            # deflated-Sharpe floor (accounts for the full search)
    oos_min_sharpe: float = 0.0
    max_is_oos_decay: float = 0.50   # OOS Sharpe must hold ≥ (1-this) × IS
    oos_fraction: float = 0.40       # holdout fraction


class ExecutionSettings(BaseSettings):
    """Multi-strategy execution safety policy (hard-won from live paper trading)."""
    model_config = SettingsConfigDict(env_prefix="FXHNT_EXEC_")
    hysteresis: float = 0.03       # only trade a market if target $ drifts > this × NLV (no churn)
    entry_floor: float = 0.005     # entry-from-zero floor (× NLV) — smaller than hysteresis
    data_tol: float = 0.25         # refuse to trade if broker summary vs positions disagree > this
    allow_live: bool = False       # must be explicitly true to trade a non-paper (live) account


class IbkrSettings(BaseSettings):
    """IB Gateway/TWS connection. Pod-to-pod must use the socat-bridged port (4004), not 4002."""
    model_config = SettingsConfigDict(env_prefix="FXHNT_IBKR_")
    host: str = "127.0.0.1"
    port: int = 4002              # local port-forward (4002) or in-cluster socat (4004)
    client_id: int = 17
    timeout: int = 15


class DatabentoSettings(BaseSettings):
    """Databento data access. The cost guard bakes in a hard-learned lesson: ALWAYS estimate cost and
    refuse any fetch above the cap before downloading (a runaway download once cost $3.5k)."""
    model_config = SettingsConfigDict(env_prefix="FXHNT_DATABENTO_")
    max_cost_usd: float = 0.50        # refuse any single fetch estimated above this
    equity_dataset: str = "DBEQ.BASIC"
    future_dataset: str = "GLBX.MDP3"
    default_start: str = "2018-05-01"
    fetch_years: int = 2              # rolling window for the forward-tracking futures fetch: only the recent
                                      # ~fetch_years of bars (>>250d signal lookback), so we don't re-pull 16y
                                      # of all-expiry history weekly — ~90% cheaper, identical forward signals


class BinanceSettings(BaseSettings):
    """Binance USDⓈ-M perp execution. SAFE BY DEFAULT: testnet on, live off, kill-switch off.
    Live trading needs testnet=false AND allow_live=true AND kill_switch=false (three independent gates)."""
    model_config = SettingsConfigDict(env_prefix="FXHNT_BINANCE_")
    api_key: str = ""                 # from env / k8s secret; never logged
    api_secret: str = ""
    testnet: bool = True              # ccxt set_sandbox_mode
    allow_live: bool = False          # must be explicitly true to trade real money
    kill_switch: bool = False         # halt: no new opens
    leverage: int = 3                 # conservative; per-symbol set_leverage
    gross_cap: float = 1.0            # max gross exposure as a multiple of NLV
    per_symbol_cap: float = 0.05      # max single-symbol notional as a fraction of NLV
    participation_rate: float = 0.05  # max order notional as a fraction of symbol ADV (USD)
    depth_cap: float = 0.25           # max order notional as a fraction of top-of-book depth (USD)
    max_slippage_bps: float = 25.0    # abort a symbol if fill drifts more than this vs arrival mid
    hysteresis: float = 0.003         # skip a rebalance delta smaller than this × NLV
    kelly_cap: float = 0.25           # cap on the Kelly fraction
    maker_wait_s: float = 5.0         # how long to wait for the post-only maker fill before crossing


class AgentSettings(BaseSettings):
    """The research agent's LLM. Provider/model are swappable (local Ollama now; cluster/API later)."""
    model_config = SettingsConfigDict(env_prefix="FXHNT_AGENT_")
    provider: str = "ollama"                       # ollama | openai
    model: str = "qwen2.5:3b"
    base_url: str = "http://localhost:11434"
    temperature: float = 0.2
    recursion_limit: int = 40                      # max agent reasoning/tool steps


class FactorySettings(BaseSettings):
    """Ongoing edge-factory: diversification gate + governance. Conservative by design."""
    model_config = SettingsConfigDict(env_prefix="FXHNT_FACTORY_")
    max_tail_corr: float = 0.5         # admit a sleeve only if its LEFT-TAIL corr to the book is below this
    min_marginal_sharpe: float = 0.05  # ...and it adds at least this much to book Sharpe
    tail_frac: float = 0.10            # "left tail" = the book's worst 10% of days
    min_forward_days: int = 20         # forward observations before a sleeve can promote
    max_deployed_sleeves: int = 20     # cap on the deployed book size
    max_new_deploys_per_cycle: int = 2  # a burst can't over-commit capital in one cycle
    kill_switch: bool = False          # halt all new promotions


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="FXHNT_", env_file=".env", extra="ignore")

    # Operational store (relational: strategies, runs, verdicts, positions, trades).
    # SQLite for dev/test; set to a postgresql+psycopg DSN in production.
    operational_dsn: str = Field(default=f"sqlite:///{_DATA_DIR / 'operational.db'}")
    # Analytical store (columnar: market data, backtest timeseries) — DuckDB embedded file.
    analytical_path: str = Field(default=str(_DATA_DIR / "analytical.duckdb"))
    # Warehouse (point-in-time SSOT: bars, funding, microstructure features) — DuckDB embedded file.
    warehouse_path: str = Field(default=str(_DATA_DIR / "warehouse.duckdb"))
    # Combined-book data path: "raw" (crypto_pit/.dbn files) or "warehouse" (SSOT). Default raw for safety;
    # the B0 Dagster deploy sets FXHNT_COMBINED_BOOK_DATA_SOURCE=warehouse.
    combined_book_data_source: str = Field(default="raw")

    cost_bps_per_turnover: float = 10.0   # round-trip cost model (bps of traded notional)
    gauntlet: GauntletSettings = Field(default_factory=GauntletSettings)
    execution: ExecutionSettings = Field(default_factory=ExecutionSettings)
    ibkr: IbkrSettings = Field(default_factory=IbkrSettings)
    databento: DatabentoSettings = Field(default_factory=DatabentoSettings)
    agent: AgentSettings = Field(default_factory=AgentSettings)
    binance: BinanceSettings = Field(default_factory=BinanceSettings)
    factory: FactorySettings = Field(default_factory=FactorySettings)

    def ensure_dirs(self) -> None:
        _DATA_DIR.mkdir(parents=True, exist_ok=True)
        Path(self.analytical_path).parent.mkdir(parents=True, exist_ok=True)
        Path(self.warehouse_path).parent.mkdir(parents=True, exist_ok=True)


@lru_cache
def get_settings() -> Settings:
    s = Settings()
    s.ensure_dirs()
    return s
