"""Select strikes from an observed option chain, use executable sides of the quote and calculate the protected payoff per underlying share.

Research example. Live observations can change.
"""

from datetime import UTC, datetime, timedelta
from decimal import Decimal
from textwrap import fill

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

AS_OF = datetime.now(UTC).date()
NAVY, BLUE, PINK = "#132244", "#8fb9ff", "#e7a2cc"


def fetch(qj):
    dates = payload(qj.domains.call("derivatives.options.get_options_expirations", provider="cboe", symbol="SPY"))
    dates = dates["expirations"] if isinstance(dates, dict) else dates
    available = sorted((str(day)[:10] for day in dates if str(day)[:10] >= AS_OF.isoformat()))
    if not available:
        raise ValueError("No nonexpired SPY option expiration")
    return {
        "chain": qj.domains.call(
            "derivatives.options.get_options_chain", provider="cboe", symbol="SPY", expiration_date=available[0]
        ),
        "spy_spot": qj.domains.call(
            "equity.pricing.get_historical_prices",
            provider="eod",
            symbols=["SPY"],
            start_date=(AS_OF - timedelta(days=14)).isoformat(),
            end_date=(AS_OF - timedelta(days=1)).isoformat(),
            frequency="1d",
        ),
    }


def analyze(raw):
    reference = history(raw["spy_spot"], "SPY", adjusted=False)["price"].iloc[-1]
    options = payload(raw["chain"])["options"]
    puts = [
        row
        for row in options
        if row["option_type"] == "put"
        and all(pd.notna(row.get(key)) for key in ["strike", "bid", "ask"])
        and decimal(row["ask"]) > 0
        and Decimal(0) <= decimal(row["bid"]) <= decimal(row["ask"])
    ]
    if not puts:
        raise ValueError("No usable observed put quotes")
    long = min(puts, key=lambda row: abs(decimal(row["strike"]) - reference * Decimal(".97")))
    lower = [row for row in puts if decimal(row["strike"]) < decimal(long["strike"])]
    if not lower:
        raise ValueError("No lower strike for the put spread")
    short = min(lower, key=lambda row: abs(decimal(row["strike"]) - reference * Decimal(".90")))
    cost = decimal(long["ask"]) - decimal(short["bid"])
    width = decimal(long["strike"]) - decimal(short["strike"])
    if not Decimal(0) <= cost <= width:
        raise ValueError("Observed quotes imply an invalid put-spread entry cost")
    rows = []
    for scenario in np.linspace(-0.15, 0.15, 121):
        terminal = reference * (Decimal(1) + Decimal(str(scenario)))
        payoff = max(decimal(long["strike"]) - terminal, Decimal(0)) - max(
            decimal(short["strike"]) - terminal, Decimal(0)
        )
        rows.append(
            {
                "Underlying move %": 100 * scenario,
                "Unprotected return %": 100 * ratio(terminal - reference, reference),
                "With put spread %": 100 * ratio(terminal - reference + payoff - cost, reference),
            }
        )
    result = pd.DataFrame(rows)
    fig, (ax,) = canvas("SPY · protection has a price and a limit", "Expiry return % · per underlying share")
    for column in ["Unprotected return %", "With put spread %"]:
        ax.plot(result["Underlying move %"], result[column], label=column)
    ax.axhline(0, color="#afc1dc", lw=0.8)
    ax.set_xlabel("Underlying price scenario at option expiry %")
    legend(ax)
    result["Entry cost / reference %"] = 100 * ratio(cost, reference)
    return result, fig


def payload(response):
    if hasattr(response, "to_dict"):
        response = response.to_dict()
    while isinstance(response, dict):
        if response.get("error") or response.get("error_code") or response.get("success") is False:
            raise ValueError("Provider request failed")
        if "data" in response:
            response = response["data"]
        elif "value" in response:
            response = response["value"]
        else:
            break
    return response


def frame(response, date="date"):
    rows = payload(response)
    if not isinstance(rows, list) or not rows:
        raise ValueError("No provider observations returned")
    result = pd.DataFrame(rows)
    if date:
        result[date] = pd.to_datetime(result[date], utc=True)
        result = result.sort_values(date)
    return result


def decimal(value):
    if pd.isna(value):
        raise ValueError("Required monetary value or quantity is missing")
    return Decimal(str(value))


def ratio(numerator, denominator):
    denominator = decimal(denominator)
    if denominator == 0:
        raise ValueError("Required denominator is zero")
    return float(decimal(numerator) / denominator)


def history(response, symbol, adjusted=True):
    values = payload(response)
    if not isinstance(values, dict) or symbol not in values:
        raise ValueError(f"No price coverage for {symbol}")
    result = frame(values[symbol]).set_index("date")
    field = "adjusted_close" if adjusted else "close"
    result["price"] = result[field].map(decimal)
    result["volume"] = result["volume"].map(decimal)
    if result.index.has_duplicates:
        raise ValueError("Duplicate trading dates")
    return result


def canvas(title, ylabel, panels=1):
    plt.rcParams.update(
        {
            "figure.facecolor": NAVY,
            "axes.facecolor": NAVY,
            "savefig.facecolor": NAVY,
            "text.color": "#e9eef8",
            "axes.labelcolor": "#afc1dc",
            "xtick.color": "#afc1dc",
            "ytick.color": "#afc1dc",
            "axes.edgecolor": "#34486b",
            "grid.color": "#34486b",
            "font.size": 10,
            "axes.prop_cycle": plt.cycler(color=[BLUE, PINK, "#ba9dea"]),
        }
    )
    fig, axes = plt.subplots(panels, 1, figsize=(9, 6.1), squeeze=False, layout="constrained")
    fig.suptitle(fill(title, width=58), fontsize=17, x=0.04, ha="left", weight="medium")
    for ax in axes.flat:
        ax.grid(axis="y", alpha=0.45)
        ax.set_axisbelow(True)
        ax.spines[["top", "right"]].set_visible(False)
    axes.flat[0].set_ylabel(ylabel)
    return fig, list(axes.flat)


def legend(ax):
    ax.legend(frameon=False, labelcolor="#e9eef8", fontsize=9)


def run():
    from quantjourney.sdk import QuantJourneyAPI

    qj = QuantJourneyAPI.from_env()
    raw = fetch(qj)
    table, fig = analyze(raw)
    print(table.to_string(index=False))
    fig.savefig(OUTPUT, dpi=180)
    plt.show()


OUTPUT = "sdk_option_overlay.png"

if __name__ == "__main__":
    run()
