"""Repository for the D2.1 REAL DU-paper-account capture: the per-run `ibkr_account_nav` snapshot (NLV/cash/
gross/positions) plus strategy-tagged reads of the (pre-existing, previously-empty) `exec_fills` table.
Mirrors `ForwardNavRepo`'s engine/StaticPool construction and its guarded idempotent ALTER pattern
(`_ensure_summary_policy_columns`) — `migrate()` never runs heavy DDL on every startup."""
from __future__ import annotations

import datetime as dt
import json
import uuid
from typing import Any

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

from fxhnt.adapters.persistence.cockpit_models import CockpitBase, ExecFillRow, IbkrAccountNavRow
from fxhnt.application.exec_capture import FillDTO


class IbkrAccountRepo:
    def __init__(self, dsn: str) -> None:
        # StaticPool keeps a single in-memory SQLite connection alive across sessions; only applied for the
        # in-memory SQLite case (tests). StaticPool is 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)
        self._is_pg = self._engine.dialect.name == "postgresql"

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

    def _ensure_exec_fills_strategy_column(self) -> None:
        """Idempotently add `exec_fills.strategy_id` to a table that pre-dates this task (`create_all` does
        NOT add columns to an already-existing table on prod Postgres). Postgres has `ADD COLUMN IF NOT
        EXISTS`; sqlite does not, so PRAGMA-check the columns first. A brand-new table already has the
        column from `create_all` — this is then a no-op on both dialects."""
        with self._engine.begin() as c:
            if self._is_pg:
                c.execute(text(
                    "ALTER TABLE exec_fills ADD COLUMN IF NOT EXISTS strategy_id VARCHAR(64) DEFAULT ''"))
            else:
                cols = {row[1] for row in c.execute(text("PRAGMA table_info(exec_fills)"))}
                if "strategy_id" not in cols:
                    c.execute(text(
                        "ALTER TABLE exec_fills ADD COLUMN strategy_id VARCHAR(64) DEFAULT ''"))

    def replace_snapshot(self, run_date: str, *, nlv: float, cash: float, gross: float,
                         positions: dict[str, float], at: dt.datetime) -> None:
        """Idempotent per run_date: delete-then-insert in one transaction, so a same-day re-run overwrites
        rather than double-books (mirrors `ForwardNavRepo.replace_nav`)."""
        rd = dt.date.fromisoformat(run_date)
        positions_json = json.dumps({str(k): float(v) for k, v in positions.items()})
        with Session(self._engine) as s:
            s.query(IbkrAccountNavRow).filter(IbkrAccountNavRow.run_date == rd).delete()
            s.add(IbkrAccountNavRow(run_date=rd, nlv=float(nlv), cash=float(cash), gross=float(gross),
                                    positions_json=positions_json, at=at))
            s.commit()

    def nav_series(self) -> list[tuple[str, float]]:
        """The account's (date, NLV) series ascending — the real-account equivalent of `ForwardNavRepo`'s
        nav history."""
        with Session(self._engine) as s:
            rows = s.scalars(select(IbkrAccountNavRow).order_by(IbkrAccountNavRow.run_date.asc()))
            return [(r.run_date.isoformat(), r.nlv) for r in rows]

    def latest_positions(self) -> dict[str, float]:
        """The most recently snapshotted real positions, or {} if none captured yet."""
        with Session(self._engine) as s:
            row = s.scalars(select(IbkrAccountNavRow)
                            .order_by(IbkrAccountNavRow.run_date.desc()).limit(1)).first()
            if row is None:
                return {}
            return {str(k): float(v) for k, v in json.loads(row.positions_json).items()}

    def record_fills(self, *, strategy_id: str, rebalance_id: str, fills: list[FillDTO],
                     at: dt.datetime) -> None:
        """Append each captured fill as an `exec_fills` row tagged `strategy_id`. `client_id` (the table's
        PK) is synthesized per fill since `FillDTO` carries no execution id of its own."""
        with Session(self._engine) as s:
            for f in fills:
                s.add(ExecFillRow(client_id=str(uuid.uuid4()), rebalance_id=rebalance_id,
                                  strategy_id=strategy_id, symbol=f.symbol, side=f.side, qty=f.qty,
                                  price=f.price, fee=f.fee, status=f.status,
                                  shortfall_bps=f.shortfall_bps, at=at))
            s.commit()

    def fills_for(self, strategy_id: str, limit: int = 20) -> list[ExecFillRow]:
        """The most recent `limit` fills tagged `strategy_id`, newest first. Read-only."""
        stmt = (select(ExecFillRow).where(ExecFillRow.strategy_id == strategy_id)
                .order_by(ExecFillRow.at.desc()).limit(limit))
        with Session(self._engine) as s:
            return list(s.scalars(stmt))
