import { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { useAuth } from "../auth/AuthContext";
import api from "../api/client";
import FlashAssetManager from "../manufacturing/FlashAssetManager";

const BOARD_TYPES = [
  { value: "vesper",      label: "Vesper" },
  { value: "vesper_plus", label: "Vesper+" },
  { value: "vesper_pro",  label: "Vesper Pro" },
  { value: "chronos",     label: "Chronos" },
  { value: "chronos_pro", label: "Chronos Pro" },
  { value: "agnus",       label: "Agnus" },
  { value: "agnus_mini",  label: "Agnus Mini" },
  { value: "bespoke",     label: "Bespoke" },
];

const CHANNELS = ["stable", "beta", "alpha", "testing"];

const CHANNEL_STYLES = {
  stable:  { bg: "var(--success-bg)", color: "var(--success-text)" },
  beta:    { bg: "var(--badge-blue-bg)", color: "var(--badge-blue-text)" },
  alpha:   { bg: "#2e1a00", color: "#fb923c" },
  testing: { bg: "var(--bg-card-hover)", color: "var(--text-muted)" },
};

function ChannelBadge({ channel }) {
  const style = CHANNEL_STYLES[channel] || CHANNEL_STYLES.testing;
  return (
    <span
      className="px-2 py-0.5 text-xs rounded-full capitalize font-medium"
      style={{ backgroundColor: style.bg, color: style.color }}
    >
      {channel}
    </span>
  );
}

const UPDATE_TYPE_STYLES = {
  mandatory: { bg: "var(--badge-blue-bg)",  color: "var(--badge-blue-text)", label: "Mandatory" },
  emergency: { bg: "var(--danger-bg)",      color: "var(--danger-text)",     label: "Emergency" },
  optional:  { bg: "var(--bg-card-hover)",  color: "var(--text-muted)",      label: "Optional"  },
};

function UpdateTypeBadge({ type }) {
  const style = UPDATE_TYPE_STYLES[type] || UPDATE_TYPE_STYLES.optional;
  return (
    <span
      className="px-2 py-0.5 text-xs rounded-full font-medium"
      style={{ backgroundColor: style.bg, color: style.color }}
    >
      {style.label}
    </span>
  );
}

// ── colour tokens for API param tags ────────────────────────────────────────
const TAG = {
  hwType:   { color: "var(--danger-text)",     bg: "var(--danger-bg)",       border: "var(--danger-text)"      },
  channel:  { color: "var(--badge-blue-text)", bg: "var(--badge-blue-bg)",   border: "var(--badge-blue-text)"  },
  version:  { color: "var(--success-text)",    bg: "var(--success-bg)",      border: "var(--success-text)"     },
  identity: { color: "#b8922a",               bg: "#2a200a",                border: "#b8922a"                 },
};

function ParamTag({ children, kind = "identity", variant = "box" }) {
  const t = TAG[kind];
  if (variant === "inline") {
    return (
      <span style={{
        fontFamily: "monospace", fontSize: "0.72rem",
        color: t.color, fontWeight: "bold",
      }}>
        {children}
      </span>
    );
  }
  return (
    <span style={{
      fontFamily: "monospace", fontSize: "0.68rem",
      backgroundColor: t.bg, color: t.color,
      border: `1px solid ${t.border}`,
      borderRadius: "4px", padding: "1px 7px",
      opacity: 0.9,
    }}>
      {children}
    </span>
  );
}

function EndpointCard({ method, label, pathParts, query, bodyFields, desc }) {
  const isPost = method === "POST";
  const methodStyle = isPost
    ? { bg: "var(--success-bg)",      color: "var(--success-text)"     }
    : { bg: "var(--badge-blue-bg)",   color: "var(--badge-blue-text)"  };
  return (
    <div className="rounded-md p-3 mb-3" style={{ backgroundColor: "var(--bg-secondary)", border: "1px solid var(--border-secondary)" }}>
      <div className="flex items-center gap-2 mb-1.5">
        <span className="text-xs font-bold px-1.5 py-0.5 rounded" style={{ backgroundColor: methodStyle.bg, color: methodStyle.color }}>{method}</span>
        <span className="text-xs font-semibold" style={{ color: "var(--text-muted)" }}>{label}</span>
      </div>
      <div className="flex flex-wrap items-center mb-2" style={{ fontFamily: "monospace", fontSize: "0.72rem" }}>
        <span style={{ color: "var(--text-muted)" }}>console.bellsystems.net</span>
        {pathParts.map((p, i) =>
          p.plain
            ? <span key={i} style={{ color: "var(--text-primary)" }}>{p.text}</span>
            : <ParamTag key={i} kind={p.kind} variant="inline">{p.text}</ParamTag>
        )}
      </div>
      {query && (
        <div className="flex flex-wrap gap-1.5 mb-2">
          {query.map((q) => (
            <ParamTag key={q.key} kind={q.kind}>?{q.key}=<span style={{ opacity: 0.6 }}>{q.eg}</span></ParamTag>
          ))}
        </div>
      )}
      {bodyFields && (
        <div className="flex flex-wrap gap-1.5 mb-2">
          {bodyFields.map((f) => <ParamTag key={f.key} kind={f.kind}>{f.key}</ParamTag>)}
        </div>
      )}
      <p className="text-xs" style={{ color: "var(--text-muted)" }}>{desc}</p>
    </div>
  );
}

const GET_ENDPOINTS = [
  {
    label: "Check for latest version",
    method: "GET",
    pathParts: [
      { text: "/api/firmware/", plain: true },
      { text: "{hw_type}", kind: "hwType" },
      { text: "/", plain: true },
      { text: "{channel}", kind: "channel" },
      { text: "/latest", plain: true },
    ],
    query: [
      { key: "hw_version",      kind: "version",  eg: "1.0"   },
      { key: "current_version", kind: "version",  eg: "1.2.3" },
    ],
    desc: "Returns metadata for the correct next firmware hop. Pass hw_version and current_version so the server resolves upgrade chains correctly.",
  },
  {
    label: "Get specific version info",
    method: "GET",
    pathParts: [
      { text: "/api/firmware/", plain: true },
      { text: "{hw_type}", kind: "hwType" },
      { text: "/", plain: true },
      { text: "{channel}", kind: "channel" },
      { text: "/", plain: true },
      { text: "{version}", kind: "version" },
      { text: "/info", plain: true },
    ],
    desc: "Returns metadata for a specific version. Used when resolving upgrade chains with min_fw_version constraints.",
  },
  {
    label: "Get latest changelog",
    method: "GET",
    pathParts: [
      { text: "/api/firmware/", plain: true },
      { text: "{hw_type}", kind: "hwType" },
      { text: "/", plain: true },
      { text: "{channel}", kind: "channel" },
      { text: "/latest/changelog", plain: true },
    ],
    desc: "Returns the full changelog for the latest firmware as plain text. Not served in metadata — for admin/display use only.",
  },
  {
    label: "Get specific version changelog",
    method: "GET",
    pathParts: [
      { text: "/api/firmware/", plain: true },
      { text: "{hw_type}", kind: "hwType" },
      { text: "/", plain: true },
      { text: "{channel}", kind: "channel" },
      { text: "/", plain: true },
      { text: "{version}", kind: "version" },
      { text: "/info/changelog", plain: true },
    ],
    desc: "Returns the full changelog for a specific firmware version as plain text.",
  },
  {
    label: "Download firmware binary",
    method: "GET",
    pathParts: [
      { text: "/api/firmware/", plain: true },
      { text: "{hw_type}", kind: "hwType" },
      { text: "/", plain: true },
      { text: "{channel}", kind: "channel" },
      { text: "/", plain: true },
      { text: "{version}", kind: "version" },
      { text: "/firmware.bin", plain: true },
    ],
    desc: "Streams the raw .bin file. Devices fetch this after confirming the version via /latest or /info.",
  },
];

const POST_ENDPOINTS = [
  {
    label: "OTA download event",
    method: "POST",
    pathParts: [{ text: "/api/ota/events/download", plain: true }],
    bodyFields: [
      { key: "device_uid",   kind: "identity" },
      { key: "hw_type",      kind: "hwType"   },
      { key: "hw_version",   kind: "version"  },
      { key: "from_version", kind: "version"  },
      { key: "to_version",   kind: "version"  },
      { key: "channel",      kind: "channel"  },
    ],
    desc: "Posted when the binary is fully written to the staged partition (before Update.end()). Best-effort — no retry on failure.",
  },
  {
    label: "OTA flash confirmed",
    method: "POST",
    pathParts: [{ text: "/api/ota/events/flash", plain: true }],
    bodyFields: [
      { key: "device_uid",   kind: "identity" },
      { key: "hw_type",      kind: "hwType"   },
      { key: "hw_version",   kind: "version"  },
      { key: "from_version", kind: "version"  },
      { key: "to_version",   kind: "version"  },
      { key: "channel",      kind: "channel"  },
      { key: "sha256",       kind: "identity" },
    ],
    desc: "Posted after Update.end() succeeds — partition committed, device about to reboot. This is ground truth for fleet version tracking.",
  },
];

function ApiLegend() {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: "0.6rem" }}>
      {[
        { kind: "hwType",   label: "{hw_type}",    eg: "vesper_plus, chronos, agnus"  },
        { kind: "channel",  label: "{channel}",    eg: "stable, beta, alpha, testing" },
        { kind: "version",  label: "{version}",    eg: "1.0 · 2.5.1 · 1.2.3"         },
        { kind: "identity", label: "device_uid / sha256", eg: "BSVSPR-26C13X-… · a3f1…" },
      ].map((row) => (
        <div key={row.kind} className="rounded-md p-2.5" style={{ backgroundColor: "var(--bg-secondary)", border: "1px solid var(--border-secondary)" }}>
          <div className="mb-1"><ParamTag kind={row.kind} variant="box">{row.label}</ParamTag></div>
          <div className="text-xs" style={{ color: "var(--text-muted)", fontFamily: "monospace" }}>{row.eg}</div>
        </div>
      ))}
      <p className="text-xs mt-1" style={{ color: "var(--text-more-muted)", lineHeight: 1.5, textAlign: "center" }}>
        Colour coding is consistent across all endpoints.<br/>The same token type always appears in the same colour whether it is a path segment, query param, or POST body field.
      </p>
    </div>
  );
}

function ApiInfoModal({ onClose }) {
  const [tab, setTab] = useState("get");
  const TABS = [
    { id: "get",  label: "GET"  },
    { id: "post", label: "POST" },
  ];
  // GAP is the single spacing unit: outer padding AND the column gap are all this value.
  const GAP = "2rem";
  return (
    <div
      className="fixed inset-0 flex items-center justify-center z-50"
      style={{ backgroundColor: "rgba(0,0,0,0.6)" }}
      onClick={onClose}
    >
      <div
        className="rounded-lg border flex flex-col"
        style={{
          backgroundColor: "var(--bg-card)",
          borderColor: "var(--border-primary)",
          width: "80%",
          maxHeight: "80vh",
          overflow: "hidden",
        }}
        onClick={(e) => e.stopPropagation()}
      >
        {/* Header */}
        <div className="flex items-center justify-between" style={{ padding: `1.5rem ${GAP} 1.25rem` }}>
          <div>
            <h3 className="text-base font-semibold" style={{ color: "var(--text-heading)" }}>Firmware API</h3>
            <p className="text-xs mt-0.5" style={{ color: "var(--text-muted)" }}>All endpoints are unauthenticated — devices call them directly.</p>
          </div>
          <button
            autoFocus
            onClick={onClose}
            onKeyDown={(e) => e.key === "Escape" && onClose()}
            className="text-lg leading-none hover:opacity-70 cursor-pointer"
            style={{ color: "var(--text-muted)" }}
          >
            ✕
          </button>
        </div>
        {/* Inset divider — indented to match section padding */}
        <div style={{ height: "1px", backgroundColor: "var(--border-secondary)", margin: `0 ${GAP}` }} />

        {/* Body: 2-col × 2-row grid. Row 1 = top bars (same height by grid). Row 2 = content. */}
        <div style={{
          display: "grid",
          gridTemplateColumns: "70fr 30fr",
          gridTemplateRows: "auto 1fr",
          columnGap: GAP,
          overflow: "auto",
          padding: `0 ${GAP}`,
        }}>
          {/* Row 1 left — tab bar */}
          <div className="flex gap-1" style={{ alignItems: "flex-end", paddingTop: "0.75rem" }}>
            {TABS.map((t) => (
              <button
                key={t.id}
                onClick={() => setTab(t.id)}
                className="px-4 py-1.5 text-xs font-medium rounded-t cursor-pointer transition-colors"
                style={{
                  backgroundColor: tab === t.id ? "var(--bg-secondary)" : "transparent",
                  color: tab === t.id ? "var(--text-primary)" : "var(--text-muted)",
                  borderBottom: tab === t.id ? "2px solid var(--btn-primary)" : "2px solid transparent",
                }}
              >
                {t.label}
              </button>
            ))}
          </div>

          {/* Row 1 right — "Legend" label, bottom-aligned to match tab bar */}
          <div style={{ display: "flex", alignItems: "flex-end", paddingTop: "0.75rem", paddingBottom: "0.375rem" }}>
            <p className="text-xs font-semibold" style={{ color: "var(--text-heading)" }}>Legend</p>
          </div>

          {/* Row 2 left — stacked tab panels */}
          <div style={{ position: "relative", paddingTop: "1rem", paddingBottom: GAP }}>
            {TABS.map((t) => (
              <div
                key={t.id}
                style={{
                  ...(tab !== t.id ? {
                    position: "absolute", top: "1rem", left: 0, right: 0,
                    visibility: "hidden", pointerEvents: "none",
                  } : {}),
                }}
              >
                {t.id === "get"  && GET_ENDPOINTS.map((ep)  => <EndpointCard key={ep.label} {...ep} />)}
                {t.id === "post" && POST_ENDPOINTS.map((ep) => <EndpointCard key={ep.label} {...ep} />)}
              </div>
            ))}
          </div>

          {/* Row 2 right — legend items */}
          <div style={{ paddingTop: "1rem", paddingBottom: GAP }}>
            <ApiLegend />
          </div>
        </div>
      </div>
    </div>
  );
}

// ── Icons ─────────────────────────────────────────────────────────────────────

function IconNotes() {
  return (
    <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>
      <line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>
    </svg>
  );
}

function IconLink() {
  return (
    <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
      <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
    </svg>
  );
}

function IconEdit() {
  return (
    <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
      <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
    </svg>
  );
}

function IconTrash() {
  return (
    <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <polyline points="3 6 5 6 21 6"/>
      <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
      <path d="M10 11v6"/><path d="M14 11v6"/>
      <path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
    </svg>
  );
}

function IconChevron() {
  return (
    <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
      <polyline points="6 9 12 15 18 9"/>
    </svg>
  );
}

// ── Full Release Notes modal ──────────────────────────────────────────────────

function ChangelogModal({ fw, onClose }) {
  return (
    <div
      className="fixed inset-0 flex items-center justify-center z-50"
      style={{ backgroundColor: "rgba(0,0,0,0.6)" }}
      onClick={onClose}
    >
      <div
        className="rounded-lg border w-full mx-4"
        style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)", maxWidth: "780px" }}
        onClick={(e) => e.stopPropagation()}
      >
        <div className="flex items-center justify-between px-5 pt-4 pb-3" style={{ borderBottom: "1px solid var(--border-secondary)" }}>
          <h3 className="text-sm font-semibold" style={{ color: "var(--text-heading)" }}>
            Changelog — {fw.hw_type} v{fw.version} ({fw.channel})
          </h3>
          <button onClick={onClose} className="text-lg leading-none hover:opacity-70 cursor-pointer" style={{ color: "var(--text-muted)" }}>✕</button>
        </div>
        <div className="px-5 py-4">
          {fw.changelog ? (
            <p className="text-sm whitespace-pre-wrap" style={{ color: "var(--text-secondary)" }}>{fw.changelog}</p>
          ) : (
            <p className="text-sm" style={{ color: "var(--text-muted)" }}>No changelog for this version.</p>
          )}
        </div>
      </div>
    </div>
  );
}

// ── Helpers ──────────────────────────────────────────────────────────────────

function formatBytes(bytes) {
  if (!bytes) return "—";
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}

function formatDate(iso) {
  if (!iso) return "—";
  try {
    return new Date(iso).toLocaleString("en-US", {
      year: "numeric", month: "short", day: "numeric",
      hour: "2-digit", minute: "2-digit",
    });
  } catch {
    return iso;
  }
}

// ── Column definitions ───────────────────────────────────────────────────────
const ALL_COLUMNS = [
  { key: "hw_type",      label: "Type",         sortable: true,  align: "left"   },
  { key: "channel",      label: "Channel",      sortable: true,  align: "center" },
  { key: "version",      label: "Version",      sortable: true,  align: "center" },
  { key: "update_type",  label: "Update Type",  sortable: true,  align: "center" },
  { key: "min_fw",       label: "Min FW",       sortable: false, align: "center" },
  { key: "size",         label: "Size",         sortable: false, align: "center" },
  { key: "sha256",       label: "SHA-256",      sortable: false, align: "center" },
  { key: "notes",        label: "Release Note", sortable: false, align: "left"   },
  { key: "uploaded_at",  label: "Uploaded",     sortable: true,  align: "center" },
  { key: "is_latest",    label: "Latest",       sortable: false, align: "center" },
];

const DEFAULT_COLS    = ALL_COLUMNS.map((c) => c.key);
const COLS_KEY        = "fw_manager_cols_v1";
const ORDER_KEY       = "fw_manager_col_order_v1";

function loadColPrefs() {
  try {
    const vis   = JSON.parse(localStorage.getItem(COLS_KEY));
    const order = JSON.parse(localStorage.getItem(ORDER_KEY));
    return {
      visible: Array.isArray(vis)   ? vis   : DEFAULT_COLS,
      order:   Array.isArray(order) ? order : DEFAULT_COLS,
    };
  } catch {
    return { visible: DEFAULT_COLS, order: DEFAULT_COLS };
  }
}

function ColumnSelector({ visible, order, onChange }) {
  const [open, setOpen]     = useState(false);
  const [dragIdx, setDragIdx] = useState(null);
  const ref = useRef(null);

  useEffect(() => {
    const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, []);

  const cols = order.map((k) => ALL_COLUMNS.find((c) => c.key === k)).filter(Boolean);

  const toggleCol = (key) => {
    const next = visible.includes(key) ? visible.filter((k) => k !== key) : [...visible, key];
    onChange({ visible: next, order });
  };

  const handleDragStart = (e, idx) => { setDragIdx(idx); e.dataTransfer.effectAllowed = "move"; };
  const handleDragOver  = (e, idx) => {
    e.preventDefault();
    if (dragIdx === null || dragIdx === idx) return;
    const next = [...order];
    const [moved] = next.splice(dragIdx, 1);
    next.splice(idx, 0, moved);
    setDragIdx(idx);
    onChange({ visible, order: next });
  };
  const handleDrop = () => setDragIdx(null);

  return (
    <div style={{ position: "relative" }} ref={ref}>
      <button
        onClick={() => setOpen((v) => !v)}
        className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-md font-medium border cursor-pointer hover:opacity-80 transition-opacity"
        style={{ backgroundColor: "var(--bg-card-hover)", borderColor: "var(--border-primary)", color: "var(--text-secondary)" }}
        title="Choose columns"
      >
        <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/>
          <line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>
        </svg>
        Columns
      </button>
      {open && (
        <div
          style={{
            position: "absolute", top: "calc(100% + 6px)", right: 0, zIndex: 50,
            backgroundColor: "var(--bg-card)", border: "1px solid var(--border-primary)",
            borderRadius: "0.5rem", padding: "0.5rem", minWidth: "180px",
            boxShadow: "0 8px 24px rgba(0,0,0,0.3)",
          }}
        >
          <div className="text-xs font-semibold mb-2 px-1" style={{ color: "var(--text-muted)", textTransform: "uppercase", letterSpacing: "0.06em" }}>Columns</div>
          {cols.map((col, idx) => (
            <div
              key={col.key}
              draggable
              onDragStart={(e) => handleDragStart(e, idx)}
              onDragOver={(e) => handleDragOver(e, idx)}
              onDrop={handleDrop}
              className="flex items-center gap-2 px-1 py-1 rounded cursor-grab"
              style={{
                backgroundColor: dragIdx === idx ? "var(--bg-card-hover)" : "transparent",
                color: "var(--text-secondary)", fontSize: "0.8rem",
              }}
            >
              <svg width="10" height="10" viewBox="0 0 10 14" fill="currentColor" style={{ opacity: 0.4, flexShrink: 0 }}>
                <circle cx="3" cy="2" r="1.2"/><circle cx="7" cy="2" r="1.2"/>
                <circle cx="3" cy="7" r="1.2"/><circle cx="7" cy="7" r="1.2"/>
                <circle cx="3" cy="12" r="1.2"/><circle cx="7" cy="12" r="1.2"/>
              </svg>
              <input
                type="checkbox"
                checked={visible.includes(col.key)}
                onChange={() => toggleCol(col.key)}
                className="h-3.5 w-3.5"
              />
              <span>{col.label}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function SortIcon({ dir }) {
  if (!dir) return <span style={{ opacity: 0.3, fontSize: "0.65rem", marginLeft: "4px" }}>↕</span>;
  return <span style={{ fontSize: "0.65rem", marginLeft: "4px" }}>{dir === "asc" ? "↑" : "↓"}</span>;
}

// ── Actions dropdown (per row) ────────────────────────────────────────────────

function ActionsMenu({ fw, canDelete, canAdd, onNotes, onEdit, onDelete }) {
  const [open, setOpen] = useState(false);
  const [copied, setCopied] = useState(false);
  const [menuPos, setMenuPos] = useState({ top: 0, left: 0 });
  const triggerRef = useRef(null);
  const menuRef = useRef(null);

  useEffect(() => {
    if (!open) return;
    const handler = (e) => {
      if (
        triggerRef.current && !triggerRef.current.contains(e.target) &&
        menuRef.current && !menuRef.current.contains(e.target)
      ) setOpen(false);
    };
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, [open]);

  const openMenu = (e) => {
    e.stopPropagation();
    const rect = triggerRef.current.getBoundingClientRect();
    setMenuPos({ top: rect.bottom + 4, left: rect.right });
    setOpen((v) => !v);
  };

  const url = `/api/firmware/${fw.hw_type}/${fw.channel}/${fw.version}/firmware.bin`;
  const fullUrl = `${window.location.origin}${url}`;

  const handleCopyUrl = () => {
    navigator.clipboard.writeText(fullUrl).then(() => {
      setCopied(true);
      setTimeout(() => { setCopied(false); setOpen(false); }, 1200);
    }).catch(() => {
      const ta = document.createElement("textarea");
      ta.value = fullUrl;
      ta.style.cssText = "position:fixed;top:0;left:0;opacity:0";
      document.body.appendChild(ta);
      ta.focus(); ta.select();
      try { document.execCommand("copy"); setCopied(true); setTimeout(() => { setCopied(false); setOpen(false); }, 1200); } catch (_) {}
      document.body.removeChild(ta);
    });
  };

  const item = (icon, label, onClick, danger = false) => (
    <button
      key={label}
      onClick={(e) => { e.stopPropagation(); onClick(); setOpen(false); }}
      className="flex items-center gap-2 w-full px-3 py-2 text-xs text-left cursor-pointer hover:opacity-80 transition-opacity rounded"
      style={{
        backgroundColor: "transparent",
        color: danger ? "var(--danger-text)" : "var(--text-secondary)",
        whiteSpace: "nowrap",
      }}
    >
      <span style={{ opacity: 0.7 }}>{icon}</span>
      {label}
    </button>
  );

  const menu = open && createPortal(
    <div
      ref={menuRef}
      style={{
        position: "fixed",
        top: menuPos.top,
        left: menuPos.left,
        transform: "translateX(-100%)",
        zIndex: 9999,
        backgroundColor: "var(--bg-card)", border: "1px solid var(--border-primary)",
        borderRadius: "0.5rem", padding: "0.25rem", minWidth: "180px",
        boxShadow: "0 8px 24px rgba(0,0,0,0.35)",
      }}
      onClick={(e) => e.stopPropagation()}
    >
      {item(<IconNotes />, "View Changelog", () => { onNotes(fw); })}
      {item(
        copied ? (
          <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12" /></svg>
        ) : <IconLink />,
        copied ? "Copied!" : "Copy .bin URL",
        handleCopyUrl,
      )}
      {canAdd && (
        <div style={{ borderTop: "1px solid var(--border-secondary)", margin: "0.25rem 0" }} />
      )}
      {canAdd && item(<IconEdit />, "Edit Release", () => { onEdit(fw); })}
      {canDelete && item(<IconTrash />, "Delete Release", () => { onDelete(fw); }, true)}
    </div>,
    document.body,
  );

  return (
    <>
      <button
        ref={triggerRef}
        onClick={openMenu}
        className="flex items-center gap-1 px-2.5 py-1.5 text-xs rounded-md border cursor-pointer hover:opacity-80 transition-opacity"
        style={{
          backgroundColor: open ? "var(--bg-card-hover)" : "transparent",
          borderColor: "var(--border-secondary)",
          color: "var(--text-muted)",
        }}
      >
        Actions <IconChevron />
      </button>
      {menu}
    </>
  );
}

// ── Firmware Upload / Edit modal ──────────────────────────────────────────────

function FirmwareFormModal({ initial, onClose, onSaved }) {
  const isEdit = !!initial;

  const [hwType,      setHwType]      = useState(initial?.hw_type      ?? "vesper");
  const [bespokeUid,  setBespokeUid]  = useState(initial?.bespoke_uid  ?? "");
  const [channel,     setChannel]     = useState(initial?.channel      ?? "stable");
  const [version,     setVersion]     = useState(initial?.version      ?? "");
  const [updateType,  setUpdateType]  = useState(initial?.update_type  ?? "mandatory");
  const [minFw,       setMinFw]       = useState(initial?.min_fw_version ?? "");
  const [changelog,     setChangelog]     = useState(initial?.changelog      ?? "");
  const [releaseNote,   setReleaseNote]   = useState(initial?.release_note   ?? "");
  const [file,        setFile]        = useState(null);
  const [uploading,   setUploading]   = useState(false);
  const [error,       setError]       = useState("");
  const fileInputRef = useRef(null);

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!isEdit && !file) { setError("A .bin file is required."); return; }
    if (hwType === "bespoke" && !bespokeUid.trim()) { setError("Bespoke UID is required."); return; }
    setError("");
    setUploading(true);

    try {
      const formData = new FormData();
      if (!isEdit) {
        formData.append("hw_type", hwType);
        formData.append("channel", channel);
        formData.append("version", version);
        formData.append("update_type", updateType);
        if (minFw) formData.append("min_fw_version", minFw);
        formData.append("changelog", changelog);
        formData.append("release_note", releaseNote);
        if (hwType === "bespoke") formData.append("bespoke_uid", bespokeUid.trim());
        formData.append("file", file);
      } else {
        // PUT — only send changed / always-sent fields
        formData.append("channel", channel);
        formData.append("version", version);
        formData.append("update_type", updateType);
        formData.append("min_fw_version", minFw);
        formData.append("changelog", changelog);
        formData.append("release_note", releaseNote);
        if (hwType === "bespoke") formData.append("bespoke_uid", bespokeUid.trim());
        if (file) formData.append("file", file);
      }

      const token = localStorage.getItem("access_token");
      const url   = isEdit ? `/api/firmware/${initial.id}` : "/api/firmware/upload";
      const method = isEdit ? "PUT" : "POST";
      const response = await fetch(url, {
        method,
        headers: { Authorization: `Bearer ${token}` },
        body: formData,
      });
      if (!response.ok) {
        const err = await response.json().catch(() => ({}));
        throw new Error(err.detail || (isEdit ? "Update failed" : "Upload failed"));
      }
      onSaved();
    } catch (err) {
      setError(err.message);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div
      className="fixed inset-0 flex items-center justify-center z-50"
      style={{ backgroundColor: "rgba(0,0,0,0.6)" }}
      onClick={onClose}
    >
      <div
        className="rounded-lg border flex flex-col w-full mx-4"
        style={{
          backgroundColor: "var(--bg-card)",
          borderColor: "var(--border-primary)",
          maxWidth: "1300px",
          maxHeight: "90vh",
        }}
        onClick={(e) => e.stopPropagation()}
      >
        {/* Top row */}
        <div className="flex items-center justify-between px-6 pt-5 pb-4" style={{ borderBottom: "1px solid var(--border-secondary)", flexShrink: 0 }}>
          <h3 className="text-base font-semibold" style={{ color: "var(--text-heading)" }}>
            {isEdit ? "Edit Release" : "Upload Firmware Release"}
          </h3>
          <button onClick={onClose} className="text-lg leading-none hover:opacity-70 cursor-pointer" style={{ color: "var(--text-muted)" }}>✕</button>
        </div>

        {/* Middle row — 3 columns */}
        <form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", flex: 1, minHeight: 0 }}>
          {error && (
            <div className="mx-6 mt-4 text-sm rounded-md p-3 border" style={{ backgroundColor: "var(--danger-bg)", borderColor: "var(--danger)", color: "var(--danger-text)", flexShrink: 0 }}>
              {error}
            </div>
          )}

          <div
            style={{
              display: "grid",
              gridTemplateColumns: "1fr 1fr 1fr",
              gap: "1.5rem",
              padding: "1.25rem 1.5rem",
              flex: 1,
              minHeight: 0,
              overflow: "hidden",
            }}
          >
            {/* ── LEFT: config (dictates height) ── */}
            <div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
              <div>
                <label className="block text-xs font-medium mb-1" style={{ color: "var(--text-muted)" }}>Board Type</label>
                <select
                  value={hwType}
                  onChange={(e) => setHwType(e.target.value)}
                  disabled={isEdit}
                  className="w-full px-3 py-2 rounded-md text-sm border"
                  style={{ backgroundColor: "var(--bg-input)", borderColor: "var(--border-input)", color: isEdit ? "var(--text-muted)" : "var(--text-primary)", cursor: isEdit ? "not-allowed" : "auto" }}
                >
                  {BOARD_TYPES.map((bt) => (
                    <option key={bt.value} value={bt.value}>{bt.label}</option>
                  ))}
                </select>
                {isEdit && <p className="text-xs mt-1" style={{ color: "var(--text-muted)" }}>Board type cannot be changed after upload.</p>}
              </div>

              {hwType === "bespoke" && (
                <div>
                  <label className="block text-xs font-medium mb-1" style={{ color: "var(--text-muted)" }}>
                    Bespoke UID <span style={{ color: "var(--danger-text)" }}>*</span>
                  </label>
                  <input
                    type="text"
                    value={bespokeUid}
                    onChange={(e) => setBespokeUid(e.target.value)}
                    placeholder="e.g. client-athens-v1"
                    disabled={isEdit}
                    className="w-full px-3 py-2 rounded-md text-sm border"
                    style={{ backgroundColor: "var(--bg-input)", borderColor: "var(--border-input)", color: isEdit ? "var(--text-muted)" : "var(--text-primary)" }}
                  />
                </div>
              )}

              <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.5rem" }}>
                <div>
                  <label className="block text-xs font-medium mb-1" style={{ color: "var(--text-muted)" }}>Channel</label>
                  <select
                    value={channel}
                    onChange={(e) => setChannel(e.target.value)}
                    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)" }}
                  >
                    {CHANNELS.map((c) => <option key={c} value={c}>{c}</option>)}
                  </select>
                </div>
                <div>
                  <label className="block text-xs font-medium mb-1" style={{ color: "var(--text-muted)" }}>Version</label>
                  <input
                    type="text"
                    value={version}
                    onChange={(e) => setVersion(e.target.value)}
                    placeholder="1.5"
                    required
                    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>
                  <label className="block text-xs font-medium mb-1" style={{ color: "var(--text-muted)" }}>Min FW</label>
                  <input
                    type="text"
                    value={minFw}
                    onChange={(e) => setMinFw(e.target.value)}
                    placeholder="e.g. 1.2"
                    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>

              <div>
                <label className="block text-xs font-medium mb-1.5" style={{ color: "var(--text-muted)" }}>Update Type</label>
                <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.5rem" }}>
                  {[
                    { value: "mandatory", label: "Mandatory", desc: "Auto on reboot",  active: { bg: "var(--badge-blue-bg)", border: "var(--badge-blue-text)", color: "var(--badge-blue-text)" } },
                    { value: "emergency", label: "Emergency", desc: "Immediate push",  active: { bg: "var(--danger-bg)",     border: "var(--danger)",           color: "var(--danger-text)"     } },
                    { value: "optional",  label: "Optional",  desc: "User-initiated",  active: { bg: "var(--success-bg)",    border: "var(--success-text)",     color: "var(--success-text)"    } },
                  ].map((opt) => {
                    const isActive = updateType === opt.value;
                    return (
                      <button
                        key={opt.value}
                        type="button"
                        onClick={() => setUpdateType(opt.value)}
                        style={{
                          padding: "0.5rem 0.25rem",
                          borderRadius: "0.5rem",
                          border: `1px solid ${isActive ? opt.active.border : "var(--border-input)"}`,
                          backgroundColor: isActive ? opt.active.bg : "var(--bg-input)",
                          color: isActive ? opt.active.color : "var(--text-muted)",
                          cursor: "pointer",
                          textAlign: "center",
                          transition: "all 0.15s ease",
                        }}
                      >
                        <div style={{ fontSize: "0.75rem", fontWeight: 600 }}>{opt.label}</div>
                        <div style={{ fontSize: "0.65rem", marginTop: "0.15rem", opacity: 0.75 }}>{opt.desc}</div>
                      </button>
                    );
                  })}
                </div>
              </div>
            </div>

            {/* ── MIDDLE: full changelog (expands vertically) ── */}
            <div style={{ display: "flex", flexDirection: "column", minHeight: 0 }}>
              <label className="block text-xs font-medium mb-1" style={{ color: "var(--text-muted)" }}>Changelog</label>
              <textarea
                value={changelog}
                onChange={(e) => setChangelog(e.target.value)}
                placeholder={"Full changelog — describe what changed, bugs fixed, new features, etc.\n\nLike the body of a commit message."}
                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)",
                  flex: 1, resize: "none", minHeight: "160px",
                }}
              />
            </div>

            {/* ── RIGHT: quick note + file upload ── */}
            <div style={{ display: "flex", flexDirection: "column", gap: "1rem", minHeight: 0 }}>
              <div>
                <label className="block text-xs font-medium mb-1" style={{ color: "var(--text-muted)" }}>Release Note</label>
                <input
                  type="text"
                  value={releaseNote}
                  onChange={(e) => setReleaseNote(e.target.value)}
                  placeholder="e.g. Fixed bell timing bug"
                  maxLength={120}
                  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)" }}
                />
                <p className="text-xs mt-1" style={{ color: "var(--text-muted)" }}>Shown in the table and served with OTA metadata.</p>
              </div>

              <div style={{ display: "flex", flexDirection: "column", flex: 1, minHeight: 0 }}>
                <label className="block text-xs font-medium mb-1" style={{ color: "var(--text-muted)" }}>
                  File Upload {isEdit && <span style={{ fontWeight: 400, opacity: 0.7 }}>(leave empty to keep existing)</span>}
                </label>
                <div
                  onClick={() => fileInputRef.current?.click()}
                  onDragOver={(e) => e.preventDefault()}
                  onDrop={(e) => {
                    e.preventDefault();
                    const f = e.dataTransfer.files[0];
                    if (f && f.name.endsWith(".bin")) setFile(f);
                    else if (f) setError("Only .bin files are accepted.");
                  }}
                  style={{
                    flex: 1,
                    display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center",
                    gap: "0.5rem",
                    border: `2px dashed ${file ? "var(--btn-primary)" : "var(--border-input)"}`,
                    borderRadius: "0.625rem",
                    backgroundColor: file ? "var(--badge-blue-bg)" : "var(--bg-input)",
                    cursor: "pointer", transition: "all 0.15s ease",
                    padding: "1rem", minHeight: "90px",
                  }}
                >
                  <input
                    ref={fileInputRef}
                    type="file"
                    accept=".bin"
                    onChange={(e) => {
                      const f = e.target.files[0];
                      if (f && !f.name.endsWith(".bin")) { setError("Only .bin files are accepted."); return; }
                      setFile(f || null);
                    }}
                    style={{ display: "none" }}
                  />
                  {file ? (
                    <>
                      <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--btn-primary)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
                        <polyline points="20 6 9 17 4 12" />
                      </svg>
                      <span style={{ fontSize: "0.75rem", fontWeight: 600, color: "var(--badge-blue-text)", textAlign: "center", wordBreak: "break-all" }}>
                        {file.name}
                      </span>
                      <span style={{ fontSize: "0.68rem", color: "var(--text-muted)" }}>{formatBytes(file.size)}</span>
                    </>
                  ) : (
                    <>
                      <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
                        <line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
                      </svg>
                      <span style={{ fontSize: "0.73rem", color: "var(--text-muted)", textAlign: "center" }}>
                        Click or drop <strong>.bin</strong> file
                      </span>
                    </>
                  )}
                </div>
              </div>
            </div>
          </div>

          {/* Bottom row */}
          <div
            className="flex items-center justify-end gap-3 px-6 py-4"
            style={{ borderTop: "1px solid var(--border-secondary)", flexShrink: 0 }}
          >
            <button
              type="button"
              onClick={onClose}
              className="px-4 py-2 text-sm rounded-md hover:opacity-80 cursor-pointer"
              style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-secondary)" }}
            >
              Cancel
            </button>
            <button
              type="submit"
              disabled={uploading}
              className="px-5 py-2 text-sm rounded-md font-medium hover:opacity-90 transition-opacity cursor-pointer disabled:opacity-50"
              style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
            >
              {uploading ? (isEdit ? "Saving…" : "Uploading…") : (isEdit ? "Save Changes" : "Upload Firmware Release")}
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

// ── Main component ────────────────────────────────────────────────────────────

const BOARD_TYPE_LABELS = {
  vesper: "Vesper", vesper_plus: "Vesper+", vesper_pro: "Vesper Pro",
  chronos: "Chronos", chronos_pro: "Chronos Pro",
  agnus: "Agnus", agnus_mini: "Agnus Mini", bespoke: "Bespoke",
};

export default function FirmwareManager() {
  const { hasPermission } = useAuth();
  const canAdd    = hasPermission("manufacturing", "add");
  const canDelete = hasPermission("manufacturing", "delete");

  const [firmware,      setFirmware]      = useState([]);
  const [loading,       setLoading]       = useState(true);
  const [error,         setError]         = useState("");

  const [hwTypeFilter,  setHwTypeFilter]  = useState("");
  const [channelFilter, setChannelFilter] = useState("");
  const [search,        setSearch]        = useState("");

  // Column state
  const [colPrefs, setColPrefs] = useState(loadColPrefs);
  const { visible: visibleCols, order: colOrder } = colPrefs;

  // Sort state
  const [sortKey, setSortKey] = useState("uploaded_at");
  const [sortDir, setSortDir] = useState("desc");

  // Modal state
  const [showUploadModal,     setShowUploadModal]     = useState(false);
  const [showFlashAssetsModal, setShowFlashAssetsModal] = useState(false);
  const [showApiInfo,         setShowApiInfo]         = useState(false);
  const [editTarget,          setEditTarget]          = useState(null); // fw to edit
  const [deleteTarget,        setDeleteTarget]        = useState(null);
  const [deleting,            setDeleting]            = useState(false);
  const [notesTarget,         setNotesTarget]         = useState(null);

  const handleColChange = (next) => {
    setColPrefs(next);
    localStorage.setItem(COLS_KEY, JSON.stringify(next.visible));
    localStorage.setItem(ORDER_KEY, JSON.stringify(next.order));
  };

  const handleSort = (key) => {
    if (!ALL_COLUMNS.find((c) => c.key === key)?.sortable) return;
    setSortKey((prev) => {
      if (prev === key) setSortDir((d) => d === "asc" ? "desc" : "asc");
      else setSortDir("asc");
      return key;
    });
  };

  const fetchFirmware = async () => {
    setLoading(true);
    setError("");
    try {
      const params = new URLSearchParams();
      if (hwTypeFilter) params.set("hw_type", hwTypeFilter);
      if (channelFilter) params.set("channel", channelFilter);
      const qs = params.toString();
      const data = await api.get(`/firmware${qs ? `?${qs}` : ""}`);
      setFirmware(data.firmware);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchFirmware();
  }, [hwTypeFilter, channelFilter]);

  const handleDelete = async () => {
    if (!deleteTarget) return;
    setDeleting(true);
    try {
      await api.delete(`/firmware/${deleteTarget.id}`);
    } catch (err) {
      setError(err.message);
    } finally {
      setDeleteTarget(null);
      setDeleting(false);
      await fetchFirmware();
    }
  };

  // Filter + search + sort
  const filteredFirmware = firmware
    .filter((fw) => {
      if (!search) return true;
      const q = search.toLowerCase();
      return (
        (fw.hw_type || "").toLowerCase().includes(q) ||
        (fw.channel || "").toLowerCase().includes(q) ||
        (fw.version || "").toLowerCase().includes(q) ||
        (fw.update_type || "").toLowerCase().includes(q) ||
        (fw.changelog || "").toLowerCase().includes(q) ||
        (fw.release_note || "").toLowerCase().includes(q)
      );
    })
    .sort((a, b) => {
      const dir = sortDir === "asc" ? 1 : -1;
      if (sortKey === "hw_type")     return dir * (a.hw_type     || "").localeCompare(b.hw_type     || "");
      if (sortKey === "channel")     return dir * (a.channel     || "").localeCompare(b.channel     || "");
      if (sortKey === "version")     return dir * (a.version     || "").localeCompare(b.version     || "");
      if (sortKey === "update_type") return dir * (a.update_type || "").localeCompare(b.update_type || "");
      if (sortKey === "uploaded_at") return dir * ((a.uploaded_at || "") < (b.uploaded_at || "") ? -1 : 1);
      return 0;
    });

  // Ordered visible columns
  const displayCols = colOrder
    .filter((k) => visibleCols.includes(k))
    .map((k) => ALL_COLUMNS.find((c) => c.key === k))
    .filter(Boolean);

  function renderCell(col, fw) {
    switch (col.key) {
      case "hw_type": return (
        <div>
          <span style={{ color: "var(--text-secondary)", fontWeight: 600 }}>{BOARD_TYPE_LABELS[fw.hw_type] || fw.hw_type}</span>
          {fw.bespoke_uid && (
            <div className="font-mono text-xs mt-0.5" style={{ color: "var(--text-muted)" }}>{fw.bespoke_uid}</div>
          )}
        </div>
      );
      case "channel":     return <ChannelBadge channel={fw.channel} />;
      case "version":     return <span className="font-mono text-xs" style={{ color: "var(--text-primary)" }}>{fw.version}</span>;
      case "update_type": return <UpdateTypeBadge type={fw.update_type} />;
      case "min_fw":      return <span className="font-mono text-xs" style={{ color: "var(--text-muted)" }}>{fw.min_fw_version || "—"}</span>;
      case "size":        return <span className="text-xs" style={{ color: "var(--text-muted)" }}>{formatBytes(fw.size_bytes)}</span>;
      case "sha256":      return (
        <span className="font-mono text-xs" style={{ color: "var(--text-muted)" }} title={fw.sha256}>
          {fw.sha256 ? fw.sha256.slice(0, 12) + "…" : "—"}
        </span>
      );
      case "notes": return (
        <span className="text-xs" style={{ color: "var(--text-secondary)", maxWidth: "240px", display: "block", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={fw.release_note || ""}>
          {fw.release_note || <span style={{ color: "var(--text-muted)", fontStyle: "italic" }}>—</span>}
        </span>
      );
      case "uploaded_at": return <span className="text-xs" style={{ color: "var(--text-muted)" }}>{formatDate(fw.uploaded_at)}</span>;
      case "is_latest":   return fw.is_latest ? (
        <span className="px-2 py-0.5 text-xs rounded-full font-medium" style={{ backgroundColor: "var(--success-bg)", color: "var(--success-text)" }}>
          latest
        </span>
      ) : null;
      default: return null;
    }
  }

  return (
    <div>
      {/* Title row */}
      <div className="mb-3">
        <h1 className="text-2xl font-bold" style={{ color: "var(--text-heading)" }}>
          Firmware Manager
        </h1>
        <p className="text-sm mt-0.5" style={{ color: "var(--text-muted)" }}>
          {filteredFirmware.length}{filteredFirmware.length !== firmware.length ? ` of ${firmware.length}` : ""} version{firmware.length !== 1 ? "s" : ""}
          {hwTypeFilter || channelFilter ? " (filtered)" : ""}
        </p>
      </div>

      {/* Toolbar */}
      <div className="flex items-center gap-2 mb-5">
        <select
          value={hwTypeFilter}
          onChange={(e) => setHwTypeFilter(e.target.value)}
          className="px-3 py-2 rounded-md text-sm border"
          style={{ backgroundColor: "var(--bg-input)", borderColor: "var(--border-input)", color: "var(--text-primary)", flexShrink: 0 }}
        >
          <option value="">All Types</option>
          {BOARD_TYPES.map((bt) => (
            <option key={bt.value} value={bt.value}>{bt.label}</option>
          ))}
        </select>
        <select
          value={channelFilter}
          onChange={(e) => setChannelFilter(e.target.value)}
          className="px-3 py-2 rounded-md text-sm border"
          style={{ backgroundColor: "var(--bg-input)", borderColor: "var(--border-input)", color: "var(--text-primary)", flexShrink: 0 }}
        >
          <option value="">All Channels</option>
          {CHANNELS.map((c) => (
            <option key={c} value={c}>{c}</option>
          ))}
        </select>

        {/* Search */}
        <div style={{ position: "relative", flex: 1, minWidth: 0 }}>
          <svg style={{ position: "absolute", left: "10px", top: "50%", transform: "translateY(-50%)", pointerEvents: "none", color: "var(--text-muted)" }}
            xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
          </svg>
          <input
            type="text"
            placeholder="Search firmware…"
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            className="pl-8 pr-3 py-2 text-sm rounded-md border w-full"
            style={{ backgroundColor: "var(--bg-input)", borderColor: "var(--border-input)", color: "var(--text-primary)" }}
          />
        </div>

        <ColumnSelector visible={visibleCols} order={colOrder} onChange={handleColChange} />
        <button
          onClick={() => setShowApiInfo(true)}
          className="px-3 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 }}
        >
          API Info
        </button>
        {canAdd && (
          <>
            <button
              onClick={() => setShowFlashAssetsModal(true)}
              className="px-3 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 }}
            >
              Flash Assets
            </button>
            <button
              onClick={() => setShowUploadModal(true)}
              className="px-3 py-2 text-sm rounded-md font-medium hover:opacity-90 transition-opacity cursor-pointer"
              style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)", flexShrink: 0 }}
            >
              + Upload
            </button>
          </>
        )}
      </div>

      {error && (
        <div className="text-sm rounded-md p-3 mb-4 border" style={{ backgroundColor: "var(--danger-bg)", borderColor: "var(--danger)", color: "var(--danger-text)" }}>
          {error}
        </div>
      )}

      {/* Table */}
      <div className="rounded-lg border overflow-hidden" style={{ borderColor: "var(--border-primary)" }}>
        <div className="overflow-x-auto">
          <table className="w-full text-sm">
            <thead>
              <tr style={{ backgroundColor: "var(--bg-secondary)", borderBottom: "1px solid var(--border-primary)" }}>
                {displayCols.map((col) => (
                  <th
                    key={col.key}
                    className="px-4 py-3 font-medium"
                    style={{
                      color: "var(--text-muted)",
                      cursor: col.sortable ? "pointer" : "default",
                      userSelect: "none",
                      whiteSpace: "nowrap",
                      textAlign: col.align,
                      // Sticky first column (Type)
                      ...(col.key === "hw_type" ? {
                        position: "sticky", left: 0, zIndex: 2,
                        boxShadow: "2px 0 4px rgba(0,0,0,0.15)",
                      } : {}),
                    }}
                    onClick={() => col.sortable && handleSort(col.key)}
                  >
                    {col.label}
                    {col.sortable && <SortIcon dir={sortKey === col.key ? sortDir : null} />}
                  </th>
                ))}
                {/* Actions column */}
                <th
                  className="px-4 py-3 font-medium text-right"
                  style={{ color: "var(--text-muted)", whiteSpace: "nowrap" }}
                >
                  Actions
                </th>
              </tr>
            </thead>
            <tbody>
              {loading ? (
                <tr>
                  <td colSpan={displayCols.length + 1} className="px-4 py-8 text-center text-sm" style={{ color: "var(--text-muted)" }}>
                    Loading…
                  </td>
                </tr>
              ) : filteredFirmware.length === 0 ? (
                <tr>
                  <td colSpan={displayCols.length + 1} className="px-4 py-8 text-center text-sm" style={{ color: "var(--text-muted)" }}>
                    No firmware versions found.{" "}
                    {canAdd && !search && (
                      <button onClick={() => setShowUploadModal(true)} className="underline cursor-pointer" style={{ color: "var(--text-link)" }}>
                        Upload the first one.
                      </button>
                    )}
                  </td>
                </tr>
              ) : (
                filteredFirmware.map((fw, idx) => (
                  <tr key={fw.id} style={{ borderBottom: "1px solid var(--border-secondary)", backgroundColor: idx % 2 === 1 ? "var(--bg-row-alt)" : undefined }}>
                    {displayCols.map((col) => (
                      <td
                        key={col.key}
                        className="px-4 py-3"
                        style={{
                          textAlign: col.align,
                          // Sticky first column
                          ...(col.key === "hw_type" ? {
                            position: "sticky", left: 0, zIndex: 1,
                            boxShadow: "2px 0 4px rgba(0,0,0,0.1)",
                          } : {}),
                        }}
                      >
                        {renderCell(col, fw)}
                      </td>
                    ))}
                    <td className="px-4 py-3" style={{ textAlign: "right", whiteSpace: "nowrap" }}>
                      <ActionsMenu
                        fw={fw}
                        canDelete={canDelete}
                        canAdd={canAdd}
                        onNotes={(f) => setNotesTarget(f)}
                        onEdit={(f) => setEditTarget(f)}
                        onDelete={(f) => setDeleteTarget(f)}
                      />
                    </td>
                  </tr>
                ))
              )}
            </tbody>
          </table>
        </div>
      </div>

      {/* ── Modals ── */}
      {showApiInfo && <ApiInfoModal onClose={() => setShowApiInfo(false)} />}

      {notesTarget && <ChangelogModal fw={notesTarget} onClose={() => setNotesTarget(null)} />}

      {(showUploadModal || editTarget) && (
        <FirmwareFormModal
          initial={editTarget ?? null}
          onClose={() => { setShowUploadModal(false); setEditTarget(null); }}
          onSaved={async () => {
            setShowUploadModal(false);
            setEditTarget(null);
            await fetchFirmware();
          }}
        />
      )}

      {showFlashAssetsModal && (
        <FlashAssetManager
          onClose={() => setShowFlashAssetsModal(false)}
        />
      )}

      {deleteTarget && (
        <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 p-6 max-w-sm w-full mx-4"
            style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
          >
            <h3 className="text-base font-semibold mb-2" style={{ color: "var(--text-heading)" }}>
              Delete Firmware
            </h3>
            <p className="text-sm mb-5" style={{ color: "var(--text-secondary)" }}>
              Delete{" "}
              <span className="font-mono" style={{ color: "var(--text-primary)" }}>
                {BOARD_TYPE_LABELS[deleteTarget.hw_type] || deleteTarget.hw_type} v{deleteTarget.version} ({deleteTarget.channel})
              </span>
              ? This cannot be undone.
            </p>
            <div className="flex gap-3">
              <button
                onClick={handleDelete}
                disabled={deleting}
                className="px-4 py-2 text-sm rounded-md hover:opacity-90 transition-opacity cursor-pointer disabled:opacity-50"
                style={{ backgroundColor: "var(--danger-btn)", color: "var(--text-white)" }}
              >
                {deleting ? "Deleting…" : "Delete"}
              </button>
              <button
                onClick={() => setDeleteTarget(null)}
                className="px-4 py-2 text-sm rounded-md hover:opacity-80 cursor-pointer"
                style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-secondary)" }}
              >
                Cancel
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}
