"""Ingest Deribit perp daily funding into the Bybit-namespaced feature store (table='bybit_features') as
feature 'deribit_funding' for the OVERLAP symbols (coins listed on BOTH venues).

The Deribit funding history is instrument-keyed (BTC-PERPETUAL, ETH-PERPETUAL, ...); the warehouse panels are
Bybit-symbol-keyed (BTCUSDT, ETHUSDT, ...), so the ingest maps instrument -> "<BASE>USDT" and writes
feature='deribit_funding' at ts = epoch_day*86400. ONLY 'deribit_funding' is written — the Bybit 'funding' /
'close' / 'spot_close' are the other ingests' job (the PK is (feature, symbol, ts) so distinct features never
collide). Idempotent (upsert overwrites in place, so re-runs are safe). Per-instrument skip-on-empty: an
instrument that returns no rows contributes nothing and never aborts the batch.

The cross-venue carry eval then reads this `deribit_funding` panel alongside the Bybit `funding` panel and
takes the per-coin/day SPREAD = bybit_funding − deribit_funding. No live paper_* tables are touched.
"""
from __future__ import annotations

import re
from typing import Any, Protocol

from fxhnt.application.bybit_concurrency import fetch_symbols_concurrently, resume_floor_ms

# Deribit perp instrument -> the Bybit USDT-perp symbol the warehouse panels key on. The default rule strips
# the "-PERPETUAL" suffix and appends "USDT" (BTC-PERPETUAL -> BTCUSDT); explicit entries pin the majors.
# IMPORTANT: only BTC/ETH trade as INVERSE "<BASE>-PERPETUAL" on Deribit; every alt (SOL, XRP, ...) is
# USDC-MARGINED "<BASE>_USDC-PERPETUAL". A bare "SOL-PERPETUAL" does NOT exist — Deribit rejects it with
# HTTP 400 "Bad Request" (the load-bearing bug that froze the deribit_funding panel + capped xvenue_carry).
# So the alt keys MUST be the "_USDC-PERPETUAL" form.
_INSTRUMENT_TO_SYMBOL = {
    "BTC-PERPETUAL": "BTCUSDT",
    "ETH-PERPETUAL": "ETHUSDT",
    "SOL_USDC-PERPETUAL": "SOLUSDT",   # USDC-margined — a bare SOL-PERPETUAL 400s on Deribit
}

# The default instrument set the nightly `deribit_funding_bars` asset ingests — the Bybit-overlap majors
# (BTC/ETH = xvenue's 2-coin deploy form, + SOL). SAME set as the manual `fxhnt ingest-deribit-funding` CLI
# default; kept here as the single source so the nightly + manual paths stay consistent. Every name MUST be a
# VALID live Deribit instrument (see _INSTRUMENT_TO_SYMBOL — SOL is the USDC-margined form).
DEFAULT_DERIBIT_INSTRUMENTS = tuple(_INSTRUMENT_TO_SYMBOL)
# Deribit perps come as inverse "<BASE>-PERPETUAL" (BTC/ETH only) AND USDC-linear "<BASE>_USDC-PERPETUAL"
# (the alts: SOL_USDC-PERPETUAL, XRP_USDC-PERPETUAL, ...). Both map to the Bybit USDT-perp "<BASE>USDT" — strip
# the optional "_USDC"/"_USDT" quote infix + "-PERPETUAL" so SOL_USDC-PERPETUAL -> SOL -> SOLUSDT.
_PERP_SUFFIX_RE = re.compile(r"(_USDC|_USDT)?-PERPETUAL$")

# Default worker count for the per-instrument fetch. DELIBERATELY lower than the Bybit ingests' 12: Deribit's
# rate limits are tighter than Bybit's shared public market limits, and there are only ~19 instruments (vs
# Bybit's ~794), so 8 already keeps the whole batch saturated in flight (~6× over the old sequential loop)
# without crowding the limit. The Deribit adapter's own exponential backoff absorbs the occasional 429.
DEFAULT_WORKERS = 8


def instrument_to_symbol(instrument: str) -> str:
    """Map a Deribit perp instrument to its Bybit USDT-perp symbol (BTC-PERPETUAL -> BTCUSDT). Falls back to
    stripping '-PERPETUAL' and appending 'USDT' for any coin not in the explicit map."""
    inst = instrument.upper()
    if inst in _INSTRUMENT_TO_SYMBOL:
        return _INSTRUMENT_TO_SYMBOL[inst]
    return f"{_PERP_SUFFIX_RE.sub('', inst)}USDT"


class _DeribitFunding(Protocol):
    def funding_history(self, instrument: str, *, start_ms: int | None = None) -> dict[int, float]: ...


def ingest_deribit_funding(store: Any, deribit: _DeribitFunding, instruments: list[str], *,
                           start_ms: int | None = None,
                           resume: bool = True,
                           workers: int = DEFAULT_WORKERS) -> dict[str, Any]:
    """Fetch + write the daily Deribit funding for each `instrument` into `store` (feature='deribit_funding',
    ts=epoch_day*86400, symbol=instrument_to_symbol(instrument)).

    Returns {"symbols": N, "day_rows": M, "oldest_day": D|None}: N = instruments that produced >=1 day-row,
    M = total day-rows written, D = the earliest epoch_day across all instruments (None if nothing ingested).
    Per-instrument skip-on-empty (an instrument with no funding history contributes nothing, never aborts).
    Writes are bulk (one transaction); the upsert is idempotent so re-running is safe and cheap.

    DATE-RESUME (`resume=True`, default): each instrument resumes FROM ITS LAST PERSISTED 'deribit_funding'
    DAY — the per-symbol last day is queried ONCE up front (`store.last_day_by_symbol('deribit_funding')`,
    keyed by the Bybit symbol) and used as the backward-pagination floor (via the shared `resume_floor_ms`),
    so an instrument already carrying funding is re-fetched only from its last day forward (the idempotent
    upsert overwrites the boundary day in place) and a fresh instrument back-fills from `start_ms`. This makes
    the nightly `deribit_funding_bars` asset incremental instead of re-paginating the full ~5y history each
    run. `resume=False` ignores the persisted days and fetches everything from `start_ms` (identical state).

    Concurrency (`workers`, default 8): the per-instrument FETCH (each paginates Deribit's funding history)
    runs across the SHARED bounded thread pool (`fetch_symbols_concurrently`, same helper the Bybit ingests
    use); each result is collected in the MAIN thread as it completes (serialized — no store thread-safety
    needed). Error isolation matches the Bybit ingests exactly: the first fetch exception surfaces to the
    caller (a bad instrument that 400s is NOT swallowed). Determinism is preserved by sorting the collected
    items by symbol before the single bulk write, so the store writes + summary are identical regardless of
    completion order."""
    collected: dict[str, list[tuple[int, dict[str, float]]]] = {}
    oldest_day: int | None = None
    day_rows = 0
    last_day = store.last_day_by_symbol("deribit_funding") if resume else {}

    def fetch_one(inst: str) -> dict[int, float]:
        # Resume from the instrument's last persisted day (keyed by its Bybit symbol); fresh -> start_ms floor.
        floor = resume_floor_ms(last_day, instrument_to_symbol(inst), start_ms)
        return deribit.funding_history(inst, start_ms=floor)

    def on_result(inst: str, daily: dict[int, float]) -> None:
        nonlocal oldest_day, day_rows
        if not daily:                                  # per-instrument skip-on-empty (never aborts the batch)
            return
        rows = [(day * 86_400, {"deribit_funding": float(val)}) for day, val in sorted(daily.items())]
        collected[instrument_to_symbol(inst)] = rows   # main-thread, serialized — no dup symbols
        day_rows += len(rows)
        run_oldest = min(daily)
        oldest_day = run_oldest if oldest_day is None else min(oldest_day, run_oldest)

    fetch_symbols_concurrently(instruments, fetch_one, workers=workers, on_result=on_result)
    if collected:
        # Sort by symbol so the bulk write is deterministic regardless of fetch-completion order.
        items = [(sym, collected[sym]) for sym in sorted(collected)]
        store.write_features_bulk(items)
    return {"symbols": len(collected), "day_rows": day_rows, "oldest_day": oldest_day}
