"""Fixed-input, forced-recalculation QuantLib Python API benchmark."""
import argparse, hashlib, importlib.metadata, json, math, platform, statistics, struct, sys, time
from pathlib import Path

def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--version', action='version', version='quantlib-compare 1')
    parser.add_argument('--preflight', action='store_true')
    args = parser.parse_args()
    import QuantLib as ql
    assert ql.__version__ == '1.41'
    today = ql.Date(9, 9, 2026)
    ql.Settings.instance().evaluationDate = today
    dc, cal = ql.Actual360(), ql.NullCalendar()
    inputs = {
        'european': dict(option='Call', spot=120.0, strike=100.0, days=270, years=0.75, rate=0.03, yield_rate=0.01, volatility=0.32),
        'american': dict(option='Put', spot=100.0, strike=105.0, days=180, years=0.5, rate=0.03, yield_rate=0.01, volatility=0.25),
    }
    options = {}
    for name, p in inputs.items():
        maturity = today + p['days']
        assert dc.yearFraction(today, maturity) == p['years']
        spot = ql.SimpleQuote(p['spot'])
        process = ql.BlackScholesMertonProcess(ql.QuoteHandle(spot),
            ql.YieldTermStructureHandle(ql.FlatForward(today, p['yield_rate'], dc)),
            ql.YieldTermStructureHandle(ql.FlatForward(today, p['rate'], dc)),
            ql.BlackVolTermStructureHandle(ql.BlackConstantVol(today, cal, p['volatility'], dc)))
        exercise = ql.EuropeanExercise(maturity) if name == 'european' else ql.AmericanExercise(today, maturity)
        option = ql.VanillaOption(ql.PlainVanillaPayoff(ql.Option.Call if p['option'] == 'Call' else ql.Option.Put, p['strike']), exercise)
        option.setPricingEngine(ql.AnalyticEuropeanEngine(process) if name == 'european' else ql.BjerksundStenslandApproximationEngine(process))
        options[name] = option, process
        p['maturity'] = maturity.ISO()
        p['f64_bits'] = {k: struct.pack('>d', p[k]).hex() for k in ['spot','strike','years','rate','yield_rate','volatility']}
    quote = struct.unpack('>d', bytes.fromhex('40397df98ba57cc1'))[0]
    def price(option):
        option.recalculate()
        return option.NPV()
    def greeks(option):
        option.recalculate()
        return (option.delta(), option.gamma(), option.vega(), option.theta()/365.0, option.rho())
    eu, process = options['european']
    am, _ = options['american']
    operations = {
        'european_price': lambda: price(eu),
        'european_iv': lambda: eu.impliedVolatility(quote, process, 1e-10, 100, 1e-7, 4.0),
        'european_greeks': lambda: greeks(eu),
        'american_price': lambda: price(am),
        'american_greeks': lambda: greeks(am),
    }
    def observe():
        result = {key: fn() for key, fn in operations.items()}
        assert abs(result['european_price'] - quote) < 1e-10
        assert abs(result['european_iv'] - inputs['european']['volatility']) < 1e-9
        assert all(math.isfinite(x) for v in result.values() for x in (v if isinstance(v, tuple) else [v]))
        return result
    before = observe()
    distribution = importlib.metadata.distribution('QuantLib')
    binary = next(Path(distribution.locate_file(f)) for f in distribution.files if str(f).endswith('.so'))
    result = dict(version=ql.__version__, python=sys.version, platform=platform.platform(), wheel_metadata=distribution.read_text('WHEEL'), binary_sha256=hashlib.sha256(binary.read_bytes()).hexdigest(), evaluation_date=today.ISO(), day_counter='Actual/360', inputs=inputs, iv_quote_bits='40397df98ba57cc1', iv_solver=dict(accuracy=1e-10, max_evaluations=100, min_vol=1e-7, max_vol=4.0), greek_order=['delta','gamma','vega','theta_per_365_day','rho'], before=before, samples={})
    if not args.preflight:
        for key, fn in operations.items():
            # Calibrate outside retained samples; no overhead subtraction.
            iterations = 1000
            start = time.perf_counter_ns()
            for _ in range(iterations): fn()
            elapsed = time.perf_counter_ns() - start
            iterations = max(100, math.ceil(50_000_000 / (elapsed/iterations)))
            end = time.perf_counter() + 1.0
            while time.perf_counter() < end: fn()
            samples = []
            for _ in range(60):
                start = time.perf_counter_ns()
                for _ in range(iterations): fn()
                samples.append(time.perf_counter_ns() - start)
            result['samples'][key] = dict(iterations=iterations, elapsed_ns=samples, median_ns=statistics.median(t/iterations for t in samples))
    result['after'] = observe()
    assert result['before'] == result['after']
    print(json.dumps(result, indent=2))

if __name__ == '__main__':
    main()
