# ---
# jupyter:
#   jupytext:
#     formats: ipynb,py:percent
#     text_representation:
#       extension: .py
#       format_name: percent
#       format_version: '1.3'
#       jupytext_version: 1.19.1
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# %% [markdown]
# # Reactive engine examples
#
# This notebook shows:
#
# - building a pricing engine from the trait-layer providers
# - registering a reactive pricing node
# - publishing fixing, curve, and quote updates
# - processing deterministic update steps without background hangs

# %%
from __future__ import annotations

import asyncio
import sys
from decimal import Decimal
from pathlib import Path

root = Path.cwd()
if (root / "src").exists():
    sys.path.insert(0, str(root / "src"))
elif (root.parent / "src").exists():
    sys.path.insert(0, str(root.parent / "src"))

# %% [markdown]
# ## Imports

# %%
from fuggers_py.core import Currency, Date, Frequency  # noqa: E402
from fuggers_py.engine import (  # noqa: E402
    CurveInputUpdate,
    IndexFixingUpdate,
    NodeId,
    PricingEngineBuilder,
    PricingInput,
    QuoteUpdate,
)
from fuggers_py.data import (
    BondReferenceData,
    BondType,
    CurveId,
    CurveInputs,
    CurvePoint,
    IndexFixing,
    InstrumentId,
    IssuerType,
    MarketDataProvider,
    QuoteSide,
    RawQuote,
    ReferenceDataProvider,
)

# %% [markdown]
# ## Define deterministic inputs

# %%
settlement = Date.from_ymd(2026, 3, 14)
instrument_id = InstrumentId("ENGINE-REACTIVE-EXAMPLE")

reference = BondReferenceData(
    instrument_id=instrument_id,
    bond_type=BondType.FIXED_RATE,
    issuer_type=IssuerType.CORPORATE,
    issue_date=Date.from_ymd(2024, 1, 15),
    maturity_date=Date.from_ymd(2031, 1, 15),
    currency=Currency.USD,
    coupon_rate=Decimal("0.0450"),
    frequency=Frequency.SEMI_ANNUAL,
)
curve_inputs = CurveInputs.from_points(
    CurveId("usd.discount"),
    settlement,
    [
        CurvePoint(Decimal("1.0"), Decimal("0.0425")),
        CurvePoint(Decimal("5.0"), Decimal("0.0390")),
        CurvePoint(Decimal("10.0"), Decimal("0.0405")),
    ],
)
fixing_update = IndexFixingUpdate(
    fixing=IndexFixing("SOFR", settlement, Decimal("0.0412")),
    source="fixing-file",
)
curve_update = CurveInputUpdate(curve_inputs=curve_inputs, source="curve-file")
quote_update = QuoteUpdate(
    quote=RawQuote(
        instrument_id=instrument_id,
        value=Decimal("101.25"),
        side=QuoteSide.MID,
        as_of=settlement,
        currency=Currency.USD,
        source="quote-file",
    ),
    source="quote-file",
)

# %% [markdown]
# ## Build the providers and engine

# %%
class _BondSource:
    def __init__(self, reference_data: BondReferenceData) -> None:
        self.reference_data = reference_data

    def get_bond_reference(self, requested_instrument_id):
        requested = InstrumentId.parse(requested_instrument_id)
        if requested == self.reference_data.instrument_id:
            return self.reference_data
        return None


market_data = MarketDataProvider()
reference_data = ReferenceDataProvider(bond_source=_BondSource(reference))
engine = (
    PricingEngineBuilder()
    .with_market_data_provider(market_data)
    .with_reference_data_provider(reference_data)
    .with_settlement_date(settlement)
    .build()
)

reactive = engine.reactive_engine
assert reactive is not None
subscriber_queue = reactive.subscribe_updates()
reactive.register_pricing_node(
    NodeId("price:ENGINE-REACTIVE-EXAMPLE"),
    PricingInput(
        instrument=instrument_id,
        settlement_date=settlement,
        instrument_id=instrument_id,
        curve_roles={"discount": "usd.discount"},
    ),
)

# %% [markdown]
# ## Run a small reactive flow
#
# As a plain Python script this notebook executes the flow automatically.
# In a live notebook session, run `results = await run_reactive_flow()` after
# the setup cells below have executed.

# %%
async def run_reactive_flow() -> dict[str, object]:
    await reactive.start()
    try:
        processed_fixing = await reactive.publish_market_data_update(fixing_update)
        assert processed_fixing is None
        fixing_step = await reactive.process_once(timeout=0.1)

        await reactive.publish_market_data_update(curve_update)
        curve_step = await reactive.process_once(timeout=0.1)

        await reactive.publish_market_data_update(quote_update)
        quote_step = await reactive.process_once(timeout=0.1)

        subscriber_updates = []
        while not subscriber_queue.empty():
            subscriber_updates.append(subscriber_queue.get_nowait())

        cached_value = reactive.calc_graph.get_node_value("price:ENGINE-REACTIVE-EXAMPLE")
        curve_value = reactive.calc_graph.get_node_value("curve:usd.discount")
        quote_value = reactive.calc_graph.get_node_value("quote:ENGINE-REACTIVE-EXAMPLE")

        return {
            "fixing_step": fixing_step,
            "curve_step": curve_step,
            "quote_step": quote_step,
            "subscriber_updates": tuple(subscriber_updates),
            "cached_value": None if cached_value is None else cached_value.value,
            "curve_value": None if curve_value is None else curve_value.value,
            "quote_value": None if quote_value is None else quote_value.value,
        }
    finally:
        reactive.unsubscribe_updates(subscriber_queue)
        await reactive.stop()


IS_NOTEBOOK = "ipykernel" in sys.modules
if IS_NOTEBOOK:
    print("Notebook mode: run `results = await run_reactive_flow()` to execute the reactive flow.")
else:
    results = asyncio.run(run_reactive_flow())
    print("Processed fixing updates:", len(results["fixing_step"]))
    print("Processed curve updates:", len(results["curve_step"]))
    print("Processed quote updates:", len(results["quote_step"]))
    print("Subscriber updates:", len(results["subscriber_updates"]))
    print("Cached pricing output:", results["cached_value"])
    print("Cached curve:", results["curve_value"])
    print("Cached quote:", results["quote_value"])
