"""Full-chain *research* option collector (§13-bis).

Diverso dal collettore operativo (`option_chain_snapshot_cycle`):

* finestra scadenze ``[now, now + expiry_max_days]`` — cattura TUTTE le
  scadenze liquide (1g/1sett/2sett/1mese/3mesi), non solo quella nella
  finestra DTE della strategia;
* entrambe le ali (nessun filtro moneyness di default), oppure entro
  ``±moneyness_band_pct`` se configurato;
* popola ``book_depth_top3`` chiamando l'orderbook per ogni strumento
  tenuto (1 call/strumento, concorrenza limitata da
  ``book_depth_concurrency``) — così lo slippage reale è modellabile;
* scrive con ``source='research'`` per non confondersi con le righe
  'live'.

Questo trasforma il dataset da "skew/premi medi" a backtest opzioni
vero, per-trade e standing. Best-effort come l'altro collettore: un
batch o un orderbook che falliscono non invalidano il resto.
"""

from __future__ import annotations

import asyncio
import logging
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from typing import TYPE_CHECKING, Any

from cerbero_bite.state import connect, transaction
from cerbero_bite.state.models import OptionChainQuoteRecord

from cerbero_bite.runtime.option_chain_snapshot_cycle import (
    DEFAULT_BATCH_SIZE,
    _fetch_tickers_in_batches,
    _to_decimal_or_none,
)

if TYPE_CHECKING:
    from cerbero_bite.runtime.dependencies import RuntimeContext

__all__ = ["collect_option_chain_research"]

_log = logging.getLogger("cerbero_bite.runtime.option_chain_research")


def _underlying_price(ticker: dict[str, Any]) -> Decimal | None:
    """Spot/index dell'underlying dal ticker, per il filtro moneyness."""
    for key in ("underlying_price", "index_price", "estimated_delivery_price"):
        val = _to_decimal_or_none(ticker.get(key))
        if val is not None and val > 0:
            return val
    return None


async def _depth_for(
    ctx: RuntimeContext, name: str, sem: asyncio.Semaphore
) -> int | None:
    """Best-effort top-3 book depth per ``name`` (None se fallisce)."""
    async with sem:
        try:
            return await ctx.deribit.orderbook_depth_top3(name)
        except Exception as exc:
            _log.debug("orderbook_depth_top3 failed for %s: %s", name, exc)
            return None


async def collect_option_chain_research(
    ctx: RuntimeContext,
    *,
    asset: str = "ETH",
    now: datetime | None = None,
    batch_size: int = DEFAULT_BATCH_SIZE,
) -> int:
    """Collect + persist un singolo snapshot full-chain ``research`` per
    ``asset``. Ritorna il numero di quote persistiti (0 su fallimento
    best-effort o se il collettore è disabilitato)."""
    rc = getattr(ctx.cfg, "research_collector", None)
    if rc is None or not rc.enabled:
        return 0

    when = (now or datetime.now(UTC)).astimezone(UTC)

    expiry_from = when
    expiry_to = when + timedelta(days=rc.expiry_max_days)

    try:
        chain = await ctx.deribit.options_chain(
            currency=asset.upper(),
            expiry_from=expiry_from,
            expiry_to=expiry_to,
            min_open_interest=int(rc.open_interest_min),
        )
    except Exception:
        _log.exception("research option chain fetch failed")
        return 0

    if not chain:
        _log.info("research option chain empty for %s in window", asset)
        return 0

    names = [meta.name for meta in chain]
    tickers = await _fetch_tickers_in_batches(ctx, names, batch_size=batch_size)

    band = rc.moneyness_band_pct  # Decimal | None

    # 1) costruisci i quote, applicando l'eventuale filtro moneyness.
    kept: list[tuple[OptionChainQuoteRecord, dict[str, Any] | None]] = []
    for meta in chain:
        ticker = tickers.get(meta.name)

        if band is not None and ticker is not None:
            spot = _underlying_price(ticker)
            if spot is not None:
                moneyness = abs(meta.strike - spot) / spot
                if moneyness > band:
                    continue  # fuori dall'ala richiesta

        if ticker is None:
            rec = OptionChainQuoteRecord(
                timestamp=when,
                asset=asset.upper(),
                instrument_name=meta.name,
                strike=meta.strike,
                expiry=meta.expiry,
                option_type=meta.option_type,
                open_interest=int(meta.open_interest)
                if meta.open_interest is not None
                else None,
                source="research",
            )
            kept.append((rec, None))
            continue

        greeks = ticker.get("greeks") or {}
        rec = OptionChainQuoteRecord(
            timestamp=when,
            asset=asset.upper(),
            instrument_name=meta.name,
            strike=meta.strike,
            expiry=meta.expiry,
            option_type=meta.option_type,
            bid=_to_decimal_or_none(ticker.get("bid")),
            ask=_to_decimal_or_none(ticker.get("ask")),
            mid=_to_decimal_or_none(ticker.get("mark_price")),
            iv=_to_decimal_or_none(ticker.get("mark_iv")),
            delta=_to_decimal_or_none(greeks.get("delta")),
            gamma=_to_decimal_or_none(greeks.get("gamma")),
            theta=_to_decimal_or_none(greeks.get("theta")),
            vega=_to_decimal_or_none(greeks.get("vega")),
            open_interest=int(meta.open_interest)
            if meta.open_interest is not None
            else None,
            volume_24h=(
                int(ticker["volume_24h"])
                if ticker.get("volume_24h") is not None
                else None
            ),
            source="research",
        )
        kept.append((rec, ticker))

    # 2) popola book_depth_top3 (concorrenza limitata) sugli strumenti tenuti.
    if rc.fetch_book_depth and kept:
        sem = asyncio.Semaphore(max(1, int(rc.book_depth_concurrency)))
        depths = await asyncio.gather(
            *(_depth_for(ctx, rec.instrument_name, sem) for rec, _ in kept)
        )
        kept = [
            (rec.model_copy(update={"book_depth_top3": depth}), tk)
            for (rec, tk), depth in zip(kept, depths, strict=True)
        ]

    quotes = [rec for rec, _ in kept]

    persisted = 0
    try:
        conn = connect(ctx.db_path)
        try:
            with transaction(conn):
                persisted = ctx.repository.record_option_chain_snapshot(conn, quotes)
        finally:
            conn.close()
    except Exception:
        _log.exception("persist research option chain snapshot failed")
        return 0

    _log.info(
        "option_chain_research persisted %d quote(s) for %s (%d expiries window<=%dd)",
        persisted,
        asset.upper(),
        len({q.expiry for q in quotes}),
        rc.expiry_max_days,
    )
    return persisted
