import QuantLib as ql


# ============================================================
# Global defaults (override via parameters if needed)
# ============================================================

CALENDAR = ql.TARGET()
BUSINESS_CONVENTION = ql.ModifiedFollowing
DATE_GEN = ql.DateGeneration.Forward

FIXED_DAYCOUNT = ql.Thirty360(ql.Thirty360.European)
FLOAT_DAYCOUNT = ql.Actual360()


# ============================================================
# Yield curve factory
# ============================================================

def make_flat_curve(
    evaluation_date: ql.Date,
    rate: float,
    day_count: ql.DayCounter = ql.Actual365Fixed()
) -> ql.YieldTermStructureHandle:
    """
    Flat yield curve factory.
    """
    ql.Settings.instance().evaluationDate = evaluation_date
    curve = ql.FlatForward(evaluation_date, rate, day_count)
    return ql.YieldTermStructureHandle(curve)


# ============================================================
# Index factory
# ============================================================

def make_euribor_6m(
    curve_handle: ql.YieldTermStructureHandle
) -> ql.IborIndex:
    """
    Euribor 6M index factory.
    """
    return ql.Euribor6M(curve_handle)


# ============================================================
# Schedule factory
# ============================================================

def make_schedule(
    start: ql.Date,
    maturity: ql.Date,
    tenor: ql.Period,
    calendar: ql.Calendar = CALENDAR
) -> ql.Schedule:
    """
    Generic schedule factory.
    """
    return ql.Schedule(
        start,
        maturity,
        tenor,
        calendar,
        BUSINESS_CONVENTION,
        BUSINESS_CONVENTION,
        DATE_GEN,
        False
    )


# ============================================================
# Swap factory
# ============================================================

def make_vanilla_swap(
    evaluation_date: ql.Date,
    curve_handle: ql.YieldTermStructureHandle,
    notional: float,
    fixed_rate: float,
    maturity_years: int,
    pay_fixed: bool = False,
    fixed_leg_freq: ql.Period = ql.Annual,
    float_leg_freq: ql.Period = ql.Semiannual
) -> ql.VanillaSwap:    
    """
    Vanilla fixed-for-float IRS factory.

    pay_fixed = True  -> payer swap
    pay_fixed = False -> receiver swap
    """

    ql.Settings.instance().evaluationDate = evaluation_date

    index = make_euribor_6m(curve_handle)

    start = CALENDAR.advance(evaluation_date, 2, ql.Days)
    maturity = CALENDAR.advance(start, maturity_years, ql.Years)

    fixed_schedule = make_schedule(
        start, maturity, ql.Period(fixed_leg_freq)
    )
    float_schedule = make_schedule(
        start, maturity, ql.Period(float_leg_freq)
    )

    swap_type = (
        ql.VanillaSwap.Payer if pay_fixed else ql.VanillaSwap.Receiver
    )

    swap = ql.VanillaSwap(
        swap_type,
        notional,
        fixed_schedule,
        fixed_rate,
        FIXED_DAYCOUNT,
        float_schedule,
        index,
        0.0,
        FLOAT_DAYCOUNT
    )

    swap.setPricingEngine(
        ql.DiscountingSwapEngine(curve_handle)
    )

    return swap


# ============================================================
# Swaption helper factory (for HW calibration)
# ============================================================

def make_swaption_helpers(
    swaption_data: list,
    curve_handle: ql.YieldTermStructureHandle,
    index: ql.IborIndex,
    model: ql.HullWhite
) -> list:
    """
    Create ATM swaption helpers.

    swaption_data = [(expiry, tenor, vol), ...]
    """

    helpers = []

    for expiry, tenor, vol in swaption_data:
        helper = ql.SwaptionHelper(
            expiry,
            tenor,
            ql.QuoteHandle(ql.SimpleQuote(vol)),
            index,
            index.tenor(),
            index.dayCounter(),
            index.dayCounter(),
            curve_handle
        )

        helper.setPricingEngine(
            ql.JamshidianSwaptionEngine(model)
        )

        helpers.append(helper)

    return helpers
