"""
PHOENIX quantum feasibility sweep -- STACKING / hybridisation.

The core question: can COMBINING quantum + classical methods create value that no
single method (and no classical baseline) provides? We build out-of-fold (OOF)
predictions from several base learners and train a classical meta-learner on them
(stacked generalisation), then compare the stack's AUC to the best single base.

Bases:
  - classical GBM on the full 11-band spectrum        (the incumbent)
  - quantum fidelity-kernel SVC                        (item 58/158)
  - tensor-network (TT-SVD) compressed GBM             (item 63/161)
Meta: logistic regression on OOF base scores.

Honest rule: a stack is only "value" if it beats the best single base by more than
noise, reproduced across seeds. Otherwise the incumbent classical wins.
"""
from __future__ import annotations
import time
import numpy as np
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.decomposition import PCA
from sklearn.svm import SVC
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import roc_auc_score

from qsweep_core import make_seviri_dataset
from qsweep_more import _tt_features


def _quantum_kernel():
    from qiskit_machine_learning.kernels import FidelityQuantumKernel
    try:
        from qiskit.circuit.library import zz_feature_map
        fm = zz_feature_map(feature_dimension=6, reps=1)
    except Exception:
        from qiskit.circuit.library import ZZFeatureMap
        fm = ZZFeatureMap(feature_dimension=6, reps=1)
    return FidelityQuantumKernel(feature_map=fm)


def oof_predictions(X, y, seed=0, folds=4):
    """Out-of-fold scores for each base learner (no leakage)."""
    n = len(y)
    Xs = StandardScaler().fit_transform(X)
    Xk = MinMaxScaler((0, np.pi)).fit_transform(PCA(6, random_state=0).fit_transform(Xs))
    Xpad = np.hstack([Xs, np.zeros((n, 5))])
    TT = np.array([_tt_features(r, (2, 2, 2, 2), 3) for r in Xpad])
    qk = _quantum_kernel()

    oof = {"classical_gbm": np.zeros(n), "quantum_kernel": np.zeros(n), "tensor_net": np.zeros(n)}
    skf = StratifiedKFold(n_splits=folds, shuffle=True, random_state=seed)
    for tr, te in skf.split(Xs, y):
        g = GradientBoostingClassifier(random_state=seed).fit(Xs[tr], y[tr])
        oof["classical_gbm"][te] = g.predict_proba(Xs[te])[:, 1]
        Ktr = qk.evaluate(x_vec=Xk[tr]); Kte = qk.evaluate(x_vec=Xk[te], y_vec=Xk[tr])
        svc = SVC(kernel="precomputed", C=2.0).fit(Ktr, y[tr])
        oof["quantum_kernel"][te] = svc.decision_function(Kte)
        gt = GradientBoostingClassifier(random_state=seed).fit(TT[tr], y[tr])
        oof["tensor_net"][te] = gt.predict_proba(TT[te])[:, 1]
    return oof


def evaluate_stacking(n=80, seeds=(0, 1, 2)):
    t0 = time.perf_counter()
    rows = {"classical_gbm": [], "quantum_kernel": [], "tensor_net": [],
            "stack_all": [], "stack_q_plus_classical": []}
    for s in seeds:
        X, y = make_seviri_dataset(n, seed=s)
        oof = oof_predictions(X, y, seed=s)
        for k in ("classical_gbm", "quantum_kernel", "tensor_net"):
            rows[k].append(roc_auc_score(y, oof[k]))
        # stacked meta-learners (OOF-fair: meta trained via CV on the OOF features)
        F_all = np.stack([oof["classical_gbm"], oof["quantum_kernel"], oof["tensor_net"]], 1)
        F_qc = np.stack([oof["classical_gbm"], oof["quantum_kernel"]], 1)
        rows["stack_all"].append(_meta_auc(F_all, y, s))
        rows["stack_q_plus_classical"].append(_meta_auc(F_qc, y, s))
    agg = {k: (float(np.mean(v)), float(np.std(v))) for k, v in rows.items()}
    best_single = max(agg["classical_gbm"][0], agg["quantum_kernel"][0], agg["tensor_net"][0])
    best_stack = max(agg["stack_all"][0], agg["stack_q_plus_classical"][0])
    emergent = best_stack - best_single
    return {"per_method": agg, "best_single": round(best_single, 4),
            "best_stack": round(best_stack, 4), "emergent_lift": round(emergent, 4),
            "latency_s": round(time.perf_counter() - t0, 1),
            "verdict": ("Stacking beats best single base by "
                        f"{emergent:+.3f} AUC" if emergent > 0.01 else
                        "No emergent value: the stack does not beat the best single base "
                        "beyond noise; the classical incumbent holds.")}


def _meta_auc(F, y, seed, folds=4):
    skf = StratifiedKFold(n_splits=folds, shuffle=True, random_state=seed + 7)
    oof = np.zeros(len(y))
    for tr, te in skf.split(F, y):
        m = LogisticRegression(max_iter=500).fit(F[tr], y[tr])
        oof[te] = m.predict_proba(F[te])[:, 1]
    return float(roc_auc_score(y, oof))


if __name__ == "__main__":
    r = evaluate_stacking()
    import json
    print(json.dumps(r, indent=2))
