"""
Test runner for all Python code snippets in docs/alpha-research.mdx.

Each snippet is wrapped in a try/except that prints PASS/FAIL.
Snippets requiring network access, real exchange connections, or hz.run() are skipped.
"""

import traceback

import horizon as hz

PASS_COUNT = 0
FAIL_COUNT = 0
SKIP_COUNT = 0


def record(name, passed, error=None):
    global PASS_COUNT, FAIL_COUNT
    if passed:
        PASS_COUNT += 1
        print(f"  PASS: {name}")
    else:
        FAIL_COUNT += 1
        print(f"  FAIL: {name}")
        if error:
            print(f"        {error}")


def skip(name, reason):
    global SKIP_COUNT
    SKIP_COUNT += 1
    print(f"  SKIP: {name} -- {reason}")


# ============================================================================
# Snippet 1: Import meta_label module
# ============================================================================
print("\n--- Snippet 1: Import meta_label module ---")
try:
    from horizon.meta_label import compute_meta_labels, meta_label_pipeline
    record("import meta_label", True)
except Exception as e:
    record("import meta_label", False, str(e))

# ============================================================================
# Snippet 2: compute_meta_labels basic usage
# ============================================================================
print("\n--- Snippet 2: compute_meta_labels ---")
try:
    labels = compute_meta_labels(
        prices=[100, 101, 102, 99, 98, 103, 105],
        timestamps=[0, 1, 2, 3, 4, 5, 6],
        primary_signals=[(0, 1), (3, -1)],  # (event_index, side)
        pt_sl=(1.0, 1.0),
        max_holding=5,
        vol_span=20,
    )

    for label in labels:
        print(f"    idx={label.event_idx} side={label.primary_side} "
              f"label={label.meta_label} ret={label.ret:.4f} "
              f"conf={label.confidence:.2f}")

    # Verify basic properties
    assert len(labels) == 2, f"Expected 2 labels, got {len(labels)}"
    assert labels[0].event_idx == 0
    assert labels[0].primary_side == 1
    assert labels[1].event_idx == 3
    assert labels[1].primary_side == -1
    assert labels[0].meta_label in (0, 1)
    assert labels[1].meta_label in (0, 1)
    assert 0.0 <= labels[0].confidence <= 1.0
    assert 0.0 <= labels[1].confidence <= 1.0
    record("compute_meta_labels", True)
except Exception as e:
    record("compute_meta_labels", False, f"{type(e).__name__}: {e}")

# ============================================================================
# Snippet 3: meta_label_pipeline in hz.run() -- SKIP (requires hz.run)
# ============================================================================
print("\n--- Snippet 3: meta_label_pipeline in hz.run ---")
skip("meta_label_pipeline hz.run", "requires hz.run() with live pipeline")

# ============================================================================
# Snippet 4: Import feature_importance module
# ============================================================================
print("\n--- Snippet 4: Import feature_importance module ---")
try:
    from horizon.feature_importance import (
        mda_importance,
        sfi_importance,
        clustered_mda,
        FeatureImportance,
    )
    record("import feature_importance", True)
except Exception as e:
    record("import feature_importance", False, str(e))

# ============================================================================
# Snippet 5: mda_importance with scorer
# ============================================================================
print("\n--- Snippet 5: mda_importance ---")
try:
    import random
    random.seed(42)

    # Create synthetic feature matrix (100 samples x 4 features)
    feature_matrix = [
        [random.gauss(0, 1) for _ in range(4)]
        for _ in range(100)
    ]
    # Labels: sign of first feature (momentum) -- so momentum should be most important
    labels_data = [1.0 if row[0] > 0 else 0.0 for row in feature_matrix]

    def my_scorer(X_train, y_train, X_test, y_test):
        from collections import Counter
        majority = Counter(y_train).most_common(1)[0][0]
        return sum(1 for y in y_test if y == majority) / len(y_test)

    results = mda_importance(
        score_fn=my_scorer,
        X=feature_matrix,
        y=labels_data,
        feature_names=["momentum", "vol", "spread", "imbalance"],
        n_splits=5,
        purge_gap=10,
        seed=42,
    )

    for fi in results:
        print(f"    {fi.feature}: {fi.importance:.4f} +/- {fi.std:.4f}")

    assert len(results) == 4, f"Expected 4 results, got {len(results)}"
    assert all(isinstance(fi, FeatureImportance) for fi in results)
    assert all(hasattr(fi, 'feature') and hasattr(fi, 'importance') and hasattr(fi, 'std') for fi in results)
    record("mda_importance", True)
except Exception as e:
    record("mda_importance", False, f"{type(e).__name__}: {e}")

# ============================================================================
# Snippet 6: sfi_importance
# ============================================================================
print("\n--- Snippet 6: sfi_importance ---")
try:
    results = sfi_importance(
        score_fn=my_scorer,
        X=feature_matrix,
        y=labels_data,
        feature_names=["momentum", "vol", "spread", "imbalance"],
        n_splits=5,
    )

    for fi in results:
        print(f"    {fi.feature}: {fi.importance:.4f} +/- {fi.std:.4f}")

    assert len(results) == 4, f"Expected 4 results, got {len(results)}"
    assert all(isinstance(fi, FeatureImportance) for fi in results)
    record("sfi_importance", True)
except Exception as e:
    record("sfi_importance", False, f"{type(e).__name__}: {e}")

# ============================================================================
# Snippet 7: clustered_mda
# ============================================================================
print("\n--- Snippet 7: clustered_mda ---")
try:
    results = clustered_mda(
        score_fn=my_scorer,
        X=feature_matrix,
        y=labels_data,
        feature_names=["momentum", "vol", "spread", "imbalance"],
        n_clusters=2,
        n_splits=5,
        seed=42,
    )

    # Each result.feature contains comma-separated names of features in the cluster
    for fi in results:
        print(f"    Cluster [{fi.feature}]: {fi.importance:.4f} +/- {fi.std:.4f}")

    assert len(results) == 2, f"Expected 2 cluster results, got {len(results)}"
    assert all(isinstance(fi, FeatureImportance) for fi in results)
    record("clustered_mda", True)
except Exception as e:
    record("clustered_mda", False, f"{type(e).__name__}: {e}")

# ============================================================================
# Snippet 8: Import alpha_decay module
# ============================================================================
print("\n--- Snippet 8: Import alpha_decay module ---")
try:
    from horizon.alpha_decay import AlphaDecayTracker, alpha_decay_pipeline, AlphaDecayReport
    record("import alpha_decay", True)
except Exception as e:
    record("import alpha_decay", False, str(e))

# ============================================================================
# Snippet 9: AlphaDecayTracker usage
# ============================================================================
print("\n--- Snippet 9: AlphaDecayTracker ---")
try:
    tracker = AlphaDecayTracker(window=100, alert_threshold=-0.05)

    # Feed new observations
    report = tracker.update(
        predictions=[0.6, 0.7, 0.55],
        outcomes=[1.0, 0.0, 1.0],
    )

    # With only 3 observations and window=100, report may be None (need window//2 = 50)
    # Let's add enough data to get a report
    random.seed(123)
    for i in range(100):
        preds = [random.random() for _ in range(5)]
        outs = [1.0 if random.random() > 0.5 else 0.0 for _ in range(5)]
        report = tracker.update(predictions=preds, outcomes=outs, timestamp=float(i))

    assert report is not None, "Expected a report after 500+ observations"
    print(f"    current_ic={report.current_ic:.3f}")
    print(f"    half_life={report.half_life:.1f}")
    print(f"    ic_trend={report.ic_trend:.4f}")
    print(f"    is_decaying={report.is_decaying}")

    if report and report.is_decaying:
        print(f"    Alpha decaying! IC={report.current_ic:.3f}, "
              f"half_life={report.half_life:.1f}")

    # Verify report fields
    assert isinstance(report, AlphaDecayReport)
    assert isinstance(report.current_ic, float)
    assert isinstance(report.rolling_ic, list)
    assert isinstance(report.half_life, float)
    assert isinstance(report.ic_trend, float)
    assert isinstance(report.is_decaying, bool)
    assert isinstance(report.rolling_sharpe, list)
    assert isinstance(report.sharpe_trend, float)
    record("AlphaDecayTracker", True)
except Exception as e:
    record("AlphaDecayTracker", False, f"{type(e).__name__}: {e}")

# ============================================================================
# Snippet 10: alpha_decay_pipeline in hz.run() -- SKIP (requires hz.run)
# ============================================================================
print("\n--- Snippet 10: alpha_decay_pipeline in hz.run ---")
skip("alpha_decay_pipeline hz.run", "requires hz.run() with live pipeline")

# ============================================================================
# Snippet 11: Import pnl_attribution module
# ============================================================================
print("\n--- Snippet 11: Import pnl_attribution module ---")
try:
    from horizon.pnl_attribution import (
        attribute_pnl,
        attribute_by_time,
        attribute_by_factor,
        pnl_attribution_pipeline,
        AttributionReport,
        PnLBreakdown,
        TimeBreakdown,
        FactorBreakdown,
    )
    record("import pnl_attribution", True)
except Exception as e:
    record("import pnl_attribution", False, str(e))

# ============================================================================
# Snippet 12: attribute_pnl(engine)
# ============================================================================
print("\n--- Snippet 12: attribute_pnl ---")
try:
    engine = hz.Engine()

    # Create some fills so we have positions
    # We need to use engine.process_fill() to build positions
    fill1 = hz.Fill(
        fill_id="f1",
        order_id="p1",
        market_id="btc-100k",
        side=hz.Side.Yes,
        order_side=hz.OrderSide.Buy,
        price=0.60,
        size=10.0,
        timestamp=1700000000.0,
        fee=0.0,
    )
    fill2 = hz.Fill(
        fill_id="f2",
        order_id="p2",
        market_id="eth-5k",
        side=hz.Side.Yes,
        order_side=hz.OrderSide.Buy,
        price=0.40,
        size=5.0,
        timestamp=1700000100.0,
        fee=0.0,
    )
    engine.process_fill(fill1)
    engine.process_fill(fill2)

    # Update mark prices so we get unrealized PnL
    engine.update_mark_price("btc-100k", hz.Side.Yes, 0.65)
    engine.update_mark_price("eth-5k", hz.Side.Yes, 0.35)

    report = attribute_pnl(engine)

    print(f"    Total PnL: {report.total_pnl:+.2f} across {report.n_positions} positions")
    for mkt in report.by_market:
        print(f"      {mkt.market_id}: {mkt.pnl:+.4f} ({mkt.contribution:.1%})")

    assert isinstance(report, AttributionReport)
    assert report.n_positions >= 2
    assert len(report.by_market) >= 2
    record("attribute_pnl", True)
except Exception as e:
    record("attribute_pnl", False, f"{type(e).__name__}: {e}\n{traceback.format_exc()}")

# ============================================================================
# Snippet 13: attribute_by_time
# ============================================================================
print("\n--- Snippet 13: attribute_by_time ---")
try:
    # Create synthetic fill objects since engine.fills() doesn't exist
    # Use the fills we already created via simple namespace objects
    class FakeFill:
        def __init__(self, timestamp, price, size, order_side, pnl=None):
            self.timestamp = timestamp
            self.price = price
            self.size = size
            self.order_side = order_side
            self.pnl = pnl

    fills = [
        FakeFill(timestamp=1700000000.0, price=0.60, size=10.0, order_side=hz.OrderSide.Buy),
        FakeFill(timestamp=1700000100.0, price=0.65, size=5.0, order_side=hz.OrderSide.Sell),
        FakeFill(timestamp=1700086400.0, price=0.40, size=5.0, order_side=hz.OrderSide.Buy),
    ]

    daily = attribute_by_time(fills, period="daily")

    for day in daily:
        print(f"    {day.period}: PnL={day.pnl:+.4f}, trades={day.n_trades}, "
              f"win_rate={day.win_rate:.1%}")

    assert isinstance(daily, list)
    assert all(isinstance(d, TimeBreakdown) for d in daily)
    assert len(daily) >= 1
    record("attribute_by_time", True)
except Exception as e:
    record("attribute_by_time", False, f"{type(e).__name__}: {e}\n{traceback.format_exc()}")

# ============================================================================
# Snippet 14: attribute_by_factor
# ============================================================================
print("\n--- Snippet 14: attribute_by_factor ---")
try:
    positions = engine.positions()

    factor_exposures = {
        "crypto": {"btc-100k": 0.8, "eth-5k": 0.9},
        "politics": {"trump-win": 1.0, "senate-flip": 0.7},
    }

    factors = attribute_by_factor(positions, factor_exposures)
    for f in factors:
        print(f"    {f.factor}: exposure={f.exposure:.2f}, "
              f"pnl={f.pnl_contribution:+.4f}, R2={f.r_squared:.3f}")

    assert isinstance(factors, list)
    assert all(isinstance(f, FactorBreakdown) for f in factors)
    # Should have 2 factors
    assert len(factors) == 2
    record("attribute_by_factor", True)
except Exception as e:
    record("attribute_by_factor", False, f"{type(e).__name__}: {e}\n{traceback.format_exc()}")

# ============================================================================
# Snippet 15: pnl_attribution_pipeline in hz.run() -- SKIP (requires hz.run)
# ============================================================================
print("\n--- Snippet 15: pnl_attribution_pipeline in hz.run ---")
skip("pnl_attribution_pipeline hz.run", "requires hz.run() with live pipeline")

# ============================================================================
# Snippet 16: Full Research Workflow
# ============================================================================
print("\n--- Snippet 16: Full Research Workflow ---")
try:
    import horizon as hz
    from horizon.meta_label import compute_meta_labels
    from horizon.feature_importance import mda_importance, clustered_mda
    from horizon.alpha_decay import AlphaDecayTracker
    from horizon.pnl_attribution import attribute_pnl, attribute_by_time

    # Create synthetic data for the full workflow
    random.seed(99)
    price_series = [100.0]
    for i in range(199):
        price_series.append(price_series[-1] * (1 + random.gauss(0, 0.01)))
    ts_series = list(range(200))

    # Generate primary signals at various indices
    signals = [(10, 1), (30, -1), (50, 1), (70, -1), (90, 1),
               (110, -1), (130, 1), (150, -1), (170, 1)]

    # 1. Meta-label your primary signals
    labels = compute_meta_labels(
        prices=price_series,
        timestamps=ts_series,
        primary_signals=signals,
        pt_sl=(1.5, 1.0),      # Asymmetric: wider profit target
        max_holding=50,
    )
    hit_rate = sum(1 for lb in labels if lb.meta_label == 1) / len(labels)
    print(f"    Meta-label hit rate: {hit_rate:.1%}")

    # 2. Rank your features
    # Build feature matrix matching the labels
    n_labels = len(labels)
    features = [
        [random.gauss(0, 1) for _ in range(4)]
        for _ in range(n_labels)
    ]
    feature_names = ["momentum", "vol", "spread", "imbalance"]

    # Need at least n_splits * 2 samples; with 9 labels, use n_splits=2
    n_splits_workflow = min(5, max(2, n_labels // 2))

    importances = mda_importance(
        score_fn=my_scorer,
        X=features,
        y=[lb.meta_label for lb in labels],
        feature_names=feature_names,
        purge_gap=0,
        n_splits=n_splits_workflow,
    )
    print(f"    Top features: {[fi.feature for fi in importances[:3]]}")

    # 3. Track alpha decay
    tracker = AlphaDecayTracker(window=200)
    # Generate enough data for a report
    predictions = [random.random() for _ in range(200)]
    outcomes = [1.0 if random.random() > 0.5 else 0.0 for _ in range(200)]
    report = tracker.update(predictions, outcomes)
    if report and report.is_decaying:
        print(f"    Edge decaying: half_life={report.half_life:.0f} obs")
    elif report:
        print(f"    Edge stable: IC={report.current_ic:.3f}, half_life={report.half_life:.0f} obs")
    else:
        print("    Not enough data for decay report")

    # 4. Attribute PnL -- use the engine from snippet 12
    attr = attribute_pnl(engine)
    if attr.by_market:
        print(f"    Top contributor: {attr.by_market[0].market_id} "
              f"({attr.by_market[0].contribution:.1%} of total)")
    else:
        print("    No positions to attribute")

    record("full research workflow", True)
except Exception as e:
    record("full research workflow", False, f"{type(e).__name__}: {e}\n{traceback.format_exc()}")

# ============================================================================
# Summary
# ============================================================================
print("\n" + "=" * 60)
print(f"SUMMARY: {PASS_COUNT} passed, {FAIL_COUNT} failed, {SKIP_COUNT} skipped")
print(f"Total snippets: {PASS_COUNT + FAIL_COUNT + SKIP_COUNT}")
print("=" * 60)
