"""Compare quoted CPI threshold contracts with the latest calculated year-on-year inflation from BLS. Preserve each bid and ask.

Research example. Live observations can change.
"""

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

import matplotlib.pyplot as plt
import pandas as pd

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


def fetch(qj):
    return {
        "kalshi_cpi": qj.domains.call(
            "predictions.markets.search_markets", provider="kalshi", series_ticker="KXCPIYOY", status="open", limit=1000
        ),
        "bls_latest": qj.domains.call(
            "macro.us.get_cpi",
            provider="bls",
            category="all",
            seasonally_adjusted=False,
            start_year=AS_OF.year - 1,
            end_year=AS_OF.year,
        ),
    }


def analyze(raw):
    market_page = payload(raw["kalshi_cpi"])
    if market_page.get("cursor"):
        raise ValueError("Paginate the market search before comparing event thresholds")
    markets = frame(market_page["markets"], "close_time")
    eligible = markets.loc[(markets["close_time"].dt.date > AS_OF) & (markets["strike_type"] == "greater")].copy()
    if eligible.empty:
        raise ValueError("No open future CPI threshold contracts")
    event = eligible.sort_values("close_time").iloc[0]["event_ticker"]
    cohort = eligible.loc[eligible["event_ticker"] == event]
    rows = []
    for contract in cohort.to_dict("records"):
        bid = decimal(contract["yes_bid_dollars"])
        ask = decimal(contract["yes_ask_dollars"])
        payout = decimal(contract["notional_value_dollars"])
        if (
            payout <= 0
            or not Decimal(0) <= bid <= ask <= payout
            or decimal(contract["yes_bid_size_fp"]) <= 0
            or decimal(contract["yes_ask_size_fp"]) <= 0
        ):
            continue
        rows.append(
            {
                "Threshold %": float(contract["floor_strike"]),
                "Yes bid / payout %": 100 * ratio(bid, payout),
                "Yes ask / payout %": 100 * ratio(ask, payout),
                "Event": event,
                "Quote updated": contract["updated_time"],
            }
        )
    result = pd.DataFrame(rows).sort_values("Threshold %") if rows else pd.DataFrame()
    if result.empty:
        raise ValueError("No valid two-sided CPI quotes with displayed size")
    cpi = bls_cpi(raw["bls_latest"]).resample(pd.offsets.MonthEnd()).last()
    # Reindex the calendar so a missing CPI month cannot shift the annual comparison.
    annual = (100 * (cpi / cpi.shift(12) - 1)).dropna()
    if annual.empty:
        raise ValueError("No exact prior-year CPI observation for a current baseline")
    baseline = annual.iloc[-1]
    fig, (top, bottom) = canvas("Inflation · reported data and market quotes", "Calculated CPI YoY %", panels=2)
    observation_months = pd.DatetimeIndex([day.replace(day=1) for day in annual.index])
    top.plot(observation_months, annual, marker="o", color=BLUE)
    top.set_xlabel("BLS observation month · unadjusted CPI")
    bottom.plot(result["Threshold %"], result["Yes bid / payout %"], marker=".", label="Yes bid")
    bottom.plot(result["Threshold %"], result["Yes ask / payout %"], marker=".", label="Yes ask")
    bottom.fill_between(
        result["Threshold %"], result["Yes bid / payout %"], result["Yes ask / payout %"], alpha=0.14, color=BLUE
    )
    bottom.axvline(baseline, color="#afc1dc", linestyle="--", label=f"BLS {annual.index[-1]:%Y-%m} · {baseline:.2f}%")
    bottom.set_xlabel("Annual CPI threshold % · " + event)
    bottom.set_ylabel("Quote / $1 payout %")
    bottom.set_ylim(0, 100)
    legend(bottom)
    result["Latest calculated CPI YoY %"] = baseline
    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 macro(response, column):
    values = frame(response).set_index("date")[column]
    values = pd.to_numeric(values, errors="coerce").dropna()
    if values.empty:
        raise ValueError(f"No numeric observations for {column}")
    return values


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()


def bls_cpi(response):
    return macro(response, "cpi_all")


OUTPUT = "sdk_inflation_market.png"

if __name__ == "__main__":
    run()
