"""
ResearchContext: read-only view of one Theta research session row plus chain caches.

This module exists so each strategy module can type-hint ``ctx: ResearchContext`` without
importing sibling strategy code. **No trading logic lives here** — only accessors and
bounds checks.
"""
from __future__ import annotations

import math
from collections.abc import Callable
from typing import Any

import pandas as pd

from RenTech.core.options_data_loader import OptionChain


class ResearchContext:
    """
    Per-session index ``i`` aligns with ``days[i]`` and ``panel.iloc[i]`` after the runner
    builds ``panel`` on the same ``days`` ordering returned by
    :func:`RenTech.strategy_stack.research_literature_theta_strategies.prepare_theta_research_context`.
    """

    __slots__ = (
        "days",
        "panel",
        "get_chain",
        "iv_atm",
        "skew_put_minus_call_iv",
        "n_contracts",
    )

    def __init__(
        self,
        days: list[pd.Timestamp],
        panel: pd.DataFrame,
        get_chain: Callable[[pd.Timestamp], OptionChain],
        iv_atm: dict[tuple[int, int], float | None],
        skew_put_minus_call_iv: dict[tuple[int, int, float, float], float | None],
        n_contracts: list[int],
    ) -> None:
        self.days = days
        self.panel = panel
        self.get_chain = get_chain
        self.iv_atm = iv_atm
        self.skew_put_minus_call_iv = skew_put_minus_call_iv
        self.n_contracts = n_contracts

    def day_count(self) -> int:
        return len(self.days)

    def row(self, i: int) -> pd.Series:
        return self.panel.iloc[int(i)]

    def spy_close(self, i: int) -> float:
        return float(self.row(i)["close"])

    def iv_atm_dte(self, i: int, dte: int) -> float | None:
        v = self.iv_atm.get((int(i), int(dte)))
        if v is None:
            return None
        if not isinstance(v, (int, float)) or not math.isfinite(float(v)) or float(v) <= 0:
            return None
        return float(v)

    def skew_iv_diff(self, i: int, dte: int, put_delta: float, call_delta: float) -> float | None:
        v = self.skew_put_minus_call_iv.get((int(i), int(dte), float(put_delta), float(call_delta)))
        if v is None:
            return None
        if not isinstance(v, (int, float)) or not math.isfinite(float(v)):
            return None
        return float(v)

    def chain_contracts(self, i: int) -> int:
        if i < 0 or i >= len(self.n_contracts):
            return 0
        return int(self.n_contracts[i])

    def vix(self, i: int) -> float:
        r = self.row(i)
        x = r.get("vix_close")
        if x is None or (isinstance(x, float) and not math.isfinite(x)):
            return float("nan")
        return float(x)

    def vvix_over_vix(self, i: int) -> float | None:
        r = self.row(i)
        vv = r.get("vvix_close")
        vx = r.get("vix_close")
        if vv is None or vx is None:
            return None
        if pd.isna(vv) or pd.isna(vx):
            return None
        fvv, fvx = float(vv), float(vx)
        if not math.isfinite(fvv) or not math.isfinite(fvx) or fvx <= 0:
            return None
        return fvv / fvx

    def optional_float(self, i: int, col: str) -> float | None:
        r = self.row(i)
        if col not in r.index:
            return None
        x = r[col]
        if x is None or (isinstance(x, float) and not math.isfinite(x)) or pd.isna(x):
            return None
        return float(x)

    def chain(self, i: int) -> OptionChain:
        return self.get_chain(self.days[i])
