import { useState, useRef, useCallback, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { ESPLoader, Transport } from "esptool-js";
import api from "../api/client";

// ─── constants ───────────────────────────────────────────────────────────────

// Row layout: Vesper Pro / Vesper Plus / Vesper | Agnus / Agnus Mini | Chronos Pro / Chronos
const BOARD_TYPES = [
  { value: "vesper_pro",  name: "VESPER PRO",   codename: "vesper-pro",    desc: "Full-featured pro controller",  family: "vesper"  },
  { value: "vesper_plus", name: "VESPER PLUS",  codename: "vesper-plus",   desc: "Extended output controller",    family: "vesper"  },
  { value: "vesper",      name: "VESPER",       codename: "vesper-basic",  desc: "Standard bell controller",      family: "vesper"  },
  { value: "agnus",       name: "AGNUS",        codename: "agnus-basic",   desc: "Standard carillon module",      family: "agnus"   },
  { value: "agnus_mini",  name: "AGNUS MINI",   codename: "agnus-mini",    desc: "Compact carillon module",       family: "agnus"   },
  { value: "chronos_pro", name: "CHRONOS PRO",  codename: "chronos-pro",   desc: "Pro clock controller",          family: "chronos" },
  { value: "chronos",     name: "CHRONOS",      codename: "chronos-basic", desc: "Basic clock controller",        family: "chronos" },
];

// Color palette per board family (idle → selected → hover glow)
const BOARD_FAMILY_COLORS = {
  vesper:  {
    selectedBg:   "#0a1929",
    selectedBorder:"#3b82f6",
    selectedText: "#60a5fa",
    hoverBorder:  "#3b82f6",
    glowColor:    "rgba(59,130,246,0.35)",
    idleBorder:   "#1d3a5c",
    idleText:     "#7ca8d4",
  },
  agnus:   {
    selectedBg:   "#1a1400",
    selectedBorder:"#f59e0b",
    selectedText: "#fbbf24",
    hoverBorder:  "#f59e0b",
    glowColor:    "rgba(245,158,11,0.35)",
    idleBorder:   "#4a3800",
    idleText:     "#c79d3a",
  },
  chronos: {
    selectedBg:   "#1a0808",
    selectedBorder:"#ef4444",
    selectedText: "#f87171",
    hoverBorder:  "#ef4444",
    glowColor:    "rgba(239,68,68,0.35)",
    idleBorder:   "#5c1a1a",
    idleText:     "#d47a7a",
  },
};

const BOARD_TYPE_LABELS = Object.fromEntries(BOARD_TYPES.map((b) => [b.value, b.name]));

// Display board version stored as semver string ("1.0", "2.1") as "Rev 1.0"
function formatHwVersion(v) {
  if (!v) return "—";
  // Already in semver form ("1.0") or legacy 2-digit ("01" → "Rev 1.0")
  if (/^\d+\.\d+/.test(v)) return `Rev ${v}`;
  // Legacy: "01" → "1.0", "02" → "2.0"
  const n = parseInt(v, 10);
  if (!isNaN(n)) return `Rev ${n}.0`;
  return `Rev ${v}`;
}

const STATUS_STYLES = {
  manufactured:   { bg: "var(--bg-card-hover)", color: "var(--text-muted)" },
  flashed:        { bg: "var(--badge-blue-bg)", color: "var(--badge-blue-text)" },
  provisioned:    { bg: "#0a2e2a", color: "#4dd6c8" },
  sold:           { bg: "#1e1036", color: "#c084fc" },
  claimed:        { bg: "#2e1a00", color: "#fb923c" },
  decommissioned: { bg: "var(--danger-bg)", color: "var(--danger-text)" },
};

// Statuses that can be re-flashed via the wizard
const FLASHABLE_STATUSES = ["manufactured", "flashed", "provisioned"];

const FLASH_BAUD       = 460800;
const NVS_ADDRESS      = 0x9000;
const FW_ADDRESS       = 0x10000;
const VERIFY_POLL_MS   = 5000;
const VERIFY_TIMEOUT_MS = 120_000;

// ─── small helpers ────────────────────────────────────────────────────────────

function StatusBadge({ status }) {
  const style = STATUS_STYLES[status] || STATUS_STYLES.manufactured;
  return (
    <span
      className="px-2.5 py-1 text-sm rounded-full capitalize font-medium"
      style={{ backgroundColor: style.bg, color: style.color }}
    >
      {status}
    </span>
  );
}

function ProgressBar({ label, percent, flex = false }) {
  return (
    <div className="space-y-1.5" style={{ flex: flex ? 1 : undefined }}>
      <div className="flex justify-between text-xs" style={{ color: "var(--text-secondary)" }}>
        <span>{label}</span>
        <span>{Math.round(percent)}%</span>
      </div>
      <div className="h-2 rounded-full overflow-hidden" style={{ backgroundColor: "var(--bg-card-hover)" }}>
        <div
          className="h-full rounded-full transition-all duration-200"
          style={{ width: `${percent}%`, backgroundColor: "var(--accent)" }}
        />
      </div>
    </div>
  );
}

function ErrorBox({ msg }) {
  if (!msg) return null;
  return (
    <div
      className="text-sm rounded-md p-3 border"
      style={{ backgroundColor: "var(--danger-bg)", borderColor: "var(--danger)", color: "var(--danger-text)" }}
    >
      {msg}
    </div>
  );
}

function inputStyle() {
  return { backgroundColor: "var(--bg-input)", borderColor: "var(--border-input)", color: "var(--text-primary)" };
}

// ─── Step Indicator ───────────────────────────────────────────────────────────
// Steps: 0=Mode, 1=Select, 2=Flash, 3=Verify, 4=Done
// current is 1-based to match step state (step 0 = mode picker, shown differently)

const STEP_LABELS = ["Begin", "Device", "Flash", "Verify", "Done"];

function CheckeredFlagIcon() {
  return (
    <svg className="w-3 h-3" viewBox="0 0 24 24" fill="currentColor">
      <path d="M4 2v20h2V13h14V3H4zm2 2h3v3H6V4zm0 5h3v3H6V9zm5-5h3v3h-3V4zm0 5h3v3h-3V9zm5-5h3v3h-3V4zm0 5h3v3h-3V9z" />
    </svg>
  );
}

function StepIndicator({ current }) {
  // current: 1..4 (step numbers matching STEP_LABELS index + 1)
  return (
    <div className="flex items-center" style={{ minWidth: 0 }}>
      {STEP_LABELS.map((label, i) => {
        const idx     = i + 1;
        const done    = idx < current;
        const active  = idx === current;
        const pending = idx > current;
        const isLast  = i === STEP_LABELS.length - 1;

        // Color tokens
        const dotBg    = done    ? "#53b15786"
                       : active  ? "#22c55e"
                       : "#251a1a";
        const dotColor = done    ? "#cbedb9"
                       : active  ? "#ffffff"
                       : "#555";
        const labelColor = active ? "#22c55e" : done ? "#53b15786" : "#555";
        const labelGlow  = active ? "0 0 8px rgba(34,197,94,0.45)" : "none";
        const lineColor  = done ? "#22c55e" : "var(--border-primary)";

        // Active dot is 38px, others are 30px
        const dotSize = active ? 38 : 30;

        return (
          <div key={label} className="flex items-center" style={{ minWidth: 0 }}>
            {/* Node */}
            <div className="flex flex-col items-center" style={{ flexShrink: 0 }}>
              <div
                style={{
                  width: dotSize, height: dotSize,
                  borderRadius: "60%",
                  display: "flex", alignItems: "center", justifyContent: "center",
                  backgroundColor: dotBg,
                  color: dotColor,
                  fontSize: active ? "1.0rem" : "0.85rem",
                  fontWeight: 500,
                  border: active ? "2px solid #22c55e" : done ? "2px solid #6d9b78c0" : "2px solid #333",
                  boxShadow: active ? "0 0 18px 8px rgba(34,197,94,0.4)" : "none",
                  transition: "all 0.2s",
                }}
              >
                {done ? (
                  <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
                  </svg>
                ) : isLast ? (
                  <CheckeredFlagIcon />
                ) : idx}
              </div>
              <span
                className="text-xs mt-1 whitespace-nowrap font-medium"
                style={{
                  color: labelColor,
                  textShadow: labelGlow,
                  opacity: pending ? 0.5 : 1,
                  maxWidth: 72,
                  textAlign: "center",
                  lineHeight: 1.2,
                }}
              >
                {label}
              </span>
            </div>

            {/* Connector line */}
            {i < STEP_LABELS.length - 1 && (
              <div
                style={{
                  height: 2,
                  flex: "1 1 16px",
                  minWidth: 28,
                  maxWidth: 40,
                  backgroundColor: lineColor,
                  marginBottom: 18,
                  marginLeft: 4,
                  marginRight: 4,
                  transition: "background-color 0.2s",
                  flexShrink: 1,
                }}
              />
            )}
          </div>
        );
      })}
    </div>
  );
}

// ─── Step 0 — Mode picker ─────────────────────────────────────────────────────

function StepModePicker({ onPick }) {
  return (
    <div>
      <h2 className="text-base font-semibold mb-2" style={{ color: "var(--text-heading)" }}>
        What would you like to do?
      </h2>
      <p className="text-sm mb-6" style={{ color: "var(--text-muted)" }}>
        Choose how to start the provisioning process.
      </p>
      <div className="grid grid-cols-2 gap-4">
        {/* Flash Existing */}
        <button
          onClick={() => onPick("existing")}
          className="rounded-xl border p-6 text-left cursor-pointer"
          style={{
            backgroundColor: "var(--bg-card)",
            borderColor: "var(--border-primary)",
            transition: "border-color 0.15s, box-shadow 0.15s",
          }}
          onMouseEnter={(e) => {
            e.currentTarget.style.borderColor = "var(--accent)";
            e.currentTarget.style.boxShadow = "0 0 14px 4px rgba(34,197,94,0.25)";
          }}
          onMouseLeave={(e) => {
            e.currentTarget.style.borderColor = "var(--border-primary)";
            e.currentTarget.style.boxShadow = "none";
          }}
        >
          <div
            className="w-10 h-10 rounded-lg flex items-center justify-center mb-4"
            style={{ backgroundColor: "var(--bg-card-hover)" }}
          >
            <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" style={{ color: "var(--accent)" }}>
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
            </svg>
          </div>
          <p className="font-semibold text-sm mb-1" style={{ color: "var(--text-heading)" }}>Flash Existing</p>
          <p className="text-xs leading-relaxed" style={{ color: "var(--text-muted)" }}>
            Re-flash a device already in inventory — manufactured, flashed, or provisioned.
          </p>
        </button>

        {/* Deploy New — same style as Flash Existing */}
        <button
          onClick={() => onPick("new")}
          className="rounded-xl border p-6 text-left cursor-pointer"
          style={{
            backgroundColor: "var(--bg-card)",
            borderColor: "var(--border-primary)",
            transition: "border-color 0.15s, box-shadow 0.15s",
          }}
          onMouseEnter={(e) => {
            e.currentTarget.style.borderColor = "var(--accent)";
            e.currentTarget.style.boxShadow = "0 0 14px 4px rgba(34,197,94,0.25)";
          }}
          onMouseLeave={(e) => {
            e.currentTarget.style.borderColor = "var(--border-primary)";
            e.currentTarget.style.boxShadow = "none";
          }}
        >
          <div
            className="w-10 h-10 rounded-lg flex items-center justify-center mb-4"
            style={{ backgroundColor: "var(--bg-card-hover)" }}
          >
            <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" style={{ color: "var(--accent)" }}>
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
            </svg>
          </div>
          <p className="font-semibold text-sm mb-1" style={{ color: "var(--text-heading)" }}>Deploy New</p>
          <p className="text-xs leading-relaxed" style={{ color: "var(--text-muted)" }}>
            Generate a new serial number, select board type and revision, then flash and provision.
          </p>
        </button>
      </div>
    </div>
  );
}

// ─── Step 1a — Flash Existing: pick from inventory ────────────────────────────

function StepSelectExisting({ onSelected }) {
  const [search, setSearch]       = useState("");
  const [results, setResults]     = useState([]);
  const [loading, setLoading]     = useState(false);
  const [error, setError]         = useState("");
  const [selected, setSelected]   = useState(null);

  const doSearch = async (q) => {
    setError("");
    setLoading(true);
    try {
      const params = new URLSearchParams({ limit: "50" });
      if (q) params.set("search", q);
      // Only show flashable statuses
      const all = await Promise.all(
        FLASHABLE_STATUSES.map((s) =>
          api.get(`/manufacturing/devices?${params}&status=${s}`).then((d) => d.devices)
        )
      );
      const merged = all.flat().sort((a, b) =>
        (b.created_at || "").localeCompare(a.created_at || "")
      );
      setResults(merged);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => { doSearch(""); }, []);

  const handleSearchSubmit = (e) => {
    e.preventDefault();
    doSearch(search);
  };

  if (selected) {
    return (
      <div
        className="rounded-lg border p-5"
        style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
      >
        <h3 className="text-sm font-semibold uppercase tracking-wide mb-4" style={{ color: "var(--text-muted)" }}>
          Device Selected
        </h3>
        <div className="grid grid-cols-2 gap-4 mb-4">
          <InfoCell label="Serial Number" value={selected.serial_number} mono />
          <InfoCell label="Board Type" value={BOARD_TYPE_LABELS[selected.hw_type] || selected.hw_type} />
          <InfoCell label="HW Version" value={formatHwVersion(selected.hw_version)} />
          <div>
            <p className="text-xs uppercase tracking-wide mb-0.5" style={{ color: "var(--text-muted)" }}>Status</p>
            <StatusBadge status={selected.mfg_status} />
          </div>
        </div>
        <div className="flex gap-3">
          <button
            onClick={() => onSelected(selected)}
            className="px-5 py-2 text-sm rounded-md font-medium hover:opacity-90 transition-opacity cursor-pointer"
            style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
          >
            Continue →
          </button>
          <button
            onClick={() => setSelected(null)}
            className="px-4 py-2 text-sm rounded-md hover:opacity-80 transition-opacity cursor-pointer"
            style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-secondary)" }}
          >
            Change Device
          </button>
        </div>
      </div>
    );
  }

  return (
    <div className="space-y-4">
      <div
        className="rounded-lg border p-5"
        style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
      >
        <h3 className="text-sm font-semibold mb-3" style={{ color: "var(--text-heading)" }}>
          Select a Device to Flash
        </h3>
        <form onSubmit={handleSearchSubmit} className="flex gap-2 mb-4">
          <input
            type="text"
            placeholder="Search serial, batch, type…"
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            className="flex-1 px-3 py-2 rounded-md text-sm border"
            style={inputStyle()}
          />
          <button
            type="submit"
            className="px-4 py-2 text-sm rounded-md font-medium cursor-pointer hover:opacity-90"
            style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
          >
            {loading ? "…" : "Search"}
          </button>
        </form>
        <ErrorBox msg={error} />

        {results.length === 0 && !loading && (
          <p className="text-sm text-center py-4" style={{ color: "var(--text-muted)" }}>
            No flashable devices found.
          </p>
        )}

        {results.length > 0 && (
          <div className="rounded-md border overflow-hidden" style={{ borderColor: "var(--border-primary)" }}>
            <table className="w-full text-sm">
              <thead>
                <tr style={{ backgroundColor: "var(--bg-secondary)", borderBottom: "1px solid var(--border-primary)" }}>
                  <th className="px-3 py-2 text-left text-xs font-medium" style={{ color: "var(--text-muted)" }}>Serial</th>
                  <th className="px-3 py-2 text-left text-xs font-medium" style={{ color: "var(--text-muted)" }}>Type</th>
                  <th className="px-3 py-2 text-left text-xs font-medium" style={{ color: "var(--text-muted)" }}>Ver</th>
                  <th className="px-3 py-2 text-left text-xs font-medium" style={{ color: "var(--text-muted)" }}>Status</th>
                </tr>
              </thead>
              <tbody>
                {results.map((d) => (
                  <tr
                    key={d.id}
                    onClick={() => setSelected(d)}
                    className="cursor-pointer transition-colors"
                    style={{ borderBottom: "1px solid var(--border-secondary)" }}
                    onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = "var(--bg-card-hover)")}
                    onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = "")}
                  >
                    <td className="px-3 py-2 font-mono text-xs" style={{ color: "var(--text-primary)" }}>{d.serial_number}</td>
                    <td className="px-3 py-2 text-xs" style={{ color: "var(--text-secondary)" }}>
                      {BOARD_TYPE_LABELS[d.hw_type] || d.hw_type}
                    </td>
                    <td className="px-3 py-2 text-xs" style={{ color: "var(--text-muted)" }}>
                      {formatHwVersion(d.hw_version)}
                    </td>
                    <td className="px-3 py-2">
                      <StatusBadge status={d.mfg_status} />
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </div>
    </div>
  );
}

// ─── Board Type Tile (shared between StepDeployNew and AddDeviceModal) ────────

function BoardTypeTile({ bt, isSelected, pal, onClick }) {
  const [hovered, setHovered] = useState(false);

  const borderColor = isSelected
    ? pal.selectedBorder
    : hovered
    ? pal.hoverBorder
    : pal.idleBorder;

  const boxShadow = isSelected
    ? `0 0 0 1px ${pal.selectedBorder}, 0 0 14px 4px ${pal.glowColor}`
    : hovered
    ? `0 0 12px 3px ${pal.glowColor}`
    : "none";

  return (
    <button
      onClick={onClick}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      className="rounded-lg border p-3 text-left transition-all cursor-pointer"
      style={{
        backgroundColor: isSelected ? pal.selectedBg : "var(--bg-card)",
        borderColor,
        boxShadow,
        transition: "border-color 0.15s, box-shadow 0.15s, background-color 0.15s",
      }}
    >
      <p
        className="font-bold text-xs tracking-wide"
        style={{ color: isSelected ? pal.selectedText : hovered ? pal.idleText : "var(--text-heading)" }}
      >
        {bt.name}
      </p>
      <p className="text-xs mt-0.5 font-mono opacity-60" style={{ color: "var(--text-muted)" }}>
        {bt.codename}
      </p>
      <p className="text-xs mt-1 leading-snug" style={{ color: "var(--text-muted)", opacity: 0.75 }}>
        {bt.desc}
      </p>
    </button>
  );
}

// ─── Bespoke Picker Modal ─────────────────────────────────────────────────────


function BespokePickerModal({ onConfirm, onClose }) {
  const [firmwares, setFirmwares]     = useState([]);
  const [loading, setLoading]         = useState(true);
  const [error, setError]             = useState("");
  const [selected, setSelected]       = useState(null);
  const [hwFamily, setHwFamily]       = useState("");

  useEffect(() => {
    api.get("/firmware?hw_type=bespoke")
      .then((data) => setFirmwares(data.firmware || []))
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, []);

  const handleConfirm = () => {
    if (!selected) return;
    onConfirm({ firmware: selected, hwFamily });
  };

  return (
    <div className="fixed inset-0 flex items-center justify-center z-50" style={{ backgroundColor: "rgba(0,0,0,0.6)" }}>
      <div
        className="rounded-lg border flex flex-col"
        style={{
          backgroundColor: "var(--bg-card)",
          borderColor: "var(--border-primary)",
          width: "100%", maxWidth: 560,
          maxHeight: "80vh",
          margin: "1rem",
        }}
      >
        {/* Header */}
        <div className="px-5 py-4 border-b flex items-center justify-between" style={{ borderColor: "var(--border-primary)" }}>
          <div>
            <h2 className="text-base font-semibold" style={{ color: "var(--text-heading)" }}>Select Bespoke Firmware</h2>
            <p className="text-xs mt-0.5" style={{ color: "var(--text-muted)" }}>
              Choose a bespoke firmware and the hardware family to register in NVS.
            </p>
          </div>
          <button onClick={onClose} className="cursor-pointer hover:opacity-70 transition-opacity" style={{ color: "var(--text-muted)" }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
            </svg>
          </button>
        </div>

        {/* Body */}
        <div className="flex-1 overflow-y-auto p-5 space-y-4" style={{ minHeight: 0 }}>
          {loading && (
            <p className="text-sm text-center py-6" style={{ color: "var(--text-muted)" }}>Loading bespoke firmwares…</p>
          )}
          {error && (
            <div className="text-sm rounded-md p-3 border" style={{ backgroundColor: "var(--danger-bg)", borderColor: "var(--danger)", color: "var(--danger-text)" }}>
              {error}
            </div>
          )}
          {!loading && !error && firmwares.length === 0 && (
            <p className="text-sm text-center py-6" style={{ color: "var(--text-muted)" }}>
              No bespoke firmwares uploaded yet. Upload one from the Firmware Manager.
            </p>
          )}
          {firmwares.length > 0 && (
            <div className="rounded-md border overflow-hidden" style={{ borderColor: "var(--border-primary)" }}>
              <table className="w-full text-sm">
                <thead>
                  <tr style={{ backgroundColor: "var(--bg-secondary)", borderBottom: "1px solid var(--border-primary)" }}>
                    <th className="px-3 py-2 text-left text-xs font-medium" style={{ color: "var(--text-muted)" }}>UID</th>
                    <th className="px-3 py-2 text-left text-xs font-medium" style={{ color: "var(--text-muted)" }}>Version</th>
                    <th className="px-3 py-2 text-left text-xs font-medium" style={{ color: "var(--text-muted)" }}>Channel</th>
                    <th className="px-3 py-2 text-left text-xs font-medium" style={{ color: "var(--text-muted)" }}>Size</th>
                  </tr>
                </thead>
                <tbody>
                  {firmwares.map((fw) => {
                    const isSelected = selected?.id === fw.id;
                    return (
                      <tr
                        key={fw.id}
                        onClick={() => setSelected(fw)}
                        className="cursor-pointer transition-colors"
                        style={{
                          borderBottom: "1px solid var(--border-secondary)",
                          backgroundColor: isSelected ? "var(--badge-blue-bg)" : "",
                          outline: isSelected ? "1px solid var(--badge-blue-text)" : "none",
                        }}
                        onMouseEnter={(e) => { if (!isSelected) e.currentTarget.style.backgroundColor = "var(--bg-card-hover)"; }}
                        onMouseLeave={(e) => { if (!isSelected) e.currentTarget.style.backgroundColor = ""; }}
                      >
                        <td className="px-3 py-2.5 font-mono text-xs font-medium" style={{ color: "var(--text-primary)" }}>
                          {fw.bespoke_uid || "—"}
                        </td>
                        <td className="px-3 py-2.5 font-mono text-xs" style={{ color: "var(--text-secondary)" }}>{fw.version}</td>
                        <td className="px-3 py-2.5 text-xs" style={{ color: "var(--text-muted)" }}>{fw.channel}</td>
                        <td className="px-3 py-2.5 text-xs" style={{ color: "var(--text-muted)" }}>
                          {fw.size_bytes ? `${(fw.size_bytes / 1024).toFixed(1)} KB` : "—"}
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          )}

          {/* HW Family — free text */}
          <div>
            <label className="block text-sm font-medium mb-1.5" style={{ color: "var(--text-secondary)" }}>
              Hardware Family for NVS
            </label>
            <p className="text-xs mb-2" style={{ color: "var(--text-muted)" }}>
              This value will be written to NVS as <span style={{ fontFamily: "monospace" }}>hw_type</span>. The device will identify as this family.
            </p>
            <input
              type="text"
              value={hwFamily}
              onChange={(e) => setHwFamily(e.target.value)}
              placeholder="e.g. vesper_plus"
              className="w-full px-3 py-2 rounded-md text-sm border"
              style={{ backgroundColor: "var(--bg-input)", borderColor: "var(--border-input)", color: "var(--text-primary)" }}
            />
          </div>
        </div>

        {/* Footer */}
        <div className="px-5 py-4 border-t flex items-center justify-between gap-3" style={{ borderColor: "var(--border-primary)" }}>
          <p className="text-xs" style={{ color: "var(--text-muted)" }}>
            hw_revision will be set to <span style={{ fontFamily: "monospace" }}>1.0</span> for bespoke devices.
          </p>
          <div className="flex gap-2">
            <button
              onClick={onClose}
              className="px-4 py-2 text-sm rounded-md hover:opacity-80 transition-opacity cursor-pointer"
              style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-secondary)" }}
            >
              Cancel
            </button>
            <button
              onClick={handleConfirm}
              disabled={!selected || !hwFamily.trim()}
              className="px-5 py-2 text-sm rounded-md font-medium hover:opacity-90 transition-opacity cursor-pointer disabled:opacity-40"
              style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
            >
              Continue →
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

// ─── Step 1b — Deploy New: pick board type + revision ─────────────────────────

function StepDeployNew({ onSelected, onCreatedSn, onBespokeSelected }) {
  const [boardType, setBoardType]             = useState(null);
  const [boardVersion, setBoardVersion]       = useState("1.0");
  const [creating, setCreating]               = useState(false);
  const [error, setError]                     = useState("");
  const [showBespokePicker, setShowBespokePicker] = useState(false);

  const handleCreate = async () => {
    if (!boardType || !boardVersion.trim()) return;
    setError("");
    setCreating(true);
    try {
      const batch = await api.post("/manufacturing/batch", {
        board_type: boardType,
        board_version: boardVersion.trim(),
        quantity: 1,
      });
      const sn = batch.serial_numbers[0];
      onCreatedSn(sn); // register for abort-cleanup
      const device = await api.get(`/manufacturing/devices/${sn}`);
      onSelected(device);
    } catch (err) {
      setError(err.message);
    } finally {
      setCreating(false);
    }
  };

  const handleBespokeConfirm = ({ firmware, hwFamily }) => {
    setShowBespokePicker(false);
    onBespokeSelected({ firmware, hwFamily });
  };

  // Group boards by family for row layout
  const vesperBoards  = BOARD_TYPES.filter((b) => b.family === "vesper");
  const agnusBoards   = BOARD_TYPES.filter((b) => b.family === "agnus");
  const chronosBoards = BOARD_TYPES.filter((b) => b.family === "chronos");

  const renderTileRow = (boards, cols) => (
    <div style={{ display: "grid", gridTemplateColumns: `repeat(${cols}, 1fr)`, gap: 10, justifyItems: "stretch" }}>
      {boards.map((bt) => (
        <BoardTypeTile
          key={bt.value}
          bt={bt}
          isSelected={boardType === bt.value}
          pal={BOARD_FAMILY_COLORS[bt.family]}
          onClick={() => setBoardType(bt.value)}
        />
      ))}
    </div>
  );

  return (
    <div className="space-y-5">
      {/* Board type tiles — Vesper: 3 col, Agnus: 2 col centered, Chronos: 2 col centered */}
      <div>
        <p className="text-sm font-medium mb-3" style={{ color: "var(--text-secondary)" }}>Board Type</p>
        <div className="space-y-2">
          {renderTileRow(vesperBoards, 3)}
          <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10 }}>
            <div style={{ gridColumn: "1 / 3", display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
              {agnusBoards.map((bt) => (
                <BoardTypeTile
                  key={bt.value}
                  bt={bt}
                  isSelected={boardType === bt.value}
                  pal={BOARD_FAMILY_COLORS[bt.family]}
                  onClick={() => setBoardType(bt.value)}
                />
              ))}
            </div>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10 }}>
            <div style={{ gridColumn: "1 / 3", display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
              {chronosBoards.map((bt) => (
                <BoardTypeTile
                  key={bt.value}
                  bt={bt}
                  isSelected={boardType === bt.value}
                  pal={BOARD_FAMILY_COLORS[bt.family]}
                  onClick={() => setBoardType(bt.value)}
                />
              ))}
            </div>
          </div>
        </div>
      </div>

      {/* Board revision (left) + Generate button (right) */}
      <div className="flex items-end justify-between gap-4">
        <div>
          <label className="block text-sm font-medium mb-1.5" style={{ color: "var(--text-secondary)" }}>
            Board Revision
          </label>
          <div className="flex items-center gap-2">
            <span className="text-sm font-medium" style={{ color: "var(--text-muted)" }}>Rev</span>
            <input
              type="text"
              value={boardVersion}
              onChange={(e) => setBoardVersion(e.target.value)}
              placeholder="1.0"
              className="px-3 py-2 rounded-md text-sm border w-32"
              style={inputStyle()}
            />
          </div>
          <p className="text-xs mt-1" style={{ color: "var(--text-muted)" }}>
            Semantic versioning: 1.0, 1.1, 2.0…
          </p>
        </div>

        <div className="flex flex-col items-end gap-2">
          <ErrorBox msg={error} />
          <button
            onClick={handleCreate}
            disabled={!boardType || creating}
            className="px-5 py-2 text-sm rounded-md font-medium hover:opacity-90 transition-opacity cursor-pointer disabled:opacity-40"
            style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
          >
            {creating ? "Creating…" : "Generate Serial & Continue →"}
          </button>
        </div>
      </div>

      {/* Bespoke divider */}
      <div className="flex items-center gap-3 pt-1">
        <div style={{ flex: 1, height: 1, backgroundColor: "var(--border-secondary)" }} />
        <span className="text-xs" style={{ color: "var(--text-muted)" }}>or</span>
        <div style={{ flex: 1, height: 1, backgroundColor: "var(--border-secondary)" }} />
      </div>

      {/* Bespoke option */}
      <div
        className="rounded-lg border p-4 flex items-center justify-between gap-4"
        style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
      >
        <div>
          <p className="text-sm font-semibold" style={{ color: "var(--text-heading)" }}>Select Bespoke Firmware</p>
          <p className="text-xs mt-0.5" style={{ color: "var(--text-muted)" }}>
            Flash a one-off bespoke firmware with a custom hardware family written to NVS.
          </p>
        </div>
        <button
          onClick={() => setShowBespokePicker(true)}
          className="px-4 py-2 text-sm rounded-md font-medium hover:opacity-90 transition-opacity cursor-pointer"
          style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-secondary)", border: "1px solid var(--border-primary)", flexShrink: 0 }}
        >
          Select Bespoke →
        </button>
      </div>

      {showBespokePicker && (
        <BespokePickerModal
          onConfirm={handleBespokeConfirm}
          onClose={() => setShowBespokePicker(false)}
        />
      )}
    </div>
  );
}

// ─── Shared info cell ─────────────────────────────────────────────────────────

function InfoCell({ label, value, mono = false }) {
  return (
    <div>
      <p className="text-xs uppercase tracking-wide mb-0.5" style={{ color: "var(--text-muted)" }}>{label}</p>
      <p className={`text-sm ${mono ? "font-mono" : ""}`} style={{ color: "var(--text-primary)" }}>{value || "—"}</p>
    </div>
  );
}

// ─── Step 2 — Flash ────────────────────────────────────────────────────────────

function StepFlash({ device, bespokeOverride, onFlashed }) {
  const [portConnected, setPortConnected]   = useState(false);
  const [portName, setPortName]             = useState("");
  const [connecting, setConnecting]         = useState(false);
  const [flashing, setFlashing]             = useState(false);
  const [done, setDone]                     = useState(false);
  const [blProgress, setBlProgress]         = useState(0);
  const [partProgress, setPartProgress]     = useState(0);
  const [nvsProgress, setNvsProgress]       = useState(0);
  const [fwProgress, setFwProgress]         = useState(0);
  const [log, setLog]                       = useState([]);
  const [serial, setSerial]                 = useState([]);
  const [serialAutoScroll, setSerialAutoScroll] = useState(true);
  const [error, setError]                   = useState("");
  const [nvsSchema, setNvsSchema]           = useState("new");

  const loaderRef           = useRef(null);
  const portRef             = useRef(null);
  const serialReaderRef     = useRef(null);
  const serialActiveRef     = useRef(false);
  const serialAutoScrollRef = useRef(true);   // mirrors state — readable inside async loops
  const logEndRef           = useRef(null);
  const serialEndRef        = useRef(null);

  // Keep the ref in sync with state so async loops always see the current value
  const handleSetSerialAutoScroll = (val) => {
    serialAutoScrollRef.current = val;
    setSerialAutoScroll(val);
  };

  const appendLog    = (msg) => setLog((prev) => [...prev, String(msg)]);
  const appendSerial = (msg) => setSerial((prev) => [...prev, String(msg)]);

  const scrollLog    = () => logEndRef.current?.scrollIntoView({ behavior: "smooth" });
  const scrollSerial = () => { if (serialAutoScrollRef.current) serialEndRef.current?.scrollIntoView({ behavior: "smooth" }); };

  // When auto-scroll is re-enabled, jump to bottom immediately
  useEffect(() => {
    if (serialAutoScroll) serialEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [serialAutoScroll]);

  const fetchBinary = async (url) => {
    const token = localStorage.getItem("access_token");
    const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
    if (!resp.ok) {
      const err = await resp.json().catch(() => ({}));
      throw new Error(err.detail || `Failed to fetch ${url}: ${resp.status}`);
    }
    return resp.arrayBuffer();
  };

  const arrayBufferToString = (buf) => {
    const bytes = new Uint8Array(buf);
    let str = "";
    for (let i = 0; i < bytes.length; i++) str += String.fromCharCode(bytes[i]);
    return str;
  };

  const startSerialMonitor = async (port) => {
    serialActiveRef.current = true;
    await new Promise((r) => setTimeout(r, 1000));
    try {
      await port.open({ baudRate: 115200 });
    } catch (openErr) {
      appendSerial(`[Error opening port: ${openErr.message}]`);
      scrollSerial();
      return;
    }
    let reader;
    try {
      reader = port.readable.getReader();
    } catch (readerErr) {
      appendSerial(`[Error getting reader: ${readerErr.message}]`);
      scrollSerial();
      try { await port.close(); } catch (_) {}
      return;
    }
    serialReaderRef.current = reader;
    const textDecoder = new TextDecoder();
    let lineBuffer = "";
    try {
      while (serialActiveRef.current) {
        const { value, done: streamDone } = await reader.read();
        if (streamDone) break;
        lineBuffer += textDecoder.decode(value, { stream: true });
        const lines = lineBuffer.split(/\r?\n/);
        lineBuffer = lines.pop();
        for (const line of lines) {
          if (line.trim()) {
            appendSerial(line);
            scrollSerial();
          }
        }
      }
    } catch (_) {
      // Reader cancelled on cleanup — expected
    } finally {
      try { reader.releaseLock(); } catch (_) {}
    }
  };

  const disconnectPort = async () => {
    serialActiveRef.current = false;
    try { await serialReaderRef.current?.cancel(); } catch (_) {}
    try { serialReaderRef.current?.releaseLock(); } catch (_) {}
    try { await portRef.current?.close(); } catch (_) {}
    portRef.current = null;
    setPortConnected(false);
    setPortName("");
    appendSerial("[Port disconnected]");
  };

  const handleConnectPort = async () => {
    setError("");
    setConnecting(true);
    try {
      const port = await navigator.serial.requestPort();
      portRef.current = port;
      // Try to get a display name from port info
      const info = port.getInfo?.() || {};
      const label = info.usbVendorId
        ? `USB ${info.usbVendorId.toString(16).toUpperCase()}:${(info.usbProductId || 0).toString(16).toUpperCase()}`
        : "Serial Port";
      setPortName(label);
      setPortConnected(true);
    } catch (err) {
      setError(err.message || "Port selection cancelled.");
    } finally {
      setConnecting(false);
    }
  };

  const handleStartFlash = async () => {
    if (!portRef.current) return;
    setError("");
    setLog([]);
    setSerial([]);
    setBlProgress(0);
    setPartProgress(0);
    setNvsProgress(0);
    setFwProgress(0);
    setDone(false);

    const port = portRef.current;

    try {
      // 1. Fetch binaries
      const sn = device.serial_number;

      // For bespoke: use the hwFamily's flash assets and override NVS params.
      // The bootloader/partitions endpoints accept an optional hw_type_override query param.
      const blUrl   = bespokeOverride
        ? `/api/manufacturing/devices/${sn}/bootloader.bin?hw_type_override=${bespokeOverride.hwFamily}`
        : `/api/manufacturing/devices/${sn}/bootloader.bin`;
      const partUrl = bespokeOverride
        ? `/api/manufacturing/devices/${sn}/partitions.bin?hw_type_override=${bespokeOverride.hwFamily}`
        : `/api/manufacturing/devices/${sn}/partitions.bin`;
      const nvsSchemaParam = nvsSchema === "legacy" ? "&nvs_schema=legacy" : "";
      const nvsUrl  = bespokeOverride
        ? `/api/manufacturing/devices/${sn}/nvs.bin?hw_type_override=${bespokeOverride.hwFamily}&hw_revision_override=1.0${nvsSchemaParam}`
        : `/api/manufacturing/devices/${sn}/nvs.bin${nvsSchema === "legacy" ? "?nvs_schema=legacy" : ""}`;

      appendLog("Fetching bootloader binary…");
      const blBuffer = await fetchBinary(blUrl);
      appendLog(`Bootloader: ${blBuffer.byteLength} bytes`);

      appendLog("Fetching partition table binary…");
      const partBuffer = await fetchBinary(partUrl);
      appendLog(`Partition table: ${partBuffer.byteLength} bytes`);

      appendLog("Fetching NVS binary…");
      const nvsBuffer = await fetchBinary(nvsUrl);
      appendLog(`NVS: ${nvsBuffer.byteLength} bytes`);

      appendLog("Fetching firmware binary…");
      const fwUrl = bespokeOverride
        ? `/api/firmware/bespoke/${bespokeOverride.firmware.channel}/${bespokeOverride.firmware.version}/firmware.bin`
        : `/api/manufacturing/devices/${sn}/firmware.bin`;
      const fwBuffer = await fetchBinary(fwUrl);
      appendLog(`Firmware: ${fwBuffer.byteLength} bytes`);

      // 2. Connect ESPLoader
      setFlashing(true);
      appendLog("Connecting to ESP32…");

      const transport = new Transport(port, true);
      loaderRef.current = new ESPLoader({
        transport,
        baudrate: FLASH_BAUD,
        terminal: {
          clean() {},
          writeLine: (line) => { appendLog(line); scrollLog(); },
          write:     (msg)  => { appendLog(msg);  scrollLog(); },
        },
      });

      await loaderRef.current.main();
      appendLog("ESP32 connected.");

      // 3. Flash all four regions: bootloader → partition table → NVS → firmware
      //    fileIndex: 0=bootloader, 1=partitions, 2=nvs, 3=firmware
      const blData   = arrayBufferToString(blBuffer);
      const partData = arrayBufferToString(partBuffer);
      const nvsData  = arrayBufferToString(nvsBuffer);
      const fwData   = arrayBufferToString(fwBuffer);

      await loaderRef.current.writeFlash({
        fileArray: [
          { data: blData,   address: 0x1000  },  // bootloader
          { data: partData, address: 0x8000  },  // partition table
          { data: nvsData,  address: NVS_ADDRESS },  // 0x9000
          { data: fwData,   address: FW_ADDRESS  },  // 0x10000
        ],
        flashSize: "keep", flashMode: "keep", flashFreq: "keep",
        eraseAll: false, compress: true,
        reportProgress(fileIndex, written, total) {
          const pct = (written / total) * 100;
          if      (fileIndex === 0) { setBlProgress(pct); }
          else if (fileIndex === 1) { setBlProgress(100); setPartProgress(pct); }
          else if (fileIndex === 2) { setPartProgress(100); setNvsProgress(pct); }
          else                      { setNvsProgress(100); setFwProgress(pct); }
        },
        calculateMD5Hash: () => "",
      });

      setBlProgress(100);
      setPartProgress(100);
      setNvsProgress(100);
      setFwProgress(100);
      appendLog("Flash complete. Resetting device…");

      // 4. Hard-reset via RTS pulse
      try {
        const t = loaderRef.current.transport;
        await t.setRTS(true);
        await new Promise((r) => setTimeout(r, 100));
        await t.setRTS(false);
        await new Promise((r) => setTimeout(r, 100));
      } catch (rstErr) {
        appendLog(`[Reset warning: ${rstErr.message}]`);
      }
      appendLog("Hard reset sent. Device is booting…");

      // 5. Disconnect esptool transport
      try { await loaderRef.current.transport.disconnect(); } catch (_) {}
      appendLog("esptool disconnected. Opening serial monitor…");

      // 6. Update status → flashed
      await api.request(`/manufacturing/devices/${device.serial_number}/status`, {
        method: "PATCH",
        body: JSON.stringify({ status: "flashed", note: "Flashed via browser provisioning wizard" }),
      });

      setFlashing(false);
      setDone(true);

      // 7. Re-open for serial monitor
      appendSerial("── Serial monitor started (115200 baud) ──");
      startSerialMonitor(port);
    } catch (err) {
      setError(err.message || String(err));
      setFlashing(false);
      try { await loaderRef.current?.transport?.disconnect(); } catch (_) {}
    }
  };

  const webSerialAvailable = "serial" in navigator;
  const busy = connecting || flashing;

  // ── Row 1: Info+buttons (left) | Flash Output (right) ──────────────────────
  const InfoPanel = (
    <div
      className="rounded-lg border p-5 flex flex-col"
      style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
    >
      {/* Header: title + COM status */}
      <div className="flex items-center justify-between mb-4">
        <h3 className="text-sm font-semibold uppercase tracking-wide" style={{ color: "var(--text-muted)" }}>
          Device to Flash
        </h3>
        <button
          onClick={portConnected ? disconnectPort : undefined}
          title={portConnected ? `${portName} — Click to disconnect` : "No port connected"}
          className={`flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium transition-all ${portConnected ? "cursor-pointer hover:opacity-80" : "cursor-default"}`}
          style={{
            backgroundColor: portConnected ? "#0a2e1a" : "var(--bg-card-hover)",
            color: portConnected ? "#4dd6c8" : "var(--text-muted)",
            border: `1px solid ${portConnected ? "#4dd6c8" : "var(--border-primary)"}`,
          }}
        >
          <span className="inline-block w-2 h-2 rounded-full" style={{ backgroundColor: portConnected ? "#22c55e" : "#444" }} />
          {portConnected ? portName || "Connected" : "No Port"}
          {portConnected && <span className="ml-1 opacity-60">✕</span>}
        </button>
      </div>

      <div className="grid grid-cols-2 gap-3 mb-4">
        <InfoCell label="Serial Number" value={device.serial_number} mono />
        {bespokeOverride ? (
          <>
            <InfoCell label="NVS hw_type" value={bespokeOverride.hwFamily} />
            <InfoCell label="NVS hw_revision" value="1.0" />
            <div>
              <p className="text-xs uppercase tracking-wide mb-0.5" style={{ color: "var(--text-muted)" }}>Firmware</p>
              <p className="text-xs font-mono" style={{ color: "#fb923c" }}>
                BESPOKE · {bespokeOverride.firmware.bespoke_uid}
              </p>
              <p className="text-xs font-mono mt-0.5" style={{ color: "var(--text-muted)" }}>
                v{bespokeOverride.firmware.version} / {bespokeOverride.firmware.channel}
              </p>
            </div>
          </>
        ) : (
          <InfoCell label="Board" value={`${BOARD_TYPE_LABELS[device.hw_type] || device.hw_type} ${formatHwVersion(device.hw_version)}`} />
        )}
      </div>

      {!webSerialAvailable && (
        <div className="text-sm rounded-md p-3 mb-3 border"
          style={{ backgroundColor: "#2e1a00", borderColor: "#fb923c", color: "#fb923c" }}>
          Web Serial API not available. Use Chrome or Edge on a desktop system.
        </div>
      )}

      <ErrorBox msg={error} />
      {error && <div className="h-2" />}

      {(flashing || blProgress > 0) && (
        <div className="space-y-3 mb-1">
          <div style={{ display: "flex", gap: "1rem" }}>
            <ProgressBar flex label="Bootloader (0x1000)"      percent={blProgress} />
            <ProgressBar flex label="Partition Table (0x8000)" percent={partProgress} />
          </div>
          <ProgressBar label="NVS (0x9000)"    percent={nvsProgress} />
          <ProgressBar label="Firmware (0x10000)" percent={fwProgress} />
        </div>
      )}

      {/* Spacer — pushes bottom bar to the actual bottom of the card */}
      <div style={{ flex: 1 }} />

      {/* Bottom bar: info (left) | buttons (right) */}
      <div
        className="flex items-end justify-between gap-3 pt-3 mt-2"
        style={{ borderTop: "1px solid var(--border-secondary)" }}
      >
        {/* Left: status hint + tech info */}
        <div>
          {portConnected && !flashing && !done && log.length === 0 && (
            <div className="flex items-center gap-2 text-sm mb-1.5" style={{ color: "#4dd6c8" }}>
              <span className="inline-block w-2 h-2 rounded-full" style={{ backgroundColor: "#22c55e" }} />
              Ready to flash.
            </div>
          )}
          {flashing && (
            <p className="text-sm mb-1.5" style={{ color: "var(--text-muted)" }}>Flashing — do not disconnect…</p>
          )}
          <p className="text-xs" style={{ color: "var(--text-muted)", opacity: 0.6 }}>
            NVS 0x9000 · FW 0x10000 · {FLASH_BAUD} baud
          </p>
        </div>

        {/* Right: action buttons */}
        {!busy && (
          <div className="flex items-center gap-2 flex-wrap">
            {!portConnected && (
              <button
                onClick={handleConnectPort}
                disabled={!webSerialAvailable}
                className="flex items-center gap-2 px-4 py-2 text-sm rounded-md font-medium hover:opacity-90 transition-opacity cursor-pointer disabled:opacity-50"
                style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-secondary)", border: "1px solid var(--border-primary)" }}
              >
                <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
                </svg>
                Select COM Port
              </button>
            )}
            {portConnected && done && (
              <button
                onClick={handleStartFlash}
                className="flex items-center gap-2 px-4 py-2 text-sm rounded-md hover:opacity-80 transition-opacity cursor-pointer"
                style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-secondary)" }}
              >
                Flash Again
              </button>
            )}
            {done && (
              <button
                onClick={onFlashed}
                className="flex items-center gap-2 px-4 py-2 text-sm rounded-md font-medium hover:opacity-90 transition-opacity cursor-pointer"
                style={{ backgroundColor: "var(--accent)", color: "var(--bg-primary)" }}
              >
                Proceed to Verify →
              </button>
            )}
            {portConnected && !done && (
              <>
                <select
                  value={nvsSchema}
                  onChange={(e) => setNvsSchema(e.target.value)}
                  disabled={flashing}
                  className="px-3 py-2 text-sm rounded-md border cursor-pointer font-medium"
                  style={{
                    backgroundColor: "var(--bg-input)",
                    borderColor: "var(--border-primary)",
                    color: "var(--text-secondary)",
                  }}
                >
                  <option value="new">NVS: Current Gen</option>
                  <option value="legacy">NVS: Legacy Gen</option>
                </select>
                <button
                  onClick={handleStartFlash}
                  className="flex items-center gap-2 px-5 py-2 text-sm rounded-md font-medium hover:opacity-90 transition-opacity cursor-pointer"
                  style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
                >
                  <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
                  </svg>
                  Start Flashing
                </button>
              </>
            )}
          </div>
        )}
      </div>
    </div>
  );

  const FlashOutputPanel = (
    <div className="rounded-lg border overflow-hidden flex flex-col" style={{ borderColor: "var(--border-primary)", height: 320 }}>
      <div
        className="px-3 py-2 text-xs font-semibold uppercase tracking-wide border-b"
        style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)", color: "var(--text-muted)" }}
      >
        Flash Output
      </div>
      <div
        className="p-3 font-mono text-xs overflow-y-auto space-y-0.5"
        style={{ backgroundColor: "var(--bg-primary)", color: "var(--text-secondary)", flex: 1, minHeight: 0 }}
      >
        {log.length === 0 ? (
          <span style={{ color: "var(--text-muted)", opacity: 0.5 }}>
            {flashing ? "Connecting…" : "Output will appear here once flashing starts."}
          </span>
        ) : (
          log.map((line, i) => <div key={i}>{line}</div>)
        )}
        <div ref={logEndRef} />
      </div>
    </div>
  );

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
      {/* Row 1: Info | Flash Output */}
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12, alignItems: "stretch" }}>
        {InfoPanel}
        {FlashOutputPanel}
      </div>

      {/* Row 2: Serial Output — full width, resizable by drag.
          ↓ EDIT THIS VALUE to adjust the serial monitor height ↓ */}
      <div
        className="rounded-lg border"
        style={{
          borderColor: "var(--border-primary)",
          display: "flex",
          flexDirection: "column",
          minHeight: 180,
          height: 320,           /* ← change this number to adjust height */
          resize: "vertical",
          overflow: "auto",
        }}
      >
        <div
          className="px-3 py-2 text-xs font-semibold uppercase tracking-wide border-b flex items-center justify-between"
          style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)", color: "var(--text-muted)", flexShrink: 0 }}
        >
          <div className="flex items-center gap-2">
            <span>Serial Output</span>
            {serialActiveRef.current && (
              <span className="flex items-center gap-1.5" style={{ color: "#4dd6c8" }}>
                <span className="inline-block w-1.5 h-1.5 rounded-full" style={{ backgroundColor: "#4dd6c8" }} />
                Live
              </span>
            )}
          </div>
          <label className="flex items-center gap-1.5 cursor-pointer select-none">
            <span className="text-xs" style={{ color: "var(--text-muted)" }}>Auto-scroll</span>
            <span
              onClick={() => handleSetSerialAutoScroll(!serialAutoScrollRef.current)}
              className="relative inline-flex items-center"
              style={{
                width: 32, height: 18,
                backgroundColor: serialAutoScroll ? "var(--accent)" : "var(--bg-card-hover)",
                borderRadius: 9,
                border: "1px solid var(--border-primary)",
                cursor: "pointer",
                transition: "background-color 0.2s",
                flexShrink: 0,
              }}
            >
              <span
                style={{
                  position: "absolute",
                  left: serialAutoScroll ? 14 : 2,
                  width: 14, height: 14,
                  borderRadius: "50%",
                  backgroundColor: "#fff",
                  transition: "left 0.15s",
                }}
              />
            </span>
          </label>
        </div>
        <div
          className="p-3 font-mono text-xs space-y-0.5"
          style={{ backgroundColor: "var(--bg-primary)", color: "#a3e635", flex: 1, overflowY: "auto", minHeight: 0 }}
        >
          {serial.length === 0 ? (
            <span style={{ color: "var(--text-muted)" }}>
              {done ? "Waiting for device boot…" : "Available after flash completes."}
            </span>
          ) : (
            serial.map((line, i) => <div key={i}>{line}</div>)
          )}
          <div ref={serialEndRef} />
        </div>
      </div>
    </div>
  );
}

// ─── Step 3 — Verify ──────────────────────────────────────────────────────────

function StepVerify({ device, onVerified }) {
  const [polling, setPolling]       = useState(false);
  const [timedOut, setTimedOut]     = useState(false);
  const [verified, setVerified]     = useState(false);
  const [heartbeatData, setHeartbeatData] = useState(null);
  const [error, setError]           = useState("");
  const intervalRef                 = useRef(null);
  const timeoutRef                  = useRef(null);

  // Auto-start polling on mount
  const startPolling = useCallback(() => {
    if (polling) return;
    setPolling(true);
    setTimedOut(false);
    setError("");

    const startTime = Date.now();

    intervalRef.current = setInterval(async () => {
      try {
        const hbData = await api.get(
          `/mqtt/heartbeats/${device.serial_number}?limit=1&offset=0`
        );
        if (hbData.heartbeats?.length > 0) {
          const latest = hbData.heartbeats[0];
          const receivedMs = latest.received_at
            ? Date.parse(latest.received_at.replace(" ", "T") + "Z")
            : NaN;
          if (!isNaN(receivedMs) && receivedMs > startTime) {
            clearInterval(intervalRef.current);
            clearTimeout(timeoutRef.current);
            try {
              await api.request(`/manufacturing/devices/${device.serial_number}/status`, {
                method: "PATCH",
                body: JSON.stringify({ status: "provisioned", note: "Auto-verified via wizard" }),
              });
            } catch (_) {}
            const deviceData = await api.get(`/manufacturing/devices/${device.serial_number}`);
            setHeartbeatData(latest);
            setPolling(false);
            setVerified(true);
            onVerified({ ...deviceData, mfg_status: "provisioned" });
            return;
          }
        }
      } catch (err) {
        setError(err.message);
      }
    }, VERIFY_POLL_MS);

    timeoutRef.current = setTimeout(() => {
      clearInterval(intervalRef.current);
      setPolling(false);
      setTimedOut(true);
    }, VERIFY_TIMEOUT_MS);
  }, [polling, device.serial_number, onVerified]);

  // Auto-start on mount
  useEffect(() => {
    startPolling();
    return () => {
      clearInterval(intervalRef.current);
      clearTimeout(timeoutRef.current);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const stopPolling = () => {
    clearInterval(intervalRef.current);
    clearTimeout(timeoutRef.current);
    setPolling(false);
  };

  return (
    <div className="space-y-4">
      {/* Fixed-height card so layout doesn't jump when data arrives */}
      <div
        className="rounded-lg border p-5"
        style={{
          backgroundColor: "var(--bg-card)",
          borderColor: "var(--border-primary)",
          minHeight: 280,
        }}
      >
        <h3 className="text-sm font-semibold uppercase tracking-wide mb-4" style={{ color: "var(--text-muted)" }}>
          Waiting for Device
        </h3>

        {/* Loading state */}
        {polling && !verified && (
          <div className="flex flex-col items-center py-6 gap-4">
            <svg className="w-12 h-12 animate-spin" style={{ color: "var(--accent)" }} fill="none" viewBox="0 0 24 24">
              <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
              <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
            </svg>
            <p className="text-sm text-center" style={{ color: "var(--text-secondary)" }}>
              Waiting for device to connect…
              <br />
              <span className="text-xs" style={{ color: "var(--text-muted)" }}>
                Power cycle the device and ensure it can reach the MQTT broker.
              </span>
            </p>
            <button
              onClick={stopPolling}
              className="px-4 py-1.5 text-xs rounded-md hover:opacity-80 transition-opacity cursor-pointer"
              style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-secondary)" }}
            >
              Stop
            </button>
          </div>
        )}

        {/* Verified state */}
        {verified && heartbeatData && (
          <div className="space-y-4">
            <div className="flex items-center gap-3 mb-2">
              <div className="w-8 h-8 rounded-full flex items-center justify-center" style={{ backgroundColor: "#0a2e2a" }}>
                <svg className="w-4 h-4" style={{ color: "#4dd6c8" }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
                </svg>
              </div>
              <p className="text-sm font-semibold" style={{ color: "#4dd6c8" }}>Device is live!</p>
            </div>
            <div className="grid grid-cols-2 gap-3 rounded-md p-3" style={{ backgroundColor: "var(--bg-primary)" }}>
              <InfoCell label="Firmware" value={heartbeatData.firmware_version || "—"} />
              <InfoCell label="IP Address" value={heartbeatData.ip_address || "—"} />
              <InfoCell label="Uptime" value={heartbeatData.uptime_display || "—"} />
              <InfoCell label="Gateway" value={heartbeatData.gateway || "—"} />
            </div>
          </div>
        )}

        {/* Timed out state */}
        {timedOut && !verified && (
          <div className="py-4 space-y-4">
            <div
              className="text-sm rounded-md p-3 border text-center"
              style={{ backgroundColor: "#2e1a00", borderColor: "#fb923c", color: "#fb923c" }}
            >
              Timed out after {VERIFY_TIMEOUT_MS / 1000}s. Check WiFi credentials and MQTT broker connectivity.
            </div>
            <div className="flex gap-3">
              <button
                onClick={startPolling}
                className="px-5 py-2 text-sm rounded-md font-medium hover:opacity-90 transition-opacity cursor-pointer"
                style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
              >
                Retry Verification
              </button>
            </div>
          </div>
        )}

        {error && !timedOut && !verified && (
          <div className="mt-3">
            <ErrorBox msg={`Poll error (will retry): ${error}`} />
          </div>
        )}
      </div>

      <p className="text-xs" style={{ color: "var(--text-muted)" }}>
        Polling every {VERIFY_POLL_MS / 1000}s · timeout {VERIFY_TIMEOUT_MS / 1000}s
      </p>
    </div>
  );
}

// ─── Step 4 — Done ────────────────────────────────────────────────────────────

function StepDone({ device, onProvisionNext }) {
  const navigate = useNavigate();

  return (
    <div className="space-y-4">
      <div
        className="rounded-lg border p-6"
        style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
      >
        <div className="flex flex-col items-center py-4 gap-4">
          <div className="w-16 h-16 rounded-full flex items-center justify-center" style={{ backgroundColor: "#0a2e2a" }}>
            <svg className="w-8 h-8" style={{ color: "#4dd6c8" }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
            </svg>
          </div>
          <div className="text-center">
            <h3 className="text-lg font-bold" style={{ color: "var(--text-heading)" }}>Device Provisioned</h3>
            <p className="text-sm mt-1" style={{ color: "var(--text-muted)" }}>
              {device.serial_number} is live.
            </p>
          </div>
        </div>

        <div className="rounded-md mb-5" style={{ backgroundColor: "var(--bg-primary)" }}>
          {/* Row 1: Serial Number (left) | Status (right) */}
          <div className="grid grid-cols-2" style={{ borderBottom: "1px solid var(--border-secondary)" }}>
            <div className="p-4" style={{ borderRight: "1px solid var(--border-secondary)" }}>
              <p className="text-xs uppercase tracking-wide mb-1" style={{ color: "var(--text-muted)" }}>Serial Number</p>
              <p className="text-sm font-mono" style={{ color: "var(--text-primary)" }}>{device.serial_number}</p>
            </div>
            <div className="p-4 flex flex-col items-end">
              <p className="text-xs uppercase tracking-wide mb-1" style={{ color: "var(--text-muted)" }}>Status</p>
              <StatusBadge status={device.mfg_status} />
            </div>
          </div>
          {/* Row 2: Board Type (left) | HW Version (right) */}
          <div className="grid grid-cols-2">
            <div className="p-4" style={{ borderRight: "1px solid var(--border-secondary)" }}>
              <p className="text-xs uppercase tracking-wide mb-1" style={{ color: "var(--text-muted)" }}>Board Type</p>
              <p className="text-sm" style={{ color: "var(--text-primary)" }}>{BOARD_TYPE_LABELS[device.hw_type] || device.hw_type}</p>
            </div>
            <div className="p-4 flex flex-col items-end">
              <p className="text-xs uppercase tracking-wide mb-1" style={{ color: "var(--text-muted)" }}>HW Version</p>
              <p className="text-sm" style={{ color: "var(--text-primary)" }}>{formatHwVersion(device.hw_version)}</p>
            </div>
          </div>
        </div>

        <div className="flex flex-wrap gap-3">
          <button
            onClick={onProvisionNext}
            className="px-5 py-2 text-sm rounded-md font-medium hover:opacity-90 transition-opacity cursor-pointer"
            style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
          >
            Provision Next Device
          </button>
          <button
            onClick={() => navigate(`/manufacturing/devices/${device.serial_number}`)}
            className="px-4 py-2 text-sm rounded-md hover:opacity-80 transition-opacity cursor-pointer"
            style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-secondary)" }}
          >
            View in Inventory
          </button>
        </div>
      </div>
    </div>
  );
}

// ─── Main Wizard ──────────────────────────────────────────────────────────────
// Steps:
//   0 = Mode picker ("Flash Existing" vs "Deploy New")
//   1 = Select device (varies by mode)
//   2 = Flash
//   3 = Verify
//   4 = Done

export default function ProvisioningWizard() {
  const [step, setStep]           = useState(0);
  const [mode, setMode]           = useState(null); // "existing" | "new"
  const [device, setDevice]       = useState(null);
  const [createdSn, setCreatedSn] = useState(null); // SN created in Deploy New, for abort cleanup
  // Bespoke override: { firmware: FirmwareVersion, hwFamily: string } | null
  const [bespokeOverride, setBespokeOverride] = useState(null);

  const handleModePicked = (m) => {
    setMode(m);
    setStep(1);
  };

  const handleDeviceSelected = (dev) => {
    setBespokeOverride(null);
    setDevice(dev);
    setStep(2);
  };

  // Called from StepDeployNew when user picks a bespoke firmware.
  // We skip serial generation and go straight to flash with a synthetic device stub.
  const handleBespokeSelected = ({ firmware, hwFamily }) => {
    setBespokeOverride({ firmware, hwFamily });
    // Create a minimal device-like object — serial will be generated on this step
    // but for bespoke we still need a real serial. Trigger normal Deploy New flow
    // with a placeholder boardType that maps to hwFamily, then override at flash time.
    // For simplicity: generate a serial with board_type=hwFamily, board_version="1.0".
    (async () => {
      try {
        const batch = await api.post("/manufacturing/batch", {
          board_type: "vesper",  // placeholder — NVS will be overridden with the bespoke hwFamily
          board_version: "1.0",
          quantity: 1,
        });
        const sn = batch.serial_numbers[0];
        setCreatedSn(sn);
        const dev = await api.get(`/manufacturing/devices/${sn}`);
        setDevice(dev);
        setStep(2);
      } catch (err) {
        // eslint-disable-next-line no-console
        console.error("Failed to create bespoke serial:", err);
      }
    })();
  };

  const handleFlashed = () => setStep(3);

  const handleVerified = (updatedDevice) => {
    setDevice(updatedDevice);
    setStep(4);
  };

  const handleBack = async () => {
    if (step === 1) {
      // Going back to mode picker — if Deploy New created a serial, clean it up
      if (mode === "new" && createdSn) {
        await cleanupCreatedSn(createdSn);
        setCreatedSn(null);
      }
      setStep(0);
      setMode(null);
      setDevice(null);
      setBespokeOverride(null);
    } else if (step === 2) {
      setStep(1);
      setDevice(null);
      setBespokeOverride(null);
    } else if (step === 3) {
      setStep(2);
    }
  };

  const handleAbort = async () => {
    if (mode === "new" && createdSn) {
      await cleanupCreatedSn(createdSn);
      setCreatedSn(null);
    }
    setStep(0);
    setMode(null);
    setDevice(null);
    setBespokeOverride(null);
  };

  const handleProvisionNext = () => {
    setDevice(null);
    setCreatedSn(null);
    setBespokeOverride(null);
    setStep(0);
    setMode(null);
  };

  // Determine Back/Abort visibility
  const showBack  = step >= 1 && step <= 3;
  const showAbort = step >= 1 && step <= 3;

  // Flash step takes full available width; all others are centered at 720px
  const isFlashStep = step === 2;

  return (
    <div style={{ display: "flex", flexDirection: "column", minHeight: "100%" }}>
      {/* ── Sticky top bar ── */}
      <div
        style={{
          position: "sticky",
          top: 0,
          zIndex: 50,
          backgroundColor: "var(--bg-primary)",
          borderBottom: "1px solid var(--border-primary)",
          padding: "12px 24px",
        }}
      >
        {/* Title + controls row */}
        <div className="flex items-center justify-between gap-4">
          {/* Left: Title */}
          <h1 className="text-2xl font-bold" style={{ color: "var(--text-heading)", minWidth: 0, flexShrink: 0 }}>
            Provisioning Wizard
          </h1>

          {/* Center: StepIndicator — always visible, current=step+1 to account for Begin as step 1 */}
          <div style={{ flex: 1, display: "flex", justifyContent: "center", paddingBottom: 8 }}>
            <StepIndicator current={step + 1} />
          </div>

          {/* Right: Back + Abort grouped together */}
          <div className="flex items-center gap-2" style={{ flexShrink: 0 }}>
            {showBack && (
              <button
                onClick={handleBack}
                className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-opacity hover:opacity-80 cursor-pointer"
                style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-secondary)" }}
              >
                ← Back
              </button>
            )}
            {showAbort && (
              <button
                onClick={handleAbort}
                className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-opacity hover:opacity-80 cursor-pointer"
                style={{ backgroundColor: "var(--danger-bg)", color: "var(--danger-text)", border: "1px solid var(--danger)" }}
              >
                Abort ✕
              </button>
            )}
          </div>
        </div>
      </div>

      {/* ── Step content ── */}
      <div
        style={{
          flex: 1,
          padding: isFlashStep ? "24px" : "32px 24px",
          ...(isFlashStep
            ? {}
            : { display: "flex", justifyContent: "center" }),
        }}
      >
        <div style={isFlashStep ? {} : { width: "100%", maxWidth: 720 }}>
          {step === 0 && <StepModePicker onPick={handleModePicked} />}

          {step === 1 && mode === "existing" && (
            <StepSelectExisting onSelected={handleDeviceSelected} />
          )}
          {step === 1 && mode === "new" && (
            <StepDeployNew
              onSelected={handleDeviceSelected}
              onCreatedSn={(sn) => setCreatedSn(sn)}
              onBespokeSelected={handleBespokeSelected}
            />
          )}

          {step === 2 && device && (
            <StepFlash device={device} bespokeOverride={bespokeOverride} onFlashed={handleFlashed} />
          )}
          {step === 3 && device && (
            <StepVerify device={device} onVerified={handleVerified} />
          )}
          {step === 4 && device && (
            <StepDone device={device} onProvisionNext={handleProvisionNext} />
          )}
        </div>
      </div>
    </div>
  );
}

// ─── Cleanup helper ───────────────────────────────────────────────────────────
// Deletes a newly-created (un-flashed) serial number when user aborts Deploy New

async function cleanupCreatedSn(sn) {
  try {
    await api.request(`/manufacturing/devices/${sn}`, { method: "DELETE" });
  } catch (_) {
    // Best-effort — if the endpoint doesn't exist yet, silently ignore
  }
}
