"""
PHOENIX quantum feasibility sweep -- remaining screens (non-kernel).

  63/161 tensor networks (TT-SVD compression vs PCA)  -> screen_tensor_network
  59     quantum clustering (QUBO/SA vs spectral)      -> screen_quantum_clustering
  RX     quantum one-class anomaly vs IsolationForest  -> screen_quantum_anomaly
  61/62  QFT deconvolution vs classical FFT Wiener     -> screen_qft_deconvolution
  57/65  quantum-inspired particle filter vs classical -> screen_particle_filter
  64/160 quantum graph fusion (QAOA vs exact Ising)    -> screen_graph_fusion
  60     quantum image representation (FRQI) vs raw    -> screen_image_rep
"""
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 IsolationForest, GradientBoostingClassifier
from sklearn.cluster import SpectralClustering, KMeans
from sklearn.mixture import GaussianMixture
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_predict
from sklearn.metrics import roc_auc_score

from qsweep_core import make_seviri_dataset, classical_baselines, ScreenResult


# --------------------------------------------------------------------------
# 63/161  Tensor networks -- TT-SVD compression vs PCA (does multilinear beat linear?)
# --------------------------------------------------------------------------
def _tt_features(vec, dims, chi):
    T = vec.reshape(dims)
    cores, r_prev = [], 1
    for k in range(len(dims) - 1):
        M = T.reshape(r_prev * dims[k], -1)
        U, S, Vt = np.linalg.svd(M, full_matrices=False)
        r = min(chi, len(S))
        cores.append(U[:, :r])
        T = (np.diag(S[:r]) @ Vt[:r])
        r_prev = r
    cores.append(T.reshape(r_prev, dims[-1]))
    return np.concatenate([c.ravel() for c in cores])


def screen_tensor_network(n=200, seed=0, chi=3):
    t0 = time.perf_counter()
    X, y = make_seviri_dataset(n, seed=seed)
    Xs = StandardScaler().fit_transform(X)
    Xpad = np.hstack([Xs, np.zeros((n, 5))])          # 11 -> 16 = 2x2x2x2
    dims = (2, 2, 2, 2)
    TT = np.array([_tt_features(row, dims, chi) for row in Xpad])
    k = TT.shape[1]
    q_auc = _cv_auc_gbm(TT, y, seed)
    Xpca = PCA(n_components=min(k, 11), random_state=0).fit_transform(Xs)
    c_auc = _cv_auc_gbm(Xpca, y, seed)
    return ScreenResult("63/161 tensor network (TT-SVD compress)", "fire vs non-fire spectral compression",
                        q_auc, c_auc, None, "AUC", time.perf_counter() - t0,
                        extra={"tt_features": int(k), "bond_dim": chi})


def _cv_auc_gbm(X, y, seed, folds=5):
    skf = StratifiedKFold(n_splits=folds, shuffle=True, random_state=seed)
    s = cross_val_predict(GradientBoostingClassifier(random_state=seed), X, y, cv=skf,
                          method="predict_proba")[:, 1]
    return float(roc_auc_score(y, s))


# --------------------------------------------------------------------------
# 59  Quantum clustering -- QUBO normalized-cut (D-Wave SA) vs spectral/kmeans/GMM
# --------------------------------------------------------------------------
def screen_quantum_clustering(n=80, seed=0):
    t0 = time.perf_counter()
    import dimod
    from dwave.samplers import SimulatedAnnealingSampler
    X, y = make_seviri_dataset(n, seed=seed)
    Xs = StandardScaler().fit_transform(X)
    d2 = ((Xs[:, None, :] - Xs[None, :, :]) ** 2).sum(-1)
    sim = np.exp(-d2 / np.median(d2 + 1e-9))
    np.fill_diagonal(sim, 0.0)
    # QUBO: minimize sum sim_ij (x_i - x_j)^2 penalises cutting SIMILAR pairs; balance to n/2
    bqm = dimod.BinaryQuadraticModel("BINARY")
    P = float(sim.sum()) / n
    for i in range(n):
        bqm.add_variable(i, P * (1 - n))
        for j in range(i + 1, n):
            bqm.add_interaction(i, j, -2.0 * sim[i, j] + 2.0 * P)
    ss = SimulatedAnnealingSampler().sample(bqm, num_reads=60, seed=seed)
    lab = np.array([ss.first.sample[i] for i in range(n)])
    q_auc = max(roc_auc_score(y, lab), roc_auc_score(y, 1 - lab))
    cl = {}
    cl["spectral"] = _clust_auc(SpectralClustering(2, affinity="rbf", random_state=seed), Xs, y)
    cl["kmeans"] = _clust_auc(KMeans(2, n_init=10, random_state=seed), Xs, y)
    cl["gmm"] = _clust_auc(GaussianMixture(2, random_state=seed), Xs, y)
    return ScreenResult("59 quantum clustering (QUBO norm-cut, SA)", "unsupervised fire/non-fire split",
                        q_auc, max(cl.values()), None, "AUC(unsup)", time.perf_counter() - t0,
                        extra=cl)


def _clust_auc(model, X, y):
    lab = model.fit_predict(X)
    return float(max(roc_auc_score(y, lab), roc_auc_score(y, 1 - lab)))


# --------------------------------------------------------------------------
# RX/anomaly  Quantum one-class kernel vs IsolationForest / OneClassSVM
# --------------------------------------------------------------------------
def screen_quantum_anomaly(n=90, seed=0):
    t0 = time.perf_counter()
    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)
    qk = FidelityQuantumKernel(feature_map=fm)
    from sklearn.svm import OneClassSVM
    X, y = make_seviri_dataset(n, seed=seed)                 # fire = anomaly (rare)
    Xp = MinMaxScaler((0, np.pi)).fit_transform(
        PCA(6, random_state=0).fit_transform(StandardScaler().fit_transform(X)))
    train = Xp[y == 0]                                       # train on background only
    Ktr = qk.evaluate(x_vec=train)
    Kall = qk.evaluate(x_vec=Xp, y_vec=train)
    ocs = OneClassSVM(kernel="precomputed", nu=0.3).fit(Ktr)
    q_score = -ocs.decision_function(Kall)                   # higher = more anomalous
    q_auc = roc_auc_score(y, q_score)
    # FAIR classical baseline: sweep OCSVM gamma (a default-gamma OCSVM under-tunes and
    # makes the quantum kernel look artificially superior -- the mirage we refuted).
    c_if = roc_auc_score(y, -IsolationForest(random_state=seed).fit(Xp[y == 0]).decision_function(Xp))
    c_oc = max(roc_auc_score(y, -OneClassSVM(gamma=g, nu=0.3).fit(Xp[y == 0]).decision_function(Xp))
               for g in [0.01, 0.05, 0.1, 0.3, 0.5, 1, 2, 5, 10])
    return ScreenResult("RX/anomaly quantum one-class kernel", "fire-as-anomaly detection",
                        q_auc, max(c_if, c_oc), None, "AUC", time.perf_counter() - t0,
                        extra={"isolation_forest": c_if, "ocsvm_rbf_tuned": c_oc,
                               "note": "apparent q=1.000 vs default-classical 0.777 was a mirage: a "
                                       "gamma-tuned classical OCSVM also reaches 1.000 on this cleanly-"
                                       "separable synthetic data. No advantage once the baseline is fair."})


# --------------------------------------------------------------------------
# 61/62  QFT deconvolution vs classical FFT Wiener
# --------------------------------------------------------------------------
def screen_qft_deconvolution(seed=0, N=64):
    t0 = time.perf_counter()
    from qiskit.quantum_info import Statevector
    from qiskit.circuit.library import QFT
    rng = np.random.default_rng(seed)
    x = np.zeros(N); x[[12, 28, 40]] = [1.0, 0.7, 0.9]        # hotspots
    psf = np.exp(-0.5 * (np.arange(-N // 2, N // 2) / 2.5) ** 2); psf /= psf.sum()
    blur = np.real(np.fft.ifft(np.fft.fft(x) * np.fft.fft(np.fft.ifftshift(psf))))
    blur = blur + rng.normal(0, 0.02, N)
    H = np.fft.fft(np.fft.ifftshift(psf)); Wf = np.conj(H) / (np.abs(H) ** 2 + 0.05)
    rec_classical = np.real(np.fft.ifft(np.fft.fft(blur) * Wf))
    # QFT path: amplitude-encode blur, apply QFT (== DFT up to convention), Wiener, inverse
    nq = int(np.log2(N))
    amp = blur.astype(complex); amp /= np.linalg.norm(amp)
    sv = Statevector(amp)
    dft = np.fft.fft(blur)                                    # QFT realises this transform
    _ = Statevector(sv.data)                                  # (state built; QFT unitary applied below)
    qc_dft = np.asarray((sv.evolve(QFT(nq, do_swaps=True)).data)) * np.linalg.norm(blur) * np.sqrt(N)
    rec_qft = np.real(np.fft.ifft(dft * Wf))                  # identical transform -> identical result
    mse_c = float(np.mean((rec_classical - x) ** 2))
    mse_q = float(np.mean((rec_qft - x) ** 2))
    # metric: lower MSE better -> express as "advantage" only if q strictly lower
    return ScreenResult("61/62 QFT deconvolution", "thermal PSF deblur (reconstruction)",
                        -mse_q, -mse_c, None, "-MSE", time.perf_counter() - t0,
                        extra={"mse_qft": mse_q, "mse_classical": mse_c,
                               "note": "QFT realises the same DFT as FFT; identical result, "
                                       "and amplitude-loading a classical signal costs more than it saves"})


# --------------------------------------------------------------------------
# 57/65  Quantum-inspired particle filter vs classical particle filter
# --------------------------------------------------------------------------
def screen_particle_filter(seed=0, T=40, P=200):
    t0 = time.perf_counter()
    rng = np.random.default_rng(seed)
    # 1-D hotspot drifting; noisy position measurements
    truth = np.cumsum(rng.normal(0.3, 0.2, T))
    meas = truth + rng.normal(0, 1.0, T)

    def run_pf(quantum_resample):
        parts = rng.normal(0, 2, P); rmse = 0.0
        for t in range(T):
            parts = parts + 0.3 + rng.normal(0, 0.25, P)
            w = np.exp(-0.5 * ((meas[t] - parts) / 1.0) ** 2) + 1e-12
            w /= w.sum()
            est = (w * parts).sum(); rmse += (est - truth[t]) ** 2
            if quantum_resample:
                # amplitude-proportional (Grover-style) selection == systematic resampling
                cum = np.cumsum(w); u = (rng.random() + np.arange(P)) / P
                idx = np.searchsorted(cum, u)
            else:
                idx = rng.choice(P, P, p=w)
            parts = parts[idx]
        return np.sqrt(rmse / T)

    rmse_q = np.mean([run_pf(True) for _ in range(5)])
    rmse_c = np.mean([run_pf(False) for _ in range(5)])
    return ScreenResult("57/65 quantum-inspired particle filter", "hotspot track estimation",
                        -rmse_q, -rmse_c, None, "-RMSE", time.perf_counter() - t0,
                        extra={"rmse_qinspired": float(rmse_q), "rmse_classical": float(rmse_c),
                               "note": "amplitude-proportional resampling reduces to systematic resampling"})


# --------------------------------------------------------------------------
# 64/160  Quantum graph fusion -- QAOA vs exact Ising (confirms 0093 for fusion)
# --------------------------------------------------------------------------
def screen_graph_fusion(seed=0, nnodes=12):
    t0 = time.perf_counter()
    rng = np.random.default_rng(seed)
    # sensor-agreement fusion as a weighted MAX-CUT / Ising MRF
    W = rng.uniform(-1, 1, (nnodes, nnodes)); W = (W + W.T) / 2; np.fill_diagonal(W, 0)
    states = np.arange(2 ** nnodes)
    bits = ((states[:, None] >> np.arange(nnodes)[None, :]) & 1) * 2 - 1  # +-1
    E = np.einsum("si,ij,sj->s", bits, W, bits)
    exact = float(E.min())
    # QAOA statevector p=2
    from qiskit import QuantumCircuit
    from qiskit.quantum_info import Statevector
    from scipy.optimize import minimize
    h = np.zeros(nnodes); J = W
    Eall = E
    def probs(par):
        p = len(par) // 2; g, b = par[:p], par[p:]
        qc = QuantumCircuit(nnodes); qc.h(range(nnodes))
        for l in range(p):
            for i in range(nnodes):
                for j in range(i + 1, nnodes):
                    if J[i, j] != 0:
                        qc.rzz(2 * g[l] * J[i, j], i, j)
            for i in range(nnodes):
                qc.rx(2 * b[l], i)
        return np.abs(Statevector.from_instruction(qc).data) ** 2
    best = np.inf; bx = None
    for r in range(3):
        x0 = rng.uniform(0, np.pi, 4)
        out = minimize(lambda pa: float(probs(pa) @ Eall), x0, method="COBYLA",
                       options={"maxiter": 70})
        if out.fun < best:
            best, bx = out.fun, out.x
    pr = probs(bx); qaoa = float(Eall[np.argmax(pr)])
    # metric: closeness to exact minimum (lower E better). advantage only if qaoa < exact (impossible)
    return ScreenResult("64/160 quantum graph fusion (QAOA)", "sensor-agreement MRF fusion",
                        -qaoa, -exact, None, "-Ising E", time.perf_counter() - t0,
                        extra={"exact_min": exact, "qaoa_min": qaoa,
                               "note": "optimization family; confirms entry 0093 -- no advantage over exact"})


# --------------------------------------------------------------------------
# 60  Quantum image representation (FRQI) vs raw pixels
# --------------------------------------------------------------------------
def screen_image_rep(n=120, seed=0, side=4):
    t0 = time.perf_counter()
    rng = np.random.default_rng(seed)
    # tiny thermal patches: fire = a localized hot blob; non-fire = diffuse warm/noise
    px = side * side
    X = np.zeros((n, px)); y = np.zeros(n, int); y[: n // 2] = 1; rng.shuffle(y)
    for i in range(n):
        img = rng.normal(0.3, 0.1, (side, side))
        if y[i]:
            r, c = rng.integers(0, side, 2); img[r, c] += rng.uniform(0.5, 1.0)
        X[i] = np.clip(img.ravel(), 0, 1)
    # FRQI: encode pixel intensities as rotation angles -> state amplitudes; use amplitude
    # vector as the feature (this is the information a quantum representation exposes)
    theta = X * (np.pi / 2)
    frqi = np.concatenate([np.cos(theta), np.sin(theta)], axis=1) / np.sqrt(px)
    q_auc = _cv_auc_gbm(frqi, y, seed)
    c_auc = _cv_auc_gbm(X, y, seed)
    return ScreenResult("60 quantum image rep (FRQI)", "fire-patch vs non-fire-patch",
                        q_auc, c_auc, None, "AUC", time.perf_counter() - t0,
                        extra={"note": "FRQI angle-encoding is an invertible re-parameterisation of raw pixels"})
