"""Repository for the Bybit testnet real-paper leg's NAV + fills tables (design §4.4). Mirrors
`ForwardNavRepo`'s engine/StaticPool construction so tests run against in-memory SQLite without Postgres.
`migrate()` creates the tables via the ORM (CREATE TABLE IF NOT EXISTS semantics). NAV is idempotent per
run_date (a re-run overwrites); fills are append-only and read back per run_date. `testnet_executed_on` is the
per-day idempotency marker the reconcile asset checks before placing any order."""
from __future__ import annotations

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

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

from fxhnt.adapters.persistence.cockpit_models import (
    BybitTestnetFillRow,
    BybitTestnetNavRow,
    CockpitBase,
)


@dataclass(frozen=True)
class TestnetNav:
    """One post-rebalance testnet NAV sample (one run_date). `run_date` is an ISO string for UI/serialization."""
    __test__ = False  # not a pytest test class (name starts with "Test")
    run_date: str
    equity: float
    realized_pnl: float
    gross: float
    n_orders: int
    mean_exec_slippage_bps: float
    mean_timing_drift_bps: float


@dataclass(frozen=True)
class TestnetFill:
    """A recorded testnet fill with decomposed slippage. `run_date` is an ISO string; the slippage fields are
    None for a low-confidence (thin-book) fill."""
    __test__ = False  # not a pytest test class (name starts with "Test")
    run_date: str
    symbol: str
    side: str
    qty: float
    fill_price: float
    mid_at_order: float | None
    book_mark: float | None
    exec_slippage_bps: float | None
    timing_drift_bps: float | None
    assumed_tier_bps: float
    slippage_delta_bps: float | None
    low_confidence: bool
    fee: float
    order_id: str
    ts: dt.datetime


class BybitTestnetRepo:
    def __init__(self, dsn: str) -> None:
        # StaticPool keeps a single in-memory SQLite connection alive across sessions; only for the in-memory
        # SQLite case (tests). NOT used for Postgres.
        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}
        self._engine = create_engine(dsn, **kw)

    def migrate(self) -> None:
        CockpitBase.metadata.create_all(self._engine)

    # ---- bybit_testnet_nav -------------------------------------------------------------------------
    def upsert_testnet_nav(self, run_date: str, *, equity: float, realized_pnl: float, gross: float,
                           n_orders: int, mean_exec_slippage_bps: float, mean_timing_drift_bps: float,
                           ts: dt.datetime) -> None:
        """Idempotent per run_date: write (or overwrite) that date's NAV row — a re-run replaces it."""
        rd = dt.date.fromisoformat(run_date)
        with Session(self._engine) as s:
            s.merge(BybitTestnetNavRow(
                run_date=rd, equity=float(equity), realized_pnl=float(realized_pnl), gross=float(gross),
                n_orders=int(n_orders), mean_exec_slippage_bps=float(mean_exec_slippage_bps),
                mean_timing_drift_bps=float(mean_timing_drift_bps), ts=ts))
            s.commit()

    def read_testnet_nav(self) -> list[TestnetNav]:
        """The testnet NAV series ascending by run_date (one row per date)."""
        with Session(self._engine) as s:
            rows = s.scalars(select(BybitTestnetNavRow).order_by(BybitTestnetNavRow.run_date.asc()))
            return [TestnetNav(
                run_date=r.run_date.isoformat(), equity=r.equity, realized_pnl=r.realized_pnl,
                gross=r.gross, n_orders=r.n_orders, mean_exec_slippage_bps=r.mean_exec_slippage_bps,
                mean_timing_drift_bps=r.mean_timing_drift_bps) for r in rows]

    def latest_testnet_nav(self) -> TestnetNav | None:
        """The most recent testnet NAV row (by run_date), or None if the table is empty."""
        with Session(self._engine) as s:
            r = s.scalars(select(BybitTestnetNavRow).order_by(BybitTestnetNavRow.run_date.desc()).limit(1)).first()
            if r is None:
                return None
            return TestnetNav(
                run_date=r.run_date.isoformat(), equity=r.equity, realized_pnl=r.realized_pnl,
                gross=r.gross, n_orders=r.n_orders, mean_exec_slippage_bps=r.mean_exec_slippage_bps,
                mean_timing_drift_bps=r.mean_timing_drift_bps)

    def testnet_executed_on(self, run_date: str) -> bool:
        """True iff a NAV row exists for `run_date` — the per-day idempotency marker (a second run is a no-op)."""
        rd = dt.date.fromisoformat(run_date)
        with Session(self._engine) as s:
            return s.get(BybitTestnetNavRow, rd) is not None

    # ---- bybit_testnet_fills -----------------------------------------------------------------------
    def insert_testnet_fills(self, fills: list[TestnetFill]) -> None:
        """Append the supplied fills (append-only ledger)."""
        with Session(self._engine) as s:
            for f in fills:
                s.add(BybitTestnetFillRow(
                    run_date=dt.date.fromisoformat(f.run_date), symbol=f.symbol, side=f.side, qty=f.qty,
                    fill_price=f.fill_price, mid_at_order=f.mid_at_order, book_mark=f.book_mark,
                    exec_slippage_bps=f.exec_slippage_bps, timing_drift_bps=f.timing_drift_bps,
                    assumed_tier_bps=f.assumed_tier_bps, slippage_delta_bps=f.slippage_delta_bps,
                    low_confidence=f.low_confidence, fee=f.fee, order_id=f.order_id, ts=f.ts))
            s.commit()

    def read_testnet_fills(self, run_date: str) -> list[TestnetFill]:
        """The fills recorded for exactly `run_date`, in insertion order."""
        rd = dt.date.fromisoformat(run_date)
        with Session(self._engine) as s:
            rows = s.scalars(select(BybitTestnetFillRow)
                             .where(BybitTestnetFillRow.run_date == rd)
                             .order_by(BybitTestnetFillRow.id.asc()))
            return [TestnetFill(
                run_date=r.run_date.isoformat(), symbol=r.symbol, side=r.side, qty=r.qty,
                fill_price=r.fill_price, mid_at_order=r.mid_at_order, book_mark=r.book_mark,
                exec_slippage_bps=r.exec_slippage_bps, timing_drift_bps=r.timing_drift_bps,
                assumed_tier_bps=r.assumed_tier_bps, slippage_delta_bps=r.slippage_delta_bps,
                low_confidence=r.low_confidence, fee=r.fee, order_id=r.order_id, ts=r.ts) for r in rows]
