# ---
# 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 Market Data Listener And Scheduling
#
# The reactive layer is easiest to understand as a tiny dependency graph: market-data updates mark nodes dirty, the graph recomputes what depends on them, and schedulers control when those refreshes are allowed to happen.

# %%
from __future__ import annotations

import asyncio
import sys
import threading
from decimal import Decimal

import matplotlib

matplotlib.use("Agg")

import matplotlib.pyplot as plt
import pandas as pd

from fuggers_py.core import Currency, Date, Frequency
from fuggers_py.data import (
    BondReferenceData,
    BondType,
    CurveId,
    CurveInputs,
    CurvePoint,
    IndexFixing,
    InstrumentId,
    IssuerType,
    MarketDataProvider,
    QuoteSide,
    RawQuote,
    ReferenceDataProvider,
)
from fuggers_py.engine import (
    CronScheduler,
    CurveInputUpdate,
    EodScheduler,
    IndexFixingUpdate,
    IntervalScheduler,
    NodeId,
    PricingEngineBuilder,
    PricingInput,
    QuoteUpdate,
)

from examples._shared.plotting import apply_research_style
from examples._shared.tables import display_table

apply_research_style()

# %% [markdown]
# ## Deterministic graph inputs
#
# The graph contains one priced bond node that depends on both a quote node and a discount-curve node. We simulate a fixing update, a curve update, and a quote update in sequence.

# %%
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',
)

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'},
    ),
)

# %%
async def run_reactive_flow() -> dict[str, object]:
    await reactive.start()
    try:
        await reactive.publish_market_data_update(fixing_update)
        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())
        return {
            'fixing_step': fixing_step,
            'curve_step': curve_step,
            'quote_step': quote_step,
            'subscriber_updates': subscriber_updates,
            'cached_value': reactive.calc_graph.get_node_value('price:ENGINE-REACTIVE-EXAMPLE'),
        }
    finally:
        reactive.unsubscribe_updates(subscriber_queue)
        await reactive.stop()


def run_reactive_flow_sync() -> dict[str, object]:
    if "ipykernel" not in sys.modules:
        return asyncio.run(run_reactive_flow())

    result: dict[str, object] = {}

    def worker() -> None:
        result["value"] = asyncio.run(run_reactive_flow())

    thread = threading.Thread(target=worker)
    thread.start()
    thread.join()
    return result["value"]


flow = run_reactive_flow_sync()
event_log = []
for label in ['fixing_step', 'curve_step', 'quote_step']:
    for update in flow[label]:
        event_log.append(
            {
                'step': label,
                'node_id': update.node_id.value,
                'source': update.source.value,
                'payload_type': type(update.payload).__name__,
            }
        )
event_table = pd.DataFrame(event_log)
scheduler_table = pd.DataFrame(
    [
        {'scheduler': 'IntervalScheduler', 'example': 'Every 60 seconds', 'object': type(IntervalScheduler([NodeId('curve:usd.discount')], interval_seconds=60.0, immediate=True)).__name__},
        {'scheduler': 'EodScheduler', 'example': 'Daily close refresh', 'object': type(EodScheduler([NodeId('quote:ENGINE-REACTIVE-EXAMPLE')])).__name__},
        {'scheduler': 'CronScheduler', 'example': '17:00 weekdays', 'object': type(CronScheduler([NodeId('price:ENGINE-REACTIVE-EXAMPLE')], expression='0 17 * * 1-5')).__name__},
    ]
)
dependency_table = pd.DataFrame(
    [
        {'upstream': 'curve_input:usd.discount', 'downstream': 'curve:usd.discount'},
        {'upstream': 'curve:usd.discount', 'downstream': 'price:ENGINE-REACTIVE-EXAMPLE'},
        {'upstream': 'quote:ENGINE-REACTIVE-EXAMPLE', 'downstream': 'price:ENGINE-REACTIVE-EXAMPLE'},
    ]
)

# %%
display_table(event_table)
display_table(scheduler_table)
display_table(dependency_table)

# %% [markdown]
# The event log shows the dirty-node propagation directly: the curve update touches the raw input node, the built curve node, and then the pricing node. The quote update only needs to touch the quote node and the pricing node.

# %%
figure, axes = plt.subplots(1, 2, figsize=(12, 4.8))
step_counts = event_table.groupby('step').size().reset_index(name='count')
axes[0].bar(step_counts['step'], step_counts['count'])
axes[0].set_title('Reactive Updates Per Step')
axes[0].set_xlabel('Processing step')
axes[0].set_ylabel('Node updates')

positions = {'curve_input:usd.discount': (0.1, 0.6), 'curve:usd.discount': (0.5, 0.6), 'quote:ENGINE-REACTIVE-EXAMPLE': (0.1, 0.2), 'price:ENGINE-REACTIVE-EXAMPLE': (0.85, 0.4)}
for node, (x, y) in positions.items():
    axes[1].scatter(x, y, s=800, color='#2c7da0')
    axes[1].text(x, y, node, ha='center', va='center', color='white', fontsize=8)
for upstream, downstream in zip(dependency_table['upstream'], dependency_table['downstream']):
    x0, y0 = positions[upstream]
    x1, y1 = positions[downstream]
    axes[1].annotate('', xy=(x1, y1), xytext=(x0, y0), arrowprops={'arrowstyle': '->', 'lw': 1.5})
axes[1].set_title('Dependency Sketch')
axes[1].set_xticks([])
axes[1].set_yticks([])
plt.tight_layout()
figure

# %%
figure, axis = plt.subplots(figsize=(10, 4.2))
axis.plot(range(1, len(event_table) + 1), [1] * len(event_table), marker='o', linewidth=2)
for idx, (_, row) in enumerate(event_table.iterrows(), start=1):
    axis.text(idx, 1.02, row['node_id'], rotation=35, ha='left', fontsize=8)
axis.set_title('Update Timeline')
axis.set_xlabel('Event order')
axis.set_ylabel('Update marker')
axis.set_yticks([])
plt.tight_layout()
figure

# %% [markdown]
# ## When to use the reactive layer
#
# Research teams typically reach for this machinery when input updates are frequent enough that rerunning every notebook cell would be wasteful, but they still want transparent dependency propagation and deterministic scheduling behavior.
