"""
Lightweight **research** backtests for options trade ideas using EOD chain data
from :mod:`RenTech.core.options_data_loader`.

This is not execution — it marks P&amp;L from historical bid/ask/mid on a schedule
you choose (e.g. roll every *N* trading days into a target-DTE / target-delta leg).

Limitations (explicit)
----------------------
* One leg at a time; no spreads, no commissions, no borrow, no assignment.
* Marks use that day’s chain only; if the exact strike/expiry row is missing on exit
  day, the trade is skipped with a warning (no interpolation).
* Delta targets are as reported in the vendor file (often Black–Scholes); not live IV.
"""

from __future__ import annotations

import math
import warnings
from dataclasses import dataclass
from enum import Enum
from typing import Iterable

import pandas as pd

from RenTech.core.options_data_loader import IVolatilityLoader, OptionChain, OptionContract


class ExecutionAssumption(str, Enum):
    """How to translate bid/ask/mid into fill prices for a **long** option."""

    MID = "mid"  # buy and sell at mid (optimistic vs crossing spread)
    REALISTIC = "realistic"  # long: pay ask on entry, sell bid on exit


def _norm_ts(x: pd.Timestamp | object) -> pd.Timestamp:
    return pd.Timestamp(x).normalize()


def find_contract_in_chain(
    chain: OptionChain,
    expiration: pd.Timestamp,
    strike: float,
    option_type: str,
) -> OptionContract | None:
    """Locate the same series by (expiration, strike, C/P)."""
    exp_n = _norm_ts(expiration)
    want = strike
    ot = option_type.strip().upper()[:1]
    if ot == "C" or option_type.upper().startswith("CALL"):
        ot = "C"
    elif ot == "P" or option_type.upper().startswith("PUT"):
        ot = "P"
    for c in chain.contracts:
        if c.option_type != ot:
            continue
        if _norm_ts(c.expiration) != exp_n:
            continue
        if math.isclose(float(c.strike), float(want), rel_tol=0, abs_tol=1e-4):
            return c
    return None


def _long_entry_price(c: OptionContract, how: ExecutionAssumption) -> float:
    if how == ExecutionAssumption.MID:
        return float(c.mid)
    return float(c.ask)


def _long_exit_price(c: OptionContract, how: ExecutionAssumption) -> float:
    if how == ExecutionAssumption.MID:
        return float(c.mid)
    return float(c.bid)


@dataclass
class TradeRecord:
    entry_date: pd.Timestamp
    exit_date: pd.Timestamp
    expiration: pd.Timestamp
    strike: float
    option_type: str
    entry_px: float
    exit_px: float
    pnl_dollars: float
    pnl_pct: float
    entry_delta: float
    entry_iv: float


def run_rebalance_long_backtest(
    loader: IVolatilityLoader,
    *,
    trading_dates: Iterable[pd.Timestamp] | None = None,
    rebalance_every: int = 5,
    target_dte: int = 30,
    target_delta: float = -0.10,
    option_type: str = "P",
    execution: ExecutionAssumption = ExecutionAssumption.MID,
    start: pd.Timestamp | None = None,
    end: pd.Timestamp | None = None,
) -> tuple[pd.DataFrame, pd.Series]:
    """
    Open a single long leg on every *rebalance_every*-th trading day; close the prior
    leg on the same day (same chain) before opening the new one.

    P&amp;L per round-trip (one contract): ``(exit_px - entry_px) * 100``.

    Parameters
    ----------
    trading_dates
        If ``None``, uses all dates in the Parquet file (sorted), optionally clipped
        by ``start`` / ``end``.
    rebalance_every
        1 = trade every listed trading day; 5 = every 5th chain date in the list.
    """
    if rebalance_every < 1:
        raise ValueError("rebalance_every must be >= 1")

    if trading_dates is None:
        dates = list(loader.iter_chain_dates())
    else:
        dates = sorted({_norm_ts(d) for d in trading_dates})

    if start is not None:
        s = _norm_ts(start)
        dates = [d for d in dates if d >= s]
    if end is not None:
        e = _norm_ts(end)
        dates = [d for d in dates if d <= e]

    if len(dates) < 2:
        return pd.DataFrame(), pd.Series(dtype=float)

    pending: dict | None = None
    records: list[TradeRecord] = []

    for i in range(0, len(dates), rebalance_every):
        d = dates[i]
        chain = loader.get_chain_for_date(d)
        if not chain.contracts:
            continue

        if pending is not None:
            ex = find_contract_in_chain(
                chain,
                pending["expiration"],
                pending["strike"],
                pending["option_type"],
            )
            exp_n = _norm_ts(pending["expiration"])
            exit_px: float | None = None
            if ex is not None:
                exit_px = _long_exit_price(ex, execution)
            elif d > exp_n:
                # Leg expired before this rebalance — absent from chain; long OTM ≈ $0.
                exit_px = 0.0
                warnings.warn(
                    f"Exit {d.date()} is after expiry {exp_n.date()}; "
                    "marking long leg at $0 (OTM expiry assumption — not intrinsic).",
                    stacklevel=2,
                )
            else:
                warnings.warn(
                    f"No matching contract on exit {d.date()} for "
                    f"{pending['option_type']} {pending['strike']} {exp_n.date()} — skipping round-trip.",
                    stacklevel=2,
                )
                pending = None

            if pending is not None and exit_px is not None:
                entry_px = pending["entry_px"]
                pnl = (exit_px - entry_px) * 100.0
                pnl_pct = (exit_px - entry_px) / entry_px * 100.0 if entry_px else float("nan")
                records.append(
                    TradeRecord(
                        entry_date=pending["entry_date"],
                        exit_date=d,
                        expiration=pending["expiration"],
                        strike=pending["strike"],
                        option_type=pending["option_type"],
                        entry_px=entry_px,
                        exit_px=exit_px,
                        pnl_dollars=pnl,
                        pnl_pct=pnl_pct,
                        entry_delta=pending["entry_delta"],
                        entry_iv=pending["entry_iv"],
                    )
                )
                pending = None

        try:
            leg = chain.find_target_leg(
                target_dte=target_dte,
                target_delta=target_delta,
                option_type=option_type,
            )
        except ValueError:
            continue

        entry_px = _long_entry_price(leg, execution)
        if not math.isfinite(entry_px) or entry_px <= 0:
            continue

        pending = {
            "entry_date": d,
            "expiration": leg.expiration,
            "strike": float(leg.strike),
            "option_type": leg.option_type,
            "entry_px": entry_px,
            "entry_delta": float(leg.delta),
            "entry_iv": float(leg.iv),
        }

    # Mark final open leg on last date if possible
    if pending is not None and dates:
        last_d = dates[-1]
        if _norm_ts(last_d) != _norm_ts(pending["entry_date"]):
            exp_n = _norm_ts(pending["expiration"])
            chain = loader.get_chain_for_date(last_d)
            ex = find_contract_in_chain(
                chain,
                pending["expiration"],
                pending["strike"],
                pending["option_type"],
            )
            exit_px: float | None = None
            if ex is not None:
                exit_px = _long_exit_price(ex, execution)
            elif _norm_ts(last_d) > exp_n:
                exit_px = 0.0
            if exit_px is not None:
                entry_px = pending["entry_px"]
                pnl = (exit_px - entry_px) * 100.0
                pnl_pct = (exit_px - entry_px) / entry_px * 100.0 if entry_px else float("nan")
                records.append(
                    TradeRecord(
                        entry_date=pending["entry_date"],
                        exit_date=last_d,
                        expiration=pending["expiration"],
                        strike=pending["strike"],
                        option_type=pending["option_type"],
                        entry_px=entry_px,
                        exit_px=exit_px,
                        pnl_dollars=pnl,
                        pnl_pct=pnl_pct,
                        entry_delta=pending["entry_delta"],
                        entry_iv=pending["entry_iv"],
                    )
                )

    if not records:
        return pd.DataFrame(), pd.Series(dtype=float)

    df = pd.DataFrame([r.__dict__ for r in records])
    df["entry_date"] = pd.to_datetime(df["entry_date"])
    df["exit_date"] = pd.to_datetime(df["exit_date"])
    df["expiration"] = pd.to_datetime(df["expiration"])

    # Cumulative P&L indexed by exit date (simple equity curve of strategy P&L)
    df = df.sort_values("exit_date")
    cum = df["pnl_dollars"].cumsum()
    equity = pd.Series(cum.values, index=df["exit_date"], name="cum_pnl_usd")

    return df, equity


def summarize_trades(trades: pd.DataFrame) -> dict[str, float | int]:
    """Basic stats on completed round-trips."""
    if trades.empty:
        return {"n_trades": 0}
    p = trades["pnl_dollars"]
    return {
        "n_trades": int(len(trades)),
        "total_pnl_usd": float(p.sum()),
        "mean_pnl_usd": float(p.mean()),
        "win_rate": float((p > 0).mean()) if len(p) else 0.0,
        "median_pnl_usd": float(p.median()),
    }
