"""The fund-level knobs SSOT: capital + risk appetite (target_vol, kelly_fraction) as a single-row DB record,
set ONLY via the `fxhnt fund-config` CLI, read by the nightly allocation. Absent table/row → settings/policy
defaults (byte-identical to the pre-fund_config nightly). No hand-editing — the CLI is the sole writer."""
from __future__ import annotations

import datetime as dt
from dataclasses import dataclass
from typing import Any

from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool

from fxhnt.adapters.persistence.cockpit_models import CockpitBase, FundConfigRow
from fxhnt.application.allocation_policy import AllocationPolicy
from fxhnt.config import get_settings

_ROW_ID = 1


@dataclass(frozen=True)
class FundConfig:
    capital: float
    target_vol: float
    kelly_fraction: float
    max_venue_weight: float


def _defaults() -> FundConfig:
    p = AllocationPolicy()
    return FundConfig(capital=get_settings().paper_capital,
                      target_vol=p.target_vol, kelly_fraction=p.kelly_fraction, max_venue_weight=1.0)


def _engine(dsn: str):
    kw: dict[str, Any] = {"future": True}
    if dsn == "sqlite://" or (dsn.startswith("sqlite://") and ":memory:" in dsn):
        kw |= {"connect_args": {"check_same_thread": False}, "poolclass": StaticPool}
    return create_engine(dsn, **kw)


def _ensure_fund_config_columns(engine) -> None:
    """Idempotently add fund_config.max_venue_weight to an EXISTING table (mirrors
    forward_nav._ensure_summary_policy_columns): Postgres `ADD COLUMN IF NOT EXISTS`; SQLite has no such
    clause, so PRAGMA-check first. A fresh table already has the column from create_all, so this is a
    no-op there. Backfills any existing row(s) where the column is NULL to 1.0 (no-cap default) so a
    pre-existing seeded row never crashes the nightly on a None."""
    with engine.begin() as c:
        if engine.dialect.name == "postgresql":
            c.execute(text("ALTER TABLE fund_config ADD COLUMN IF NOT EXISTS max_venue_weight DOUBLE PRECISION"))
        else:
            cols = {row[1] for row in c.execute(text("PRAGMA table_info(fund_config)"))}
            if "max_venue_weight" not in cols:
                c.execute(text("ALTER TABLE fund_config ADD COLUMN max_venue_weight DOUBLE PRECISION"))
        c.execute(text("UPDATE fund_config SET max_venue_weight = 1.0 WHERE max_venue_weight IS NULL"))


def _validate(capital: float | None, target_vol: float | None, kelly_fraction: float | None,
             max_venue_weight: float | None = None) -> None:
    if capital is not None and not (capital > 0):
        raise ValueError(f"capital must be > 0, got {capital}")
    if target_vol is not None and not (0 < target_vol <= 1.0):
        raise ValueError(f"target_vol must be in (0, 1.0], got {target_vol}")
    if kelly_fraction is not None and not (0 < kelly_fraction <= 1.0):
        raise ValueError(f"kelly_fraction must be in (0, 1.0], got {kelly_fraction}")
    if max_venue_weight is not None and not (0 < max_venue_weight <= 1.0):
        raise ValueError(f"max_venue_weight must be in (0, 1.0], got {max_venue_weight}")


def read_fund_config(dsn: str) -> FundConfig:
    """Row id=1 as a FundConfig; settings/policy defaults if the table or row is absent (never raises on a
    fresh DB — the nightly must not break)."""
    try:
        eng = _engine(dsn)
        with Session(eng) as s:
            row = s.get(FundConfigRow, _ROW_ID)
    except Exception:   # noqa: BLE001 — absent table on a fresh DB → defaults
        return _defaults()
    if row is None:
        return _defaults()
    max_venue_weight = row.max_venue_weight if row.max_venue_weight is not None else 1.0
    return FundConfig(capital=row.capital, target_vol=row.target_vol, kelly_fraction=row.kelly_fraction,
                      max_venue_weight=max_venue_weight)


def write_fund_config(dsn: str, *, capital: float | None = None, target_vol: float | None = None,
                      kelly_fraction: float | None = None, max_venue_weight: float | None = None) -> FundConfig:
    """Partial upsert of row id=1: overlay only the provided fields on the current effective config, validate,
    write. Returns the new effective FundConfig. Creates the table if absent."""
    if capital is None and target_vol is None and kelly_fraction is None and max_venue_weight is None:
        raise ValueError(
            "write_fund_config: provide at least one of capital/target_vol/kelly_fraction/max_venue_weight")
    _validate(capital, target_vol, kelly_fraction, max_venue_weight)
    eng = _engine(dsn)
    CockpitBase.metadata.create_all(eng, tables=[FundConfigRow.__table__])
    _ensure_fund_config_columns(eng)
    cur = read_fund_config(dsn)
    new = FundConfig(
        capital=cur.capital if capital is None else float(capital),
        target_vol=cur.target_vol if target_vol is None else float(target_vol),
        kelly_fraction=cur.kelly_fraction if kelly_fraction is None else float(kelly_fraction),
        max_venue_weight=cur.max_venue_weight if max_venue_weight is None else float(max_venue_weight),
    )
    with Session(eng) as s:
        s.merge(FundConfigRow(id=_ROW_ID, capital=new.capital, target_vol=new.target_vol,
                              kelly_fraction=new.kelly_fraction, max_venue_weight=new.max_venue_weight,
                              updated_at=dt.datetime.now(dt.UTC).replace(tzinfo=None)))
        s.commit()
    return new


def seed_fund_config(dsn: str) -> None:
    """Insert row id=1 with the initial knobs (100k / 0.20 / 0.5 / 1.0) IF ABSENT. Never clobbers an
    operator-set value (idempotent)."""
    eng = _engine(dsn)
    CockpitBase.metadata.create_all(eng, tables=[FundConfigRow.__table__])
    _ensure_fund_config_columns(eng)
    with Session(eng) as s:
        if s.get(FundConfigRow, _ROW_ID) is not None:
            return
        s.add(FundConfigRow(id=_ROW_ID, capital=100_000.0, target_vol=0.20, kelly_fraction=0.5,
                            max_venue_weight=1.0,
                            updated_at=dt.datetime.now(dt.UTC).replace(tzinfo=None)))
        s.commit()
