"""
Poor Man's Covered Call (PMCC) / long diagonal call spread — selection, validation,
and management helpers for **low-volatility** contexts (e.g. VIX < 12).

Designed to work with any source that can supply :class:`~RenTech.core.options_data_loader.OptionChain`
(ThetaData pipeline, Alpaca adapter, Parquet loader, etc.).

Example (Theta-style loader you already have)::

    provider = ChainProviderFromLoader(theta_loader)  # see class below
    strat = PMCCStrategy()
    sel = strat.run(provider, \"SPY\", pd.Timestamp(\"2024-06-01\"), underlying_px=500.0)
    print(sel.to_json(indent=2))
"""

from __future__ import annotations

import json
import math
from dataclasses import dataclass, field
from typing import Any, Literal, Protocol, runtime_checkable

import pandas as pd

from RenTech.core.options_data_loader import OptionChain, OptionContract

CONTRACT_MULTIPLIER = 100.0

__all__ = [
    "PMCCConfig",
    "PMCCSelection",
    "PMCCStrategy",
    "OptionsDataProvider",
    "ChainProviderFromLoader",
    "selection_summary_frame",
    "selection_to_summary_dict",
]


@dataclass(frozen=True)
class PMCCConfig:
    """Default selection targets (align with typical PMCC / low-VIX deployment)."""

    long_min_dte: int = 180
    long_target_delta: float = 0.80
    short_min_dte: int = 7
    short_max_dte: int = 14
    short_delta_min: float = 0.20
    short_delta_max: float = 0.30
    short_target_delta: float = 0.25
    profit_take_frac: float = 0.50
    defensive_short_delta: float = 0.45
    quantity: int = 1


@dataclass
class PMCCSelection:
    """Result of leg selection + validation (one diagonal unit)."""

    symbol: str
    as_of: pd.Timestamp
    long_leg: OptionContract
    short_leg: OptionContract
    qty: int
    net_debit_usd: float
    net_debit_per_share: float
    width_per_share: float
    debit_lt_width: bool
    net_delta: float
    net_vega: float
    net_gamma: float
    net_theta: float
    break_even_approx: float
    capital_efficiency: float
    notes: list[str] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        """Structured dict suitable for JSON (ISO dates, floats)."""
        return {
            "symbol": self.symbol,
            "as_of": pd.Timestamp(self.as_of).isoformat(),
            "qty": self.qty,
            "long_leg": _contract_to_dict(self.long_leg),
            "short_leg": _contract_to_dict(self.short_leg),
            "net_debit_usd": self.net_debit_usd,
            "net_debit_per_share": self.net_debit_per_share,
            "width_per_share": self.width_per_share,
            "debit_lt_width": self.debit_lt_width,
            "net_delta": self.net_delta,
            "net_vega": self.net_vega,
            "net_gamma": self.net_gamma,
            "net_theta": self.net_theta,
            "break_even_approx": self.break_even_approx,
            "capital_efficiency": self.capital_efficiency,
            "notes": list(self.notes),
        }

    def to_json(self, **kwargs: Any) -> str:
        return json.dumps(self.to_dict(), **kwargs)


def _contract_to_dict(c: OptionContract) -> dict[str, Any]:
    return {
        "expiration": pd.Timestamp(c.expiration).isoformat(),
        "strike": float(c.strike),
        "option_type": c.option_type,
        "bid": float(c.bid),
        "ask": float(c.ask),
        "mid": float(c.mid),
        "delta": float(c.delta),
        "gamma": float(c.gamma),
        "theta": float(c.theta),
        "vega": float(c.vega),
        "iv": float(c.iv),
    }


@runtime_checkable
class OptionsDataProvider(Protocol):
    """Minimal interface for ThetaData / Alpaca / Parquet wrappers."""

    def get_option_chain(self, symbol: str, as_of: pd.Timestamp) -> OptionChain:
        """Return a single-date chain with greeks populated where possible."""
        ...


class PMCCStrategy:
    """
    PMCC: long LEAPS call (~0.80Δ, 180+ DTE) + short OTM call (7–14 DTE, ~0.20–0.30Δ).

    * **Financial check:** net debit (long cost − short credit) must be **strictly less than**
      the strike-width dollar value ``(K_short − K_long) × 100 × qty`` so that at very high
      spot the spread approaches full width and cannot be underwater vs. debit (instant gap-up
      sanity for a call vertical component).
    * **Management:** profit target on the short leg, defensive roll when short Δ breaches a
      threshold, and net vega sign check.
    """

    def __init__(self, config: PMCCConfig | None = None) -> None:
        self.config = config or PMCCConfig()

    # --- selection ---

    def select_legs(self, chain: OptionChain) -> tuple[OptionContract, OptionContract]:
        """
        Pick long and short calls from ``chain`` using :meth:`OptionChain.find_target_leg`.

        Raises ``ValueError`` if strikes are not ordered ``K_long < K_short`` or chain is empty.
        """
        cfg = self.config
        long_leg = chain.find_target_leg(
            target_dte=cfg.long_min_dte,
            target_delta=cfg.long_target_delta,
            option_type="C",
        )
        # Target DTE inside 7–14 window (use midpoint for search)
        short_dte = int(round(0.5 * (cfg.short_min_dte + cfg.short_max_dte)))
        short_leg = chain.find_target_leg(
            target_dte=short_dte,
            target_delta=cfg.short_target_delta,
            option_type="C",
        )

        if float(long_leg.strike) >= float(short_leg.strike):
            raise ValueError(
                "PMCC requires long strike < short strike (OTM short call). "
                f"Got long K={long_leg.strike}, short K={short_leg.strike} — adjust targets or chain."
            )
        return long_leg, short_leg

    def compute_net_debit(
        self,
        long_leg: OptionContract,
        short_leg: OptionContract,
        *,
        fill_style: Literal["mid", "conservative"] = "conservative",
        qty: int | None = None,
    ) -> float:
        """
        Net debit in dollars for opening the diagonal (long buy, short sell).

        * ``mid``: (long_mid − short_mid) × 100 × qty
        * ``conservative``: pay ask on long, collect bid on short (max debit).
        """
        q = int(qty if qty is not None else self.config.quantity)
        if fill_style == "mid":
            long_px = float(long_leg.mid)
            short_px = float(short_leg.mid)
        else:
            long_px = float(long_leg.ask)
            short_px = float(short_leg.bid)
        per_share = long_px - short_px
        return per_share * CONTRACT_MULTIPLIER * q

    def validate_debit_vs_width(
        self,
        long_leg: OptionContract,
        short_leg: OptionContract,
        net_debit_usd: float,
        qty: int | None = None,
    ) -> bool:
        """
        True iff ``net_debit < (K_short − K_long) × 100 × qty``.

        This is the standard "debit smaller than spread width" check for the embedded call spread.
        """
        q = int(qty if qty is not None else self.config.quantity)
        width = (float(short_leg.strike) - float(long_leg.strike)) * CONTRACT_MULTIPLIER * q
        if width <= 0:
            return False
        return net_debit_usd < width

    def build_selection(
        self,
        symbol: str,
        chain: OptionChain,
        underlying_px: float,
        *,
        qty: int | None = None,
        fill_style: Literal["mid", "conservative"] = "conservative",
    ) -> PMCCSelection:
        """
        Full selection + validation + summary metrics.

        ``underlying_px`` is used for capital efficiency vs. covered call (100 shares).
        """
        cfg = self.config
        q = int(qty if qty is not None else cfg.quantity)
        long_leg, short_leg = self.select_legs(chain)
        net_debit = self.compute_net_debit(long_leg, short_leg, fill_style=fill_style, qty=q)
        width_ps = float(short_leg.strike) - float(long_leg.strike)
        debit_ps = net_debit / (CONTRACT_MULTIPLIER * q)
        ok = self.validate_debit_vs_width(long_leg, short_leg, net_debit, qty=q)

        net_delta = (long_leg.delta - short_leg.delta) * q
        net_vega = (long_leg.vega - short_leg.vega) * q
        net_gamma = (long_leg.gamma - short_leg.gamma) * q
        net_theta = (long_leg.theta - short_leg.theta) * q

        # Approx BE: stock must exceed long strike by net debit/share for net zero at short expiry
        # (ignores time value of long — standard PMCC back-of-envelope).
        break_even = float(long_leg.strike) + debit_ps

        # Capital efficiency: notional for 100-share covered call vs. cash outlay for PMCC
        cc_notional = float(underlying_px) * 100.0 * q
        capital_efficiency = cc_notional / net_debit if net_debit > 1e-9 else float("inf")

        notes: list[str] = []
        if not ok:
            notes.append("debit_vs_width_check_failed")
        if net_vega <= 0:
            notes.append("net_vega_not_positive")

        return PMCCSelection(
            symbol=symbol,
            as_of=chain.as_of,
            long_leg=long_leg,
            short_leg=short_leg,
            qty=q,
            net_debit_usd=float(net_debit),
            net_debit_per_share=float(debit_ps),
            width_per_share=float(width_ps),
            debit_lt_width=ok,
            net_delta=float(net_delta),
            net_vega=float(net_vega),
            net_gamma=float(net_gamma),
            net_theta=float(net_theta),
            break_even_approx=float(break_even),
            capital_efficiency=float(capital_efficiency),
            notes=notes,
        )

    def run(
        self,
        data_provider: OptionsDataProvider,
        symbol: str,
        as_of: pd.Timestamp,
        underlying_px: float,
        **kwargs: Any,
    ) -> PMCCSelection:
        """Fetch chain from provider and run :meth:`build_selection`."""
        chain = data_provider.get_option_chain(symbol, as_of)
        return self.build_selection(symbol, chain, underlying_px, **kwargs)

    # --- management ---

    @staticmethod
    def short_leg_max_profit_credit(short_credit_per_share: float, qty: int = 1) -> float:
        """Max profit on short call leg ≈ credit received (per spread unit)."""
        return float(short_credit_per_share * CONTRACT_MULTIPLIER * qty)

    def should_take_profit_on_short(
        self,
        short_entry_credit_usd: float,
        short_current_mark_usd: float,
    ) -> bool:
        """Close or roll short when captured profit ≥ ``profit_take_frac`` of max (credit)."""
        if short_entry_credit_usd <= 0:
            return False
        captured = short_entry_credit_usd - short_current_mark_usd
        return captured >= self.config.profit_take_frac * short_entry_credit_usd

    def should_defensive_roll_short(self, short_delta: float) -> bool:
        """Roll short out/up when delta tests the strike (default Δ > 0.45)."""
        return float(short_delta) > self.config.defensive_short_delta

    def defensive_roll_instruction(self) -> dict[str, str]:
        """Human-readable roll policy."""
        return {
            "action": "roll_short_leg",
            "direction": "out_in_time_and_up_in_strike",
            "goal": "collect_net_credit_while_reducing_assignment_risk",
            "trigger": f"short_call_delta > {self.config.defensive_short_delta}",
        }

    def net_vega_positive(self, long_vega: float, short_vega: float, qty: int = 1) -> bool:
        q = int(qty)
        return (long_vega - short_vega) * q > 0.0


def selection_summary_frame(sel: PMCCSelection) -> pd.DataFrame:
    """Single-row :class:`~pandas.DataFrame` for notebooks / logs."""
    return pd.DataFrame([selection_to_summary_dict(sel)])


class ChainProviderFromLoader:
    """
    Wrap any object with ``get_chain_for_date(trading_date) -> OptionChain`` (e.g. ThetaChunksLoader).

    ``symbol`` is ignored if the loader is single-ticker; override for multi-underlying.
    """

    def __init__(self, loader: Any, symbol: str = "SPY") -> None:
        self._loader = loader
        self._symbol = symbol

    def get_option_chain(self, symbol: str, as_of: pd.Timestamp) -> OptionChain:
        _ = symbol  # default: single-name loader
        return self._loader.get_chain_for_date(pd.Timestamp(as_of).normalize())


def selection_to_summary_dict(sel: PMCCSelection) -> dict[str, Any]:
    """
    Compact JSON-friendly summary (explicit fields requested for dashboards).

    * ``capital_efficiency``: ratio of covered-call notional (100 × S × qty) to ``net_debit``
      — higher means less capital tied up vs. stock.
    """
    return {
        "long_strike": float(sel.long_leg.strike),
        "short_strike": float(sel.short_leg.strike),
        "long_expiration": pd.Timestamp(sel.long_leg.expiration).strftime("%Y-%m-%d"),
        "short_expiration": pd.Timestamp(sel.short_leg.expiration).strftime("%Y-%m-%d"),
        "net_debit": float(sel.net_debit_usd),
        "break_even": float(sel.break_even_approx),
        "capital_efficiency": float(sel.capital_efficiency),
        "net_delta": float(sel.net_delta),
        "net_vega": float(sel.net_vega),
        "debit_lt_width": bool(sel.debit_lt_width),
        "notes": list(sel.notes),
    }
