"use client";

import type { CSSProperties } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import Link from "next/link";

import { optimizePortfolio } from "@/lib/api";
import { FONT } from "@/lib/theme";
import { MAX_IBM_VQE_ASSETS } from "@/lib/quantumPortfolioJobs";
import {
  clipNormalizeWeights,
  portfolioMetricsFromWeights,
  runOptimisation,
} from "@/lib/simulationEngine";

export type LabData = {
  assets: Array<{
    name: string;
    annReturn: number;
    annVol: number;
    sector: string;
  }>;
  corr: number[][];
};

export type SidebarSnapshot = {
  objective: string;
  weightMin: number;
  weightMax: number;
  seed: number;
  cardinality: number | null;
  kScreen: number | null;
  kSelect: number | null;
};

export type ObjectiveOption = {
  value: string;
  label: string;
  group: string;
};

export type LabContext = {
  marketMode: "live" | "synthetic";
  isLiveLoaded: boolean;
  ibmConnected: boolean;
  nAssets: number;
};

type Theme = Record<string, string>;

type Props = {
  data: LabData | null;
  theme: Theme;
  objectiveOptions: ObjectiveOption[];
  sidebar: SidebarSnapshot;
  labContext?: LabContext;
};

type BenchSpec = {
  objective: string;
  weightMin: number;
  weightMax: number;
  seed: number;
  K: string;
  kScreen: string;
  kSelect: string;
  nLayers: number;
  nRestarts: number;
  lambdaRisk: number;
  gamma: number;
};

type MetricsLite = {
  sharpe: number;
  portReturn: number;
  portVol: number;
  nActive: number;
};

type Snapshot = {
  spec: BenchSpec;
  weights: number[];
  metrics: MetricsLite | null;
  hypothesisLabel: string;
  ts: string;
};

const SHARPE_CLOSE_EPS = 0.02;
const SHARPE_LARGE_GAP = 0.15;

function emptySpec(sidebar: SidebarSnapshot): BenchSpec {
  return {
    objective: sidebar.objective,
    weightMin: sidebar.weightMin,
    weightMax: sidebar.weightMax,
    seed: sidebar.seed,
    K: sidebar.cardinality != null ? String(sidebar.cardinality) : "",
    kScreen: sidebar.kScreen != null ? String(sidebar.kScreen) : "",
    kSelect: sidebar.kSelect != null ? String(sidebar.kSelect) : "",
    nLayers: 3,
    nRestarts: 8,
    lambdaRisk: 1,
    gamma: 8,
  };
}

function specDiff(before: BenchSpec, after: BenchSpec): string[] {
  const out: string[] = [];
  if (before.objective !== after.objective) {
    out.push(`objective: ${before.objective} → ${after.objective}`);
  }
  if (before.weightMin !== after.weightMin) {
    out.push(
      `w_min: ${(before.weightMin * 100).toFixed(1)}% → ${(after.weightMin * 100).toFixed(1)}%`,
    );
  }
  if (before.weightMax !== after.weightMax) {
    out.push(
      `w_max: ${(before.weightMax * 100).toFixed(0)}% → ${(after.weightMax * 100).toFixed(0)}%`,
    );
  }
  if (before.seed !== after.seed) {
    out.push(`seed: ${before.seed} → ${after.seed}`);
  }
  const keys: (keyof BenchSpec)[] = [
    "K",
    "kScreen",
    "kSelect",
    "nLayers",
    "nRestarts",
    "lambdaRisk",
    "gamma",
  ];
  for (const k of keys) {
    if (before[k] !== after[k]) {
      out.push(`${k}: ${before[k]} → ${after[k]}`);
    }
  }
  if (out.length === 0) out.push("No field changes vs previous spec");
  return out;
}

function maxWeightDelta(a: number[], b: number[]): number {
  if (a.length !== b.length) return Infinity;
  let m = 0;
  for (let i = 0; i < a.length; i++) {
    m = Math.max(m, Math.abs(a[i] - b[i]));
  }
  return m;
}

function metricsLiteFromWeights(
  w: number[],
  data: LabData,
): MetricsLite | null {
  if (!data.assets.length || w.length !== data.assets.length) return null;
  const m = portfolioMetricsFromWeights(w, data);
  return {
    sharpe: m.sharpe,
    portReturn: m.portReturn,
    portVol: m.portVol,
    nActive: m.nActive,
  };
}

export default function SensitivityLabPanel({
  data,
  theme: t,
  objectiveOptions,
  sidebar,
  labContext,
}: Props) {
  const [hypothesisLabel, setHypothesisLabel] = useState("");
  const [spec, setSpec] = useState<BenchSpec>(() => emptySpec(sidebar));
  const [weights, setWeights] = useState<number[]>([]);
  const [weightsDirty, setWeightsDirty] = useState(false);
  const [lastSource, setLastSource] = useState<"manual" | "client" | "api">(
    "manual",
  );
  const [syncDiffChips, setSyncDiffChips] = useState<string[]>([]);
  const [lastClientMetrics, setLastClientMetrics] = useState<MetricsLite | null>(
    null,
  );
  const [lastApiMetrics, setLastApiMetrics] = useState<MetricsLite | null>(null);
  const [snapshotA, setSnapshotA] = useState<Snapshot | null>(null);
  const [snapshotB, setSnapshotB] = useState<Snapshot | null>(null);
  const [checklistDismissed, setChecklistDismissed] = useState(false);
  const [quantumMeta, setQuantumMeta] = useState<Record<
    string,
    unknown
  > | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [clientBusy, setClientBusy] = useState(false);
  const [apiBusy, setApiBusy] = useState(false);

  const n = data?.assets?.length ?? 0;

  const universeKey = useMemo(
    () => data?.assets?.map((a) => a.name).join("|") ?? "",
    [data],
  );

  useEffect(() => {
    if (!data?.assets?.length) {
      setWeights([]);
      setWeightsDirty(false);
      return;
    }
    const nn = data.assets.length;
    setWeights(Array.from({ length: nn }, () => 1 / nn));
    setWeightsDirty(false);
  }, [universeKey]);

  const metrics = useMemo(() => {
    if (!data?.assets?.length || weights.length !== data.assets.length)
      return null;
    return portfolioMetricsFromWeights(weights, data);
  }, [data, weights]);

  const sharpeCompareNote = useMemo(() => {
    if (!lastClientMetrics || !lastApiMetrics) return null;
    const d = Math.abs(lastClientMetrics.sharpe - lastApiMetrics.sharpe);
    if (d < SHARPE_CLOSE_EPS) {
      return "Close match on Sharpe — caps may not bind, or client surrogate aligns with the server on this Σ.";
    }
    if (d > SHARPE_LARGE_GAP) {
      return "Large Sharpe gap between quick sim and full optimizer — expected when objectives differ or the client path is a surrogate; trust API for production-style weights.";
    }
    return null;
  }, [lastClientMetrics, lastApiMetrics]);

  const confirmDestructive = useCallback((message: string) => {
    if (typeof window === "undefined") return true;
    return window.confirm(message);
  }, []);

  const syncFromSidebar = useCallback(() => {
    if (
      weightsDirty &&
      !confirmDestructive(
        "Sync from sidebar replaces the bench spec with the sidebar. Current weights stay until you re-run Quick sim or Full optimizer. Continue?",
      )
    ) {
      return;
    }
    const before = spec;
    const after = emptySpec(sidebar);
    setSyncDiffChips(specDiff(before, after));
    setSpec(after);
    setLastSource("manual");
    setQuantumMeta(null);
    setWeightsDirty(false);
  }, [spec, sidebar, weightsDirty, confirmDestructive]);

  const applyBounds = useCallback(() => {
    if (!data?.assets?.length) return;
    if (
      weightsDirty &&
      !confirmDestructive(
        "Clip + normalize will change weights. Continue?",
      )
    ) {
      return;
    }
    setWeights((w) =>
      clipNormalizeWeights(w, spec.weightMin, spec.weightMax),
    );
    setLastSource("manual");
    setWeightsDirty(true);
  }, [data, spec.weightMin, spec.weightMax, weightsDirty, confirmDestructive]);

  const setEqualWeights = useCallback(() => {
    if (!data?.assets?.length) return;
    if (
      weightsDirty &&
      !confirmDestructive(
        "Replace weights with equal weight? Current edits will be overwritten.",
      )
    ) {
      return;
    }
    const nn = data.assets.length;
    const eq = Array.from({ length: nn }, () => 1 / nn);
    setWeights(clipNormalizeWeights(eq, spec.weightMin, spec.weightMax));
    setLastSource("manual");
    setWeightsDirty(true);
  }, [data, spec.weightMin, spec.weightMax, weightsDirty, confirmDestructive]);

  const runClient = useCallback(() => {
    if (!data?.assets?.length) return;
    setClientBusy(true);
    setError(null);
    setQuantumMeta(null);
    try {
      const K = spec.K.trim() ? parseInt(spec.K, 10) : NaN;
      const KScreen = spec.kScreen.trim() ? parseInt(spec.kScreen, 10) : NaN;
      const KSelect = spec.kSelect.trim() ? parseInt(spec.kSelect, 10) : NaN;
      const r = runOptimisation(data, {
        objective: spec.objective,
        wMax: spec.weightMax,
        K: Number.isFinite(K) ? K : undefined,
        KScreen: Number.isFinite(KScreen) ? KScreen : undefined,
        KSelect: Number.isFinite(KSelect) ? KSelect : undefined,
      });
      const w = [...r.weights];
      setWeights(w);
      const ml = metricsLiteFromWeights(w, data);
      setLastClientMetrics(ml);
      setLastSource("client");
      setWeightsDirty(false);
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e));
    } finally {
      setClientBusy(false);
    }
  }, [data, spec]);

  const runApi = useCallback(async () => {
    if (!data?.assets?.length) return;
    setApiBusy(true);
    setError(null);
    setQuantumMeta(null);
    try {
      const returns = data.assets.map((a) => a.annReturn);
      const covariance = Array.from({ length: n }, (_, i) =>
        Array.from({ length: n }, (_, j) =>
          data.assets[i].annVol *
            data.assets[j].annVol *
            data.corr[i][j],
        ),
      );
      const payload: Record<string, unknown> = {
        returns,
        covariance,
        asset_names: data.assets.map((a) => a.name),
        sectors: data.assets.map((a) => a.sector),
        objective: spec.objective,
        weight_min: spec.weightMin,
        maxWeight: spec.weightMax,
        seed: spec.seed,
        n_layers: spec.nLayers,
        n_restarts: spec.nRestarts,
        lambda_risk: spec.lambdaRisk,
        gamma: spec.gamma,
      };
      if (spec.K.trim()) payload.K = parseInt(spec.K, 10);
      if (spec.kScreen.trim()) payload.K_screen = parseInt(spec.kScreen, 10);
      if (spec.kSelect.trim()) payload.K_select = parseInt(spec.kSelect, 10);

      const resp = (await optimizePortfolio(payload)) as Record<
        string,
        unknown
      >;
      const qsw = (resp.qsw_result || resp) as Record<string, unknown>;
      const raw =
        (qsw.weights as number[] | undefined) ||
        (resp.weights as number[] | undefined) ||
        [];
      if (raw.length) {
        const w = raw.map((x) => Number(x));
        setWeights(w);
        setLastApiMetrics(metricsLiteFromWeights(w, data));
      }
      const qm = resp.quantum_metadata as Record<string, unknown> | undefined;
      setQuantumMeta(qm && typeof qm === "object" ? qm : null);
      setLastSource("api");
      setWeightsDirty(false);
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e));
    } finally {
      setApiBusy(false);
    }
  }, [data, n, spec]);

  const updateWeight = useCallback((i: number, val: string) => {
    const x = parseFloat(val);
    setWeights((prev) => {
      const next = [...prev];
      next[i] = Number.isFinite(x) ? x : 0;
      return next;
    });
    setLastSource("manual");
    setWeightsDirty(true);
  }, []);

  const applyPreset = useCallback(
    (key: "tighter" | "hybrid" | "quantum") => {
      if (key === "tighter") {
        setSpec((s) => ({
          ...s,
          weightMax: Math.max(0.05, Math.round(s.weightMax * 0.9 * 1000) / 1000),
        }));
      } else if (key === "hybrid") {
        setSpec((s) => {
          const parsed =
            s.kScreen.trim() !== "" ? parseInt(s.kScreen, 10) : NaN;
          const base = Number.isFinite(parsed)
            ? parsed
            : Math.min(15, Math.max(5, n));
          return {
            ...s,
            objective: "hybrid",
            kScreen: String(base + 2),
          };
        });
      } else if (key === "quantum") {
        const hasVqe = objectiveOptions.some((o) => o.value === "vqe");
        if (hasVqe) setSpec((s) => ({ ...s, objective: "vqe" }));
      }
    },
    [n, objectiveOptions],
  );

  const saveSnapshot = useCallback(
    (slot: "A" | "B") => {
      if (!data) return;
      const snap: Snapshot = {
        spec: { ...spec },
        weights: [...weights],
        metrics: metrics
          ? {
              sharpe: metrics.sharpe,
              portReturn: metrics.portReturn,
              portVol: metrics.portVol,
              nActive: metrics.nActive,
            }
          : null,
        hypothesisLabel,
        ts: new Date().toISOString(),
      };
      if (slot === "A") setSnapshotA(snap);
      else setSnapshotB(snap);
    },
    [data, spec, weights, metrics, hypothesisLabel],
  );

  const abDiff = useMemo(() => {
    if (!snapshotA || !snapshotB || !data) return null;
    const specLines = specDiff(snapshotA.spec, snapshotB.spec);
    const dSharpe =
      (snapshotA.metrics?.sharpe ?? 0) - (snapshotB.metrics?.sharpe ?? 0);
    const dRet =
      (snapshotA.metrics?.portReturn ?? 0) - (snapshotB.metrics?.portReturn ?? 0);
    const dVol =
      (snapshotA.metrics?.portVol ?? 0) - (snapshotB.metrics?.portVol ?? 0);
    const wDelta = maxWeightDelta(snapshotA.weights, snapshotB.weights);
    return { specLines, dSharpe, dRet, dVol, wDelta };
  }, [snapshotA, snapshotB, data]);

  const exportPayload = useCallback(() => {
    return {
      hypothesisLabel,
      timestamp: new Date().toISOString(),
      spec,
      weights,
      metrics: metrics
        ? {
            sharpe: metrics.sharpe,
            annReturn: metrics.portReturn,
            annVol: metrics.portVol,
            nActive: metrics.nActive,
          }
        : null,
      lastQuickSimMetrics: lastClientMetrics,
      lastApiMetrics,
      snapshotA,
      snapshotB,
    };
  }, [
    hypothesisLabel,
    spec,
    weights,
    metrics,
    lastClientMetrics,
    lastApiMetrics,
    snapshotA,
    snapshotB,
  ]);

  const copyJson = useCallback(async () => {
    try {
      await navigator.clipboard.writeText(
        JSON.stringify(exportPayload(), null, 2),
      );
    } catch {
      setError("Could not copy to clipboard");
    }
  }, [exportPayload]);

  const downloadJson = useCallback(() => {
    const blob = new Blob([JSON.stringify(exportPayload(), null, 2)], {
      type: "application/json",
    });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `scientist-bench-${Date.now()}.json`;
    a.click();
    URL.revokeObjectURL(url);
  }, [exportPayload]);

  const downloadCsv = useCallback(() => {
    if (!data?.assets.length) return;
    const lines: string[] = [];
    lines.push("field,value");
    lines.push(`hypothesis,${csvEscape(hypothesisLabel)}`);
    lines.push(`timestamp,${new Date().toISOString()}`);
    if (metrics) {
      lines.push(`sharpe,${metrics.sharpe}`);
      lines.push(`ann_return,${metrics.portReturn}`);
      lines.push(`ann_vol,${metrics.portVol}`);
      lines.push(`n_active,${metrics.nActive}`);
    }
    lines.push("asset,weight");
    data.assets.forEach((a, i) => {
      lines.push(`${csvEscape(a.name)},${weights[i] ?? 0}`);
    });
    const blob = new Blob([lines.join("\n")], { type: "text/csv" });
    const url = URL.createObjectURL(blob);
    const el = document.createElement("a");
    el.href = url;
    el.download = `scientist-bench-${Date.now()}.csv`;
    el.click();
    URL.revokeObjectURL(url);
  }, [data, hypothesisLabel, metrics, weights]);

  const cardStyle: CSSProperties = {
    padding: 14,
    borderRadius: 10,
    border: `1px solid ${t.border}`,
    background: t.surface,
  };

  const ctx = labContext;

  if (!data?.assets?.length) {
    return (
      <div style={{ ...cardStyle, color: t.textDim, fontSize: 12 }}>
        Load a universe (sidebar) to use the scientist bench.
      </div>
    );
  }

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
      <details
        style={{
          ...cardStyle,
          padding: "10px 14px",
        }}
      >
        <summary
          style={{
            cursor: "pointer",
            fontSize: 12,
            fontWeight: 700,
            color: t.text,
            fontFamily: FONT.sans,
          }}
        >
          How to use this bench
        </summary>
        <ul
          style={{
            margin: "10px 0 0",
            paddingLeft: 18,
            fontSize: 10,
            color: t.textMuted,
            lineHeight: 1.55,
          }}
        >
          <li>Optional: name your hypothesis below.</li>
          <li>
            Use <strong style={{ color: t.text }}>Sync from sidebar</strong> to
            align the bench with the left panel; review the change chips.
          </li>
          <li>
            Run <strong style={{ color: t.text }}>Quick sim</strong> for fast
            browser math, then <strong style={{ color: t.text }}>Full optimizer</strong>{" "}
            for Python results on the same spec.
          </li>
          <li>Edit weights or use Equal / Clip; metrics update instantly.</li>
          <li>
            Save <strong style={{ color: t.text }}>A</strong> /{" "}
            <strong style={{ color: t.text }}>B</strong> snapshots to compare
            scenarios.
          </li>
          <li>
            Hardware / IBM lab runs with metadata live under{" "}
            <Link
              href="/reports"
              style={{ color: t.accent, fontWeight: 600 }}
            >
              Reports
            </Link>
            — not on this optimize path.
          </li>
        </ul>
      </details>

      <div style={{ ...cardStyle }}>
        <label style={labelStyle(t)}>Hypothesis label</label>
        <input
          type="text"
          placeholder="e.g. Tighter cap vs baseline"
          value={hypothesisLabel}
          onChange={(e) => setHypothesisLabel(e.target.value)}
          style={inputStyle(t)}
          aria-label="Hypothesis label"
        />
        {syncDiffChips.length > 0 && (
          <div style={{ marginTop: 8 }} aria-live="polite">
            <span style={{ fontSize: 9, color: t.textMuted, fontFamily: FONT.mono }}>
              After last sync:
            </span>
            <div
              style={{
                display: "flex",
                flexWrap: "wrap",
                gap: 6,
                marginTop: 6,
              }}
            >
              {syncDiffChips.map((c) => (
                <span
                  key={c}
                  style={{
                    fontSize: 9,
                    fontFamily: FONT.mono,
                    padding: "3px 8px",
                    borderRadius: 999,
                    background: t.accentDim,
                    color: t.accent,
                    border: `1px solid ${t.border}`,
                  }}
                >
                  {c}
                </span>
              ))}
            </div>
          </div>
        )}
      </div>

      {!checklistDismissed && ctx && (
        <div
          style={{
            ...cardStyle,
            background: t.surfaceLight,
            position: "relative",
          }}
        >
          <button
            type="button"
            aria-label="Dismiss checklist"
            onClick={() => setChecklistDismissed(true)}
            style={{
              position: "absolute",
              top: 8,
              right: 8,
              border: "none",
              background: "transparent",
              color: t.textDim,
              cursor: "pointer",
              fontSize: 14,
            }}
          >
            ×
          </button>
          <h4
            style={{
              margin: "0 0 8px",
              fontSize: 11,
              fontWeight: 700,
              color: t.text,
            }}
          >
            Before you trust API results
          </h4>
          <ul
            style={{
              margin: 0,
              paddingLeft: 18,
              fontSize: 10,
              color: t.textMuted,
              lineHeight: 1.5,
            }}
          >
            <li style={{ color: ctx.marketMode === "live" && !ctx.isLiveLoaded ? t.accentWarm : t.textMuted }}>
              {ctx.marketMode === "live" && !ctx.isLiveLoaded
                ? "Live mode: load market data in the sidebar so Σ matches your intent."
                : "Data: synthetic or live loaded ✓"}
            </li>
            <li style={{ color: spec.objective === "vqe" && ctx.nAssets > MAX_IBM_VQE_ASSETS ? t.accentWarm : t.textMuted }}>
              {spec.objective === "vqe" && ctx.nAssets > MAX_IBM_VQE_ASSETS
                ? `VQE on IBM is limited to ~${MAX_IBM_VQE_ASSETS} assets; universe is ${ctx.nAssets}.`
                : `Universe size ${ctx.nAssets} (OK for typical VQE API path).`}
            </li>
            <li style={{ color: !ctx.ibmConnected ? t.textDim : t.textMuted }}>
              {ctx.ibmConnected
                ? "IBM token stored — hardware paths use Quantum Engine / lab runs."
                : "IBM not connected — classical / sim paths only unless you add a token."}
            </li>
          </ul>
        </div>
      )}

      <div
        style={{
          display: "grid",
          gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 340px), 1fr))",
          gap: 16,
          alignItems: "start",
        }}
      >
        <section style={cardStyle} aria-label="Experiment spec">
          <h3
            style={{
              margin: "0 0 10px",
              fontSize: 13,
              fontWeight: 700,
              color: t.text,
              fontFamily: FONT.sans,
            }}
          >
            Spec
          </h3>
          <p
            style={{
              fontSize: 10,
              color: t.textMuted,
              marginBottom: 10,
              lineHeight: 1.45,
            }}
          >
            Presets apply small, documented tweaks. Sync pulls from the sidebar.
          </p>
          <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 12 }}>
            <button
              type="button"
              onClick={() => applyPreset("tighter")}
              style={chipBtn(t)}
              title="Reduce w_max by ~10%"
            >
              Tighter cap
            </button>
            <button
              type="button"
              onClick={() => applyPreset("hybrid")}
              style={chipBtn(t)}
              title="Objective hybrid, slightly larger K_screen"
            >
              More hybrid screening
            </button>
            <button
              type="button"
              onClick={() => applyPreset("quantum")}
              disabled={!objectiveOptions.some((o) => o.value === "vqe")}
              style={{
                ...chipBtn(t),
                opacity: objectiveOptions.some((o) => o.value === "vqe") ? 1 : 0.45,
              }}
              title="Set objective to VQE if available"
            >
              Quantum-style (VQE)
            </button>
          </div>
          <label style={labelStyle(t)}>Objective</label>
          <select
            value={spec.objective}
            onChange={(e) =>
              setSpec((s) => ({ ...s, objective: e.target.value }))
            }
            style={inputStyle(t)}
          >
            {objectiveOptions.map((o) => (
              <option key={o.value} value={o.value}>
                {o.label} ({o.group})
              </option>
            ))}
          </select>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
            <div>
              <label style={labelStyle(t)}>w_min</label>
              <input
                type="number"
                step={0.001}
                min={0}
                max={1}
                value={spec.weightMin}
                onChange={(e) =>
                  setSpec((s) => ({
                    ...s,
                    weightMin: parseFloat(e.target.value) || 0,
                  }))
                }
                style={inputStyle(t)}
              />
            </div>
            <div>
              <label style={labelStyle(t)}>w_max</label>
              <input
                type="number"
                step={0.01}
                min={0}
                max={1}
                value={spec.weightMax}
                onChange={(e) =>
                  setSpec((s) => ({
                    ...s,
                    weightMax: parseFloat(e.target.value) || 0.2,
                  }))
                }
                style={inputStyle(t)}
              />
            </div>
          </div>
          <label style={labelStyle(t)}>Seed</label>
          <input
            type="number"
            value={spec.seed}
            onChange={(e) =>
              setSpec((s) => ({
                ...s,
                seed: parseInt(e.target.value, 10) || 0,
              }))
            }
            style={inputStyle(t)}
          />
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 8 }}>
            <div>
              <label style={labelStyle(t)}>K (optional)</label>
              <input
                value={spec.K}
                onChange={(e) => setSpec((s) => ({ ...s, K: e.target.value }))}
                placeholder="—"
                style={inputStyle(t)}
              />
            </div>
            <div>
              <label style={labelStyle(t)}>K_screen</label>
              <input
                value={spec.kScreen}
                onChange={(e) =>
                  setSpec((s) => ({ ...s, kScreen: e.target.value }))
                }
                placeholder="—"
                style={inputStyle(t)}
              />
            </div>
            <div>
              <label style={labelStyle(t)}>K_select</label>
              <input
                value={spec.kSelect}
                onChange={(e) =>
                  setSpec((s) => ({ ...s, kSelect: e.target.value }))
                }
                placeholder="—"
                style={inputStyle(t)}
              />
            </div>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
            <div>
              <label style={labelStyle(t)}>n_layers (API)</label>
              <input
                type="number"
                min={1}
                value={spec.nLayers}
                onChange={(e) =>
                  setSpec((s) => ({
                    ...s,
                    nLayers: parseInt(e.target.value, 10) || 3,
                  }))
                }
                style={inputStyle(t)}
              />
            </div>
            <div>
              <label style={labelStyle(t)}>n_restarts (API)</label>
              <input
                type="number"
                min={1}
                value={spec.nRestarts}
                onChange={(e) =>
                  setSpec((s) => ({
                    ...s,
                    nRestarts: parseInt(e.target.value, 10) || 8,
                  }))
                }
                style={inputStyle(t)}
              />
            </div>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
            <div>
              <label style={labelStyle(t)}>λ_risk (API)</label>
              <input
                type="number"
                step={0.1}
                value={spec.lambdaRisk}
                onChange={(e) =>
                  setSpec((s) => ({
                    ...s,
                    lambdaRisk: parseFloat(e.target.value) || 1,
                  }))
                }
                style={inputStyle(t)}
              />
            </div>
            <div>
              <label style={labelStyle(t)}>γ (API)</label>
              <input
                type="number"
                step={0.5}
                value={spec.gamma}
                onChange={(e) =>
                  setSpec((s) => ({
                    ...s,
                    gamma: parseFloat(e.target.value) || 8,
                  }))
                }
                style={inputStyle(t)}
              />
            </div>
          </div>
          <button type="button" onClick={syncFromSidebar} style={btnSecondary(t)}>
            Sync from sidebar
          </button>
          <p style={{ fontSize: 9, color: t.textDim, marginTop: 8, lineHeight: 1.4 }}>
            After sync, run Quick sim or Full optimizer to align weights with the new spec.
          </p>
        </section>

        <section style={cardStyle} aria-label="Run optimization">
          <h3
            style={{
              margin: "0 0 10px",
              fontSize: 13,
              fontWeight: 700,
              color: t.text,
              fontFamily: FONT.sans,
            }}
          >
            Run
          </h3>
          <p style={{ fontSize: 10, color: t.textMuted, marginBottom: 8 }}>
            Same spec; different fidelity.{" "}
            <abbr
              title="Quick sim runs in-browser surrogates. Full optimizer calls the Python service (network latency, authoritative math)."
              style={{ cursor: "help", color: t.accent }}
            >
              ?
            </abbr>
          </p>
          <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
            <button
              type="button"
              onClick={runClient}
              disabled={clientBusy}
              style={btnPrimary(t, clientBusy)}
              title="Fast browser simulation — good for iteration"
            >
              {clientBusy ? "Running…" : "Quick sim (browser)"}
            </button>
            <button
              type="button"
              onClick={() => void runApi()}
              disabled={apiBusy}
              style={btnPrimary(t, apiBusy)}
              title="Python /api/portfolio/optimize — authoritative"
            >
              {apiBusy ? "Running…" : "Full optimizer (server)"}
            </button>
          </div>
          <p style={{ fontSize: 10, color: t.textDim, marginTop: 10 }}>
            Last weights from:{" "}
            <strong style={{ color: t.accent }}>
              {lastSource === "client"
                ? "Quick sim"
                : lastSource === "api"
                  ? "Full optimizer"
                  : "Manual / equal"}
            </strong>
            {weightsDirty ? (
              <span style={{ color: t.accentWarm }}> · weights edited</span>
            ) : null}
          </p>

          {lastClientMetrics && lastApiMetrics && (
            <div
              style={{
                marginTop: 12,
                padding: 10,
                borderRadius: 8,
                background: t.bg,
                border: `1px solid ${t.border}`,
              }}
            >
              <div
                style={{
                  fontSize: 10,
                  fontWeight: 700,
                  color: t.text,
                  marginBottom: 8,
                }}
              >
                Compare last two runs
              </div>
              <table style={{ width: "100%", fontSize: 10, fontFamily: FONT.mono }}>
                <thead>
                  <tr>
                    <th style={{ textAlign: "left", color: t.textMuted }} />
                    <th style={{ color: t.textMuted }}>Quick sim</th>
                    <th style={{ color: t.textMuted }}>Full opt.</th>
                    <th style={{ color: t.textMuted }}>Δ</th>
                  </tr>
                </thead>
                <tbody>
                  <tr>
                    <td style={{ color: t.textDim }}>Sharpe</td>
                    <td>{lastClientMetrics.sharpe.toFixed(4)}</td>
                    <td>{lastApiMetrics.sharpe.toFixed(4)}</td>
                    <td
                      style={{
                        color:
                          Math.abs(lastClientMetrics.sharpe - lastApiMetrics.sharpe) <
                          SHARPE_CLOSE_EPS
                            ? t.green
                            : t.text,
                      }}
                    >
                      {(lastClientMetrics.sharpe - lastApiMetrics.sharpe).toFixed(4)}
                    </td>
                  </tr>
                  <tr>
                    <td style={{ color: t.textDim }}>Return</td>
                    <td>{(lastClientMetrics.portReturn * 100).toFixed(2)}%</td>
                    <td>{(lastApiMetrics.portReturn * 100).toFixed(2)}%</td>
                    <td>
                      {(
                        (lastClientMetrics.portReturn - lastApiMetrics.portReturn) *
                        100
                      ).toFixed(2)}
                      pp
                    </td>
                  </tr>
                  <tr>
                    <td style={{ color: t.textDim }}>Vol</td>
                    <td>{(lastClientMetrics.portVol * 100).toFixed(2)}%</td>
                    <td>{(lastApiMetrics.portVol * 100).toFixed(2)}%</td>
                    <td>
                      {(
                        (lastClientMetrics.portVol - lastApiMetrics.portVol) *
                        100
                      ).toFixed(2)}
                      pp
                    </td>
                  </tr>
                </tbody>
              </table>
              {sharpeCompareNote && (
                <p style={{ fontSize: 9, color: t.textMuted, marginTop: 8, lineHeight: 1.4 }}>
                  {sharpeCompareNote}
                </p>
              )}
            </div>
          )}

          {error && (
            <p
              style={{
                fontSize: 10,
                color: t.red,
                marginTop: 8,
                fontFamily: FONT.mono,
              }}
            >
              {error}
            </p>
          )}
        </section>

        <section style={cardStyle} aria-label="Weights">
          <h3
            style={{
              margin: "0 0 10px",
              fontSize: 13,
              fontWeight: 700,
              color: t.text,
              fontFamily: FONT.sans,
            }}
          >
            Weights
          </h3>
          <div style={{ display: "flex", gap: 8, marginBottom: 10, flexWrap: "wrap" }}>
            <button type="button" onClick={setEqualWeights} style={btnSecondary(t)}>
              Equal weight
            </button>
            <button type="button" onClick={applyBounds} style={btnSecondary(t)}>
              Clip + normalize
            </button>
          </div>
          <div style={{ maxHeight: 280, overflowY: "auto" }}>
            <table
              style={{
                width: "100%",
                borderCollapse: "collapse",
                fontSize: 11,
                fontFamily: FONT.mono,
              }}
            >
              <thead>
                <tr>
                  <th style={thStyle(t)}>Asset</th>
                  <th style={thStyle(t)}>w</th>
                </tr>
              </thead>
              <tbody>
                {data.assets.map((a, i) => (
                  <tr key={a.name}>
                    <td style={tdStyle(t)}>{a.name}</td>
                    <td style={tdStyle(t)}>
                      <input
                        type="number"
                        step={0.001}
                        min={0}
                        max={1}
                        value={weights[i] ?? 0}
                        onChange={(e) => updateWeight(i, e.target.value)}
                        style={{
                          width: "100%",
                          minWidth: 72,
                          padding: "4px 6px",
                          borderRadius: 4,
                          border: `1px solid ${t.border}`,
                          background: t.bg,
                          color: t.text,
                          fontFamily: FONT.mono,
                          fontSize: 11,
                        }}
                      />
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </section>

        <section style={cardStyle} aria-label="Metrics">
          <h3
            style={{
              margin: "0 0 10px",
              fontSize: 13,
              fontWeight: 700,
              color: t.text,
              fontFamily: FONT.sans,
            }}
          >
            Metrics (from current w)
          </h3>
          {metrics ? (
            <dl
              style={{
                display: "grid",
                gridTemplateColumns: "auto 1fr",
                gap: "6px 12px",
                fontSize: 11,
                fontFamily: FONT.mono,
                margin: 0,
              }}
            >
              <dt style={{ color: t.textMuted }}>Ann. return</dt>
              <dd style={{ margin: 0 }}>
                {(metrics.portReturn * 100).toFixed(2)}%
              </dd>
              <dt style={{ color: t.textMuted }}>Vol</dt>
              <dd style={{ margin: 0 }}>{(metrics.portVol * 100).toFixed(2)}%</dd>
              <dt style={{ color: t.textMuted }}>Sharpe</dt>
              <dd style={{ margin: 0 }}>{metrics.sharpe.toFixed(4)}</dd>
              <dt style={{ color: t.textMuted }}>n_active</dt>
              <dd style={{ margin: 0 }}>{metrics.nActive}</dd>
            </dl>
          ) : (
            <p style={{ color: t.textDim, fontSize: 11 }}>—</p>
          )}

          <div style={{ marginTop: 12, display: "flex", flexWrap: "wrap", gap: 8 }}>
            <button
              type="button"
              onClick={() => saveSnapshot("A")}
              style={btnSecondary(t)}
            >
              Save snapshot A
            </button>
            <button
              type="button"
              onClick={() => saveSnapshot("B")}
              style={btnSecondary(t)}
            >
              Save snapshot B
            </button>
          </div>
          {abDiff && (
            <div style={{ marginTop: 10, fontSize: 9, fontFamily: FONT.mono, color: t.textMuted }}>
              <div style={{ fontWeight: 700, color: t.text, marginBottom: 4 }}>
                A vs B
              </div>
              <div>Δ Sharpe: {abDiff.dSharpe.toFixed(4)}</div>
              <div>Δ return: {(abDiff.dRet * 100).toFixed(2)} pp</div>
              <div>Δ vol: {(abDiff.dVol * 100).toFixed(2)} pp</div>
              <div>Max |Δw|: {abDiff.wDelta.toFixed(4)}</div>
              <div style={{ marginTop: 6 }}>
                {abDiff.specLines.map((l) => (
                  <div key={l}>{l}</div>
                ))}
              </div>
            </div>
          )}
        </section>

        <section style={cardStyle} aria-label="Export">
          <h3
            style={{
              margin: "0 0 10px",
              fontSize: 13,
              fontWeight: 700,
              color: t.text,
              fontFamily: FONT.sans,
            }}
          >
            Export
          </h3>
          <p style={{ fontSize: 10, color: t.textMuted, marginBottom: 8 }}>
            Copy or download spec, weights, metrics, snapshots for tickets or
            papers.
          </p>
          <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
            <button type="button" onClick={() => void copyJson()} style={btnSecondary(t)}>
              Copy JSON
            </button>
            <button type="button" onClick={downloadJson} style={btnSecondary(t)}>
              Download JSON
            </button>
            <button type="button" onClick={downloadCsv} style={btnSecondary(t)}>
              Download CSV
            </button>
          </div>
        </section>

        <section style={cardStyle} aria-label="Quantum">
          <h3
            style={{
              margin: "0 0 10px",
              fontSize: 13,
              fontWeight: 700,
              color: t.text,
              fontFamily: FONT.sans,
            }}
          >
            Quantum / metadata
          </h3>
          <p style={{ fontSize: 10, color: t.textMuted, lineHeight: 1.45 }}>
            Standard optimize responses usually have no quantum block. Lab runs
            with IBM may expose metadata elsewhere (Reports).
          </p>
          {quantumMeta && Object.keys(quantumMeta).length > 0 ? (
            <pre
              style={{
                fontSize: 9,
                fontFamily: FONT.mono,
                color: t.text,
                whiteSpace: "pre-wrap",
                wordBreak: "break-all",
                margin: "10px 0 0",
                padding: 8,
                borderRadius: 6,
                background: t.bg,
                border: `1px solid ${t.border}`,
              }}
            >
              {JSON.stringify(quantumMeta, null, 2)}
            </pre>
          ) : (
            <p style={{ fontSize: 10, color: t.textDim, marginTop: 8 }}>
              Classical / no quantum_metadata on last API response.
            </p>
          )}
        </section>

        <section style={cardStyle} aria-label="Circuit placeholder">
          <h3
            style={{
              margin: "0 0 10px",
              fontSize: 13,
              fontWeight: 700,
              color: t.text,
              fontFamily: FONT.sans,
            }}
          >
            Circuit
          </h3>
          <p style={{ fontSize: 10, color: t.textDim, lineHeight: 1.5 }}>
            Circuit visualization requires backend circuit JSON (not yet exposed
            on optimize).
          </p>
        </section>
      </div>
    </div>
  );
}

function csvEscape(s: string): string {
  if (/[",\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
  return s;
}

function chipBtn(t: Theme): CSSProperties {
  return {
    padding: "5px 10px",
    borderRadius: 6,
    border: `1px solid ${t.border}`,
    background: t.surfaceLight,
    color: t.text,
    fontSize: 10,
    fontFamily: FONT.mono,
    cursor: "pointer",
  };
}

function labelStyle(t: Theme): CSSProperties {
  return {
    display: "block",
    fontSize: 9,
    color: t.textMuted,
    marginBottom: 4,
    textTransform: "uppercase" as const,
    letterSpacing: "0.06em",
    fontFamily: FONT.mono,
  };
}

function inputStyle(t: Theme): CSSProperties {
  return {
    width: "100%",
    boxSizing: "border-box",
    padding: "6px 8px",
    marginBottom: 10,
    borderRadius: 4,
    border: `1px solid ${t.border}`,
    background: t.bg,
    color: t.text,
    fontSize: 11,
    fontFamily: FONT.mono,
  };
}

function btnPrimary(t: Theme, disabled: boolean): CSSProperties {
  return {
    padding: "8px 14px",
    borderRadius: 4,
    border: "none",
    background: disabled ? t.surfaceLight : t.accent,
    color: disabled ? t.textDim : t.bg,
    fontSize: 11,
    fontWeight: 600,
    cursor: disabled ? "default" : "pointer",
    fontFamily: FONT.mono,
  };
}

function btnSecondary(t: Theme): CSSProperties {
  return {
    padding: "6px 10px",
    marginTop: 8,
    borderRadius: 4,
    border: `1px solid ${t.border}`,
    background: "transparent",
    color: t.accent,
    fontSize: 10,
    fontWeight: 600,
    cursor: "pointer",
    fontFamily: FONT.mono,
  };
}

function thStyle(t: Theme): CSSProperties {
  return {
    textAlign: "left" as const,
    padding: "6px 8px",
    color: t.textMuted,
    borderBottom: `1px solid ${t.border}`,
  };
}

function tdStyle(t: Theme): CSSProperties {
  return {
    padding: "4px 8px",
    color: t.text,
    borderBottom: `1px solid ${t.border}`,
  };
}
