"""
PHOENIX quantum feasibility sweep -- shared harness.

Purpose (the honest mandate): screen EVERY simulable quantum / quantum-inspired
method on the untested list against a MANDATORY classical baseline, on real-shaped
PHOENIX fire-detection artifacts, and test whether STACKING several of them creates
any new value a single method (or the classical baseline) does not already provide.

Discipline (same as the research log):
  - every quantum method is scored head-to-head with a classical baseline on the
    same data by the same metric;
  - a shuffled-label PLACEBO accompanies every learned-separability screen;
  - nothing is promoted; "no advantage" is a valid, reported result;
  - feasibility screens run on a realistic SEVIRI-calibrated dataset (derived
    features ~ chance, full 11-band spectrum ~ 0.63 AUC, matching entry 0088);
    anything that beats classical here escalates to the REAL spec12 corpus on
    RunPod before any claim.

Compute: CPU only, small (n<=~120, <=8 qubits), laptop-light, load-aware. No QPU,
no DGX. Real-QPU is reached only if a screen survives the simulator.
"""
from __future__ import annotations
import time
import numpy as np
from dataclasses import dataclass, field
from sklearn.svm import SVC
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_predict
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import roc_auc_score


BANDS = ["ir039", "wv062", "wv073", "ir087", "ir097", "ir108",
         "ir120", "ir134", "vis006", "vis008", "nir016"]


# --------------------------------------------------------------------------
# Realistic SEVIRI-calibrated fire / non-fire dataset
# --------------------------------------------------------------------------
def make_seviri_dataset(n: int = 120, seed: int = 0, signal: float = 0.85):
    """
    11-band fire vs intensity-matched non-fire pixels.

    Construction goal (calibrated to entry 0088): the three DERIVED features
    (temperature ~ ir039, MIR-TIR ~ ir039-ir108, spatial-z proxy) do NOT separate
    (intensity-matched), but a genuine combustion direction spread across the IR
    bands gives a modest full-spectrum signal. Non-fire distractors (hot ground,
    sun-glint) mimic the derived features without the full spectral structure.
    """
    rng = np.random.default_rng(seed)
    half = n // 2
    # shared thermal background (intensity), identical distribution for both classes
    base_temp = rng.normal(295, 6, n)                     # K-ish
    X = np.zeros((n, 11))
    X[:, 0] = base_temp + rng.normal(0, 2, n)             # ir039 (MIR) driven by temp
    X[:, 5] = base_temp + rng.normal(0, 2, n)             # ir108 (TIR) driven by temp
    for b in [1, 2, 3, 4, 6, 7]:                          # other IR bands
        X[:, b] = base_temp * rng.uniform(0.9, 1.0) + rng.normal(0, 3, n)
    for b in [8, 9, 10]:                                  # visible/NIR: solar, class-blind
        X[:, b] = rng.normal(40, 12, n)
    y = np.zeros(n, int); y[:half] = 1
    rng.shuffle(y)
    # genuine combustion direction: kept BLIND to the derived features by giving ir039
    # (idx0) and ir108 (idx5) equal, small weight (so temperature and MIR-TIR net ~0),
    # and placing the real signal in the other IR/WV bands -> only the full spectrum reads it
    comb_dir = np.array([0.15, 0.30, 0.30, 0.50, 0.25, 0.15, 0.45, 0.40, 0.0, 0.0, 0.0])
    comb_dir = comb_dir / np.linalg.norm(comb_dir)
    X += signal * np.outer(y, comb_dir) * 6.0
    # intensity-matched distractors: a CLASS-BLIND subset (both fire and non-fire) get a
    # "hot" spike on ir039 & MIR-TIR -> intensity/temperature cannot read the label
    distract = rng.random(n) < 0.35
    X[distract, 0] += rng.uniform(3, 8, distract.sum())
    X[distract, 5] -= rng.uniform(0, 3, distract.sum())
    X += rng.normal(0, 1.5, X.shape)
    return X, y


def derived_features(X):
    """The three summary features PHOENIX historically scored on (entry 0088)."""
    temp = X[:, 0]
    mir_tir = X[:, 0] - X[:, 5]
    spatial_z = (X[:, 0] - X[:, 0].mean()) / (X[:, 0].std() + 1e-9)
    return np.stack([temp, mir_tir, spatial_z], axis=1)


# --------------------------------------------------------------------------
# Scoring helpers
# --------------------------------------------------------------------------
def cv_auc(model, X, y, seed: int = 0, folds: int = 5) -> float:
    skf = StratifiedKFold(n_splits=folds, shuffle=True, random_state=seed)
    try:
        if hasattr(model, "decision_function"):
            s = cross_val_predict(model, X, y, cv=skf, method="decision_function")
        else:
            s = cross_val_predict(model, X, y, cv=skf, method="predict_proba")[:, 1]
    except Exception:
        s = cross_val_predict(model, X, y, cv=skf, method="predict_proba")[:, 1]
    return float(roc_auc_score(y, s))


def auc_from_scores(y, scores) -> float:
    return float(roc_auc_score(y, scores))


def placebo_auc(model_fn, X, y, seed: int = 0) -> float:
    rng = np.random.default_rng(seed + 999)
    yp = y.copy(); rng.shuffle(yp)
    return cv_auc(model_fn(), X, yp, seed=seed)


@dataclass
class ScreenResult:
    name: str                 # method (list item)
    task: str
    quantum_metric: float
    classical_metric: float
    placebo: float | None
    metric_name: str
    latency_s: float
    verdict: str = ""
    extra: dict = field(default_factory=dict)

    def advantage(self, tol: float = 0.01) -> bool:
        return self.quantum_metric > self.classical_metric + tol


def classical_baselines(X, y, seed=0) -> dict:
    """Mandatory classical baselines: full-spectrum and derived-only."""
    Xs = StandardScaler().fit_transform(X)
    out = {
        "rbf_svm_full": cv_auc(SVC(kernel="rbf", C=2.0), Xs, y, seed),
        "gbm_full": cv_auc(GradientBoostingClassifier(random_state=seed), Xs, y, seed),
        "logreg_full": cv_auc(LogisticRegression(max_iter=500), Xs, y, seed),
        "rbf_svm_derived": cv_auc(SVC(kernel="rbf", C=2.0),
                                  StandardScaler().fit_transform(derived_features(X)), y, seed),
    }
    out["best_classical_full"] = max(out["rbf_svm_full"], out["gbm_full"], out["logreg_full"])
    return out


def verdict_line(r: ScreenResult) -> str:
    if r.advantage():
        return (f"POSSIBLE lift: {r.metric_name} {r.quantum_metric:.3f} vs classical "
                f"{r.classical_metric:.3f} (+{r.quantum_metric - r.classical_metric:.3f}) "
                f"-> escalate to real corpus before any claim.")
    return (f"No advantage: {r.metric_name} {r.quantum_metric:.3f} vs classical "
            f"{r.classical_metric:.3f}. Classical baseline holds.")
