import QuantLib as ql


def build_calibrated_hw(
    curve_handle,
    swaption_helpers,
    a_init=0.05,
    sigma_init=0.01
):
    hw = ql.HullWhite(curve_handle, a_init, sigma_init)

    optimizer = ql.LevenbergMarquardt()
    end_criteria = ql.EndCriteria(1000, 500, 1e-8, 1e-8, 1e-8)

    hw.calibrate(swaption_helpers, optimizer, end_criteria)

    return hw

import numpy as np


def simulate_hw_state(
    hw: ql.HullWhite,
    curve_handle,
    time_grid,
    n_paths,
    seed=42
):
    a, sigma = hw.params()

    dt = np.diff(time_grid)
    n_steps = len(time_grid)

    np.random.seed(seed)
    x = np.zeros((n_paths, n_steps))

    for i in range(1, n_steps):
        decay = np.exp(-a * dt[i-1])
        vol = sigma * np.sqrt((1 - np.exp(-2 * a * dt[i-1])) / (2 * a))
        z = np.random.normal(size=n_paths)

        x[:, i] = x[:, i-1] * decay + vol * z

    return x


def swap_npv_from_state(
    swap,
    hw,
    curve_handle,
    t,
    x_t,
    today
):
    ql.Settings.instance().evaluationDate = today + int(t * 365)

    hw.setState(x_t)

    engine = ql.DiscountingSwapEngine(
        curve_handle,
        False,
        hw
    )

    swap.setPricingEngine(engine)
    return swap.NPV()
