// Mission Control rearchitecture, Phase 1 — Admin UI for Device Profiles
// (§10-13 of the "Mission Control — Full Functional Scope" spec). See
// migrations/0042_device_profiles.sql and routes/admin-device-profiles.ts
// for the schema/API this page drives.
//
// Two nested levels, same "list page -> row -> modal" idiom as
// admin-staff-page.jsx:
//   1. AdminDeviceProfilesPage: list every profile, create/edit/delete,
//      each row opens...
//   2. ManageSlotsModal: the profile's ordered slots (the §13
//      PROFILE-level fields: Device Category, Device Type, Display
//      Name, Enabled, Sort Order, API Integration, Control/Telemetry/
//      Event Enabled) — add/edit/delete slots here.
//
// A third piece, AssetDeviceProfileModal, is NOT reached from this page —
// it's launched from admin-assets-page.jsx's UnitRow ("Device profile"
// button) since it operates per-ASSET: assign/clear this asset's
// profile, then fill in the §13 ASSET-level fields (External Device ID,
// Serial Number, IMEI, MAC) per slot. Exported here (not defined there)
// because it needs VALID_INTEGRATIONS-style knowledge of the profile
// domain, but referenced by name from admin-assets-page.jsx — safe
// regardless of <script> load order since that reference only resolves
// at render time, long after every site/*.jsx file has executed (same
// reasoning already relied on throughout this codebase, e.g. every admin
// page referencing AdminShell).
//
// Reuses ModalShell/ModalError/SectionLabel/EmptyNote/MiniField/
// ActionButton/adminInputStyle/adminSelectStyle from admin-assets-page.jsx
// (loaded earlier — see index.html) rather than redefining them.

// Presets shown via <datalist> for convenience only — the backend never
// enforces these (Rule 19: new vendors/device types must not require a
// schema or code change), so admins can always type something else.
const DP_CATEGORY_PRESETS = ["Ajax", "Victron", "Teltonika", "EFOY", "Other"];
// "Camera Relay" was renamed to "Reboot Relay" per direct customer
// instruction (2026-09-05, device-mapping auto-population request),
// matching the asset-import spreadsheet's "Ajax Reboot Relay Serial"
// column. This is a display-only rename: the underlying /relay/i
// detection in UM_DeviceDetailPage (unit-management-page.jsx) still
// matches any relay name generically, so no other relay-handling logic
// needed to change.
// "LED Relay" and "Schedule Relay" are two separate, genuinely distinct
// physical Ajax relays -- CORRECTED 2026-09-06 (customer, after live
// testing with a real unit: "i made a mistake there is 3 relays").
// A prior 2026-09-05 instruction had mistakenly described "LED Relay" as
// a duplicate/legacy name for "Schedule Relay"; that assumption has been
// reverted here and in lib/assetImport.ts's extractDeviceMappingCandidates().
const DP_TYPE_PRESETS = [
  "Hub", "Camera", "Tilt/Tamper Sensor", "LED Relay", "Schedule Relay", "Reboot Relay", "Sounder",
  "GlobalLink 520", "MPPT Charge Controller", "Battery Shunt", "Mains Charger",
  "Router", "Speakerphone", "Fuel Cell", "Network Switch", "Battery",
];
const DP_INTEGRATIONS = [
  { value: "none", label: "None" },
  { value: "victron", label: "Victron VRM" },
  { value: "ajax", label: "Ajax Systems" },
  { value: "teltonika", label: "Teltonika RMS" },
  { value: "efoy", label: "EFOY" },
];

function AdminDeviceProfilesPage({ onNavigate }) {
  const [state, setState] = useState({ status: "loading", admin: null, profiles: [] });
  const [busyId, setBusyId] = useState(null);
  const [showAdd, setShowAdd] = useState(false);
  const [editingProfile, setEditingProfile] = useState(null); // profile row or null
  const [managingProfile, setManagingProfile] = useState(null); // profile row or null

  const load = () => {
    Promise.all([
      fetch("/api/admin/me", { credentials: "same-origin" }).then((r) => (r.ok ? r.json() : Promise.reject())),
      fetch("/api/admin/device-profiles", { credentials: "same-origin" }).then((r) => (r.ok ? r.json() : Promise.reject())),
    ])
      .then(([me, profiles]) => {
        setState({ status: "ready", admin: me.admin, profiles: profiles.profiles || [] });
      })
      .catch(() => onNavigate("admin-login"));
  };
  useEffect(load, []); // eslint-disable-line react-hooks/exhaustive-deps -- mount-only

  const toggleActive = async (profile) => {
    setBusyId(profile.id);
    try {
      await fetch(`/api/admin/device-profiles/${profile.id}`, {
        method: "PATCH", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ is_active: !profile.is_active }),
      });
      load();
    } finally { setBusyId(null); }
  };

  const deleteProfile = async (profile) => {
    if (!window.confirm(`Delete "${profile.name}"? This can't be undone.`)) return;
    setBusyId(profile.id);
    try {
      const res = await fetch(`/api/admin/device-profiles/${profile.id}`, { method: "DELETE", credentials: "same-origin" });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { window.alert(data.error || "Something went wrong."); return; }
      load();
    } finally { setBusyId(null); }
  };

  if (state.status === "loading") {
    return <section style={{ background: "#fff", minHeight: "calc(100vh - 88px)" }} />;
  }

  const isReadOnly = state.admin && state.admin.role !== "super_admin";

  return (
    <AdminShell admin={state.admin} page="admin-device-profiles" onNavigate={onNavigate}
      subtitle="Staff only · Mission Control" title="Device profiles"
      actions={<ActionButton readOnly={isReadOnly} onClick={() => setShowAdd(true)} testId="dp-add-btn">+ New profile</ActionButton>}>
      <EmptyNote>
        A Device Profile defines the hardware an asset is expected to carry (§8-11). Assign a
        profile to an asset from the Assets page ("Device profile" button) to control what shows
        up in that asset's Mission Control Devices nav.
      </EmptyNote>
      <SectionLabel>Profiles ({state.profiles.length})</SectionLabel>
      {state.profiles.length === 0 ? (
        <EmptyNote>No device profiles yet.</EmptyNote>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          {state.profiles.map((profile) => (
            <ProfileRow
              key={profile.id} profile={profile} busy={busyId === profile.id} isReadOnly={isReadOnly}
              onEdit={() => setEditingProfile(profile)}
              onManageSlots={() => setManagingProfile(profile)}
              onToggleActive={() => toggleActive(profile)}
              onDelete={() => deleteProfile(profile)}
            />
          ))}
        </div>
      )}

      {showAdd && (
        <ProfileFormModal title="New device profile"
          onCancel={() => setShowAdd(false)}
          onSaved={() => { setShowAdd(false); load(); }}
        />
      )}
      {editingProfile && (
        <ProfileFormModal title={`Edit ${editingProfile.name}`} existing={editingProfile}
          onCancel={() => setEditingProfile(null)}
          onSaved={() => { setEditingProfile(null); load(); }}
        />
      )}
      {managingProfile && (
        <ManageSlotsModal profile={managingProfile} isReadOnly={isReadOnly}
          onCancel={() => setManagingProfile(null)}
          onChanged={load}
        />
      )}
    </AdminShell>
  );
}

function ProfileRow({ profile, busy, isReadOnly, onEdit, onManageSlots, onToggleActive, onDelete }) {
  return (
    <div data-testid={`dp-row-${profile.id}`} style={{
      background: "rgba(0,0,0,0.03)", border: "1px solid rgba(0,0,0,0.14)",
      padding: "16px 20px", display: "flex", justifyContent: "space-between",
      alignItems: "center", flexWrap: "wrap", gap: 14,
    }}>
      <div>
        <div style={{ fontFamily: "var(--font-body)", fontSize: 14.5, fontWeight: 600, color: "#000" }}>
          {profile.name}
          {!profile.is_active && (
            <span style={{
              marginLeft: 10, fontSize: 9.5, letterSpacing: "0.14em", textTransform: "uppercase",
              color: "rgba(190,40,40,0.85)", fontWeight: 500,
            }}>Inactive</span>
          )}
        </div>
        {profile.description && (
          <div style={{ fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.55)", marginTop: 4 }}>
            {profile.description}
          </div>
        )}
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", marginTop: 4 }}>
          {profile.slot_count} slot{profile.slot_count === 1 ? "" : "s"} · used by {profile.assigned_asset_count} asset{profile.assigned_asset_count === 1 ? "" : "s"}
        </div>
      </div>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
        <ActionButton disabled={busy} onClick={onManageSlots} testId={`dp-slots-${profile.id}`}>Manage slots</ActionButton>
        <ActionButton disabled={busy} readOnly={isReadOnly} onClick={onEdit}>Edit</ActionButton>
        <ActionButton disabled={busy} readOnly={isReadOnly} onClick={onToggleActive}>
          {profile.is_active ? "Deactivate" : "Activate"}
        </ActionButton>
        <ActionButton disabled={busy} danger readOnly={isReadOnly} onClick={onDelete} testId={`dp-delete-${profile.id}`}>
          Delete
        </ActionButton>
      </div>
    </div>
  );
}

function ProfileFormModal({ title, existing, onCancel, onSaved }) {
  const [name, setName] = useState(existing ? existing.name : "");
  const [description, setDescription] = useState(existing ? existing.description : "");
  const [isActive, setIsActive] = useState(existing ? !!existing.is_active : true);
  const [status, setStatus] = useState("idle");
  const [error, setError] = useState("");

  const handleSubmit = async (e) => {
    e.preventDefault();
    setStatus("submitting");
    setError("");
    try {
      const res = existing
        ? await fetch(`/api/admin/device-profiles/${existing.id}`, {
            method: "PATCH", credentials: "same-origin",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ name, description, is_active: isActive }),
          })
        : await fetch("/api/admin/device-profiles", {
            method: "POST", credentials: "same-origin",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ name, description }),
          });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setError(data.error || "Something went wrong."); setStatus("idle"); return; }
      onSaved();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
      setStatus("idle");
    }
  };

  return (
    <ModalShell title={title} onCancel={onCancel}>
      {error && <ModalError>{error}</ModalError>}
      <form onSubmit={handleSubmit}>
        <MiniField label="Name">
          <input style={adminInputStyle} value={name} onChange={(e) => setName(e.target.value)} required
            placeholder="e.g. Standard Solo Trailer" />
        </MiniField>
        <MiniField label="Description (optional)">
          <input style={adminInputStyle} value={description} onChange={(e) => setDescription(e.target.value)}
            placeholder="What kind of unit this profile is for" />
        </MiniField>
        {existing && (
          <MiniField label="Status">
            <label style={{ display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--font-body)", fontSize: 13.5, color: "#000" }}>
              <input type="checkbox" checked={isActive} onChange={(e) => setIsActive(e.target.checked)} />
              Active
            </label>
          </MiniField>
        )}
        <div style={{ display: "flex", justifyContent: "flex-end", gap: 10, marginTop: 10 }}>
          <ActionButton disabled={status === "submitting"} danger onClick={onCancel}>Cancel</ActionButton>
          <button type="submit" disabled={status === "submitting"} style={{
            background: "#000", color: "#fff", border: "1px solid #000",
            padding: "10px 18px", cursor: "pointer",
            fontFamily: "var(--font-body)", fontSize: 11.5, fontWeight: 500,
            letterSpacing: "0.14em", textTransform: "uppercase",
          }}>
            {status === "submitting" ? "Saving…" : existing ? "Save changes" : "Create profile"}
          </button>
        </div>
      </form>
    </ModalShell>
  );
}

// Fetches its own fresh copy of {profile, slots} on mount/refresh rather
// than trusting the summary row passed in from the list page (which only
// has slot_count, not the slots themselves).
function ManageSlotsModal({ profile, isReadOnly, onCancel, onChanged }) {
  const [state, setState] = useState({ status: "loading", slots: [] });
  const [showAdd, setShowAdd] = useState(false);
  const [editingSlot, setEditingSlot] = useState(null);
  const [busyId, setBusyId] = useState(null);

  const load = () => {
    fetch(`/api/admin/device-profiles/${profile.id}`, { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setState({ status: "ready", slots: data.slots || [] }))
      .catch(() => setState({ status: "error", slots: [] }));
  };
  useEffect(load, [profile.id]); // eslint-disable-line react-hooks/exhaustive-deps

  const deleteSlot = async (slot) => {
    if (!window.confirm(`Remove the "${slot.display_name}" slot? Any asset device mapping for it will also be cleared.`)) return;
    setBusyId(slot.id);
    try {
      await fetch(`/api/admin/device-profiles/${profile.id}/slots/${slot.id}`, { method: "DELETE", credentials: "same-origin" });
      load();
      onChanged();
    } finally { setBusyId(null); }
  };

  return (
    <ModalShell title={`${profile.name} — slots`} onCancel={onCancel} wide>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
        <SectionLabel>Slots ({state.slots.length})</SectionLabel>
        <ActionButton readOnly={isReadOnly} onClick={() => setShowAdd(true)} testId="dp-slot-add-btn">+ Add slot</ActionButton>
      </div>

      {state.status === "loading" && (
        <div style={{ fontFamily: "var(--font-body)", fontSize: 13, color: "rgba(0,0,0,0.5)" }}>Loading…</div>
      )}
      {state.status === "ready" && state.slots.length === 0 && (
        <EmptyNote>No slots yet — add one for each piece of hardware this profile should expect.</EmptyNote>
      )}
      {state.status === "ready" && state.slots.length > 0 && (
        <div style={{ display: "flex", flexDirection: "column", gap: 10, marginBottom: 10 }}>
          {state.slots.map((slot) => (
            <SlotRow key={slot.id} slot={slot} busy={busyId === slot.id} isReadOnly={isReadOnly}
              onEdit={() => setEditingSlot(slot)} onDelete={() => deleteSlot(slot)} />
          ))}
        </div>
      )}

      <div style={{ display: "flex", justifyContent: "flex-end", marginTop: 18 }}>
        <ActionButton onClick={onCancel}>Close</ActionButton>
      </div>

      {showAdd && (
        <SlotFormModal profileId={profile.id}
          onCancel={() => setShowAdd(false)}
          onSaved={() => { setShowAdd(false); load(); onChanged(); }}
        />
      )}
      {editingSlot && (
        <SlotFormModal profileId={profile.id} existing={editingSlot}
          onCancel={() => setEditingSlot(null)}
          onSaved={() => { setEditingSlot(null); load(); onChanged(); }}
        />
      )}
    </ModalShell>
  );
}

function SlotRow({ slot, busy, isReadOnly, onEdit, onDelete }) {
  const integrationLabel = (DP_INTEGRATIONS.find((i) => i.value === slot.api_integration) || {}).label || slot.api_integration;
  const flags = [];
  if (slot.control_enabled) flags.push("Control");
  if (slot.telemetry_enabled) flags.push("Telemetry");
  if (slot.event_enabled) flags.push("Events");

  return (
    <div style={{
      background: "rgba(0,0,0,0.03)", border: "1px solid rgba(0,0,0,0.12)",
      padding: "12px 16px", display: "flex", justifyContent: "space-between",
      alignItems: "center", flexWrap: "wrap", gap: 12,
    }}>
      <div>
        <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, fontWeight: 600, color: "#000" }}>
          {slot.display_name}
          {!slot.enabled && (
            <span style={{ marginLeft: 8, fontSize: 9, letterSpacing: "0.12em", textTransform: "uppercase", color: "rgba(190,40,40,0.8)" }}>Disabled</span>
          )}
        </div>
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.5)", marginTop: 3 }}>
          {slot.device_category} · {slot.device_type} · key: {slot.slot_key} · sort {slot.sort_order}
        </div>
        <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(0,0,0,0.45)", marginTop: 3 }}>
          API: {integrationLabel}{flags.length > 0 ? ` · ${flags.join(", ")}` : ""}
        </div>
      </div>
      <div style={{ display: "flex", gap: 8 }}>
        <ActionButton disabled={busy} readOnly={isReadOnly} onClick={onEdit}>Edit</ActionButton>
        <ActionButton disabled={busy} danger readOnly={isReadOnly} onClick={onDelete}>Delete</ActionButton>
      </div>
    </div>
  );
}

function SlotFormModal({ profileId, existing, onCancel, onSaved }) {
  const [deviceCategory, setDeviceCategory] = useState(existing ? existing.device_category : "");
  const [deviceType, setDeviceType] = useState(existing ? existing.device_type : "");
  const [displayName, setDisplayName] = useState(existing ? existing.display_name : "");
  const [slotKey, setSlotKey] = useState(existing ? existing.slot_key : "");
  const [enabled, setEnabled] = useState(existing ? !!existing.enabled : true);
  const [sortOrder, setSortOrder] = useState(existing ? String(existing.sort_order) : "0");
  const [apiIntegration, setApiIntegration] = useState(existing ? existing.api_integration : "none");
  const [controlEnabled, setControlEnabled] = useState(existing ? !!existing.control_enabled : false);
  const [telemetryEnabled, setTelemetryEnabled] = useState(existing ? !!existing.telemetry_enabled : true);
  const [eventEnabled, setEventEnabled] = useState(existing ? !!existing.event_enabled : true);
  const [status, setStatus] = useState("idle");
  const [error, setError] = useState("");

  const handleSubmit = async (e) => {
    e.preventDefault();
    setStatus("submitting");
    setError("");
    const body = {
      device_category: deviceCategory, device_type: deviceType, display_name: displayName,
      slot_key: slotKey || undefined,
      enabled, sort_order: Number(sortOrder) || 0,
      api_integration: apiIntegration,
      control_enabled: controlEnabled, telemetry_enabled: telemetryEnabled, event_enabled: eventEnabled,
    };
    try {
      const res = existing
        ? await fetch(`/api/admin/device-profiles/${profileId}/slots/${existing.id}`, {
            method: "PATCH", credentials: "same-origin",
            headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
          })
        : await fetch(`/api/admin/device-profiles/${profileId}/slots`, {
            method: "POST", credentials: "same-origin",
            headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
          });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setError(data.error || "Something went wrong."); setStatus("idle"); return; }
      onSaved();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
      setStatus("idle");
    }
  };

  return (
    <ModalShell title={existing ? `Edit ${existing.display_name}` : "New slot"} onCancel={onCancel}>
      {error && <ModalError>{error}</ModalError>}
      <form onSubmit={handleSubmit}>
        <datalist id="dp-category-presets">
          {DP_CATEGORY_PRESETS.map((c) => <option key={c} value={c} />)}
        </datalist>
        <datalist id="dp-type-presets">
          {DP_TYPE_PRESETS.map((t) => <option key={t} value={t} />)}
        </datalist>

        <MiniField label="Display name">
          <input style={adminInputStyle} value={displayName} onChange={(e) => setDisplayName(e.target.value)} required
            placeholder="e.g. MPPT 1" />
        </MiniField>
        <div style={{ display: "flex", gap: 12 }}>
          <div style={{ flex: 1 }}>
            <MiniField label="Device category">
              <input style={adminInputStyle} list="dp-category-presets" value={deviceCategory}
                onChange={(e) => setDeviceCategory(e.target.value)} required placeholder="e.g. Victron" />
            </MiniField>
          </div>
          <div style={{ flex: 1 }}>
            <MiniField label="Device type">
              <input style={adminInputStyle} list="dp-type-presets" value={deviceType}
                onChange={(e) => setDeviceType(e.target.value)} required placeholder="e.g. MPPT Charge Controller" />
            </MiniField>
          </div>
        </div>
        <MiniField label="Slot key (optional — auto-generated if left blank)">
          <input style={adminInputStyle} value={slotKey} onChange={(e) => setSlotKey(e.target.value)}
            placeholder="e.g. victron_mppt_1" />
        </MiniField>
        <div style={{ display: "flex", gap: 12 }}>
          <div style={{ flex: 1 }}>
            <MiniField label="Sort order">
              <input style={adminInputStyle} type="number" value={sortOrder} onChange={(e) => setSortOrder(e.target.value)} />
            </MiniField>
          </div>
          <div style={{ flex: 1 }}>
            <MiniField label="API integration">
              <select style={adminSelectStyle} value={apiIntegration} onChange={(e) => setApiIntegration(e.target.value)}>
                {DP_INTEGRATIONS.map((i) => <option key={i.value} value={i.value}>{i.label}</option>)}
              </select>
            </MiniField>
          </div>
        </div>
        <div style={{ display: "flex", gap: 18, flexWrap: "wrap", margin: "4px 0 18px" }}>
          <label style={{ display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--font-body)", fontSize: 13, color: "#000" }}>
            <input type="checkbox" checked={enabled} onChange={(e) => setEnabled(e.target.checked)} /> Enabled
          </label>
          <label style={{ display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--font-body)", fontSize: 13, color: "#000" }}>
            <input type="checkbox" checked={controlEnabled} onChange={(e) => setControlEnabled(e.target.checked)} /> Control enabled
          </label>
          <label style={{ display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--font-body)", fontSize: 13, color: "#000" }}>
            <input type="checkbox" checked={telemetryEnabled} onChange={(e) => setTelemetryEnabled(e.target.checked)} /> Telemetry enabled
          </label>
          <label style={{ display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--font-body)", fontSize: 13, color: "#000" }}>
            <input type="checkbox" checked={eventEnabled} onChange={(e) => setEventEnabled(e.target.checked)} /> Events enabled
          </label>
        </div>
        <div style={{ display: "flex", justifyContent: "flex-end", gap: 10, marginTop: 10 }}>
          <ActionButton disabled={status === "submitting"} danger onClick={onCancel}>Cancel</ActionButton>
          <button type="submit" disabled={status === "submitting"} style={{
            background: "#000", color: "#fff", border: "1px solid #000",
            padding: "10px 18px", cursor: "pointer",
            fontFamily: "var(--font-body)", fontSize: 11.5, fontWeight: 500,
            letterSpacing: "0.14em", textTransform: "uppercase",
          }}>
            {status === "submitting" ? "Saving…" : existing ? "Save changes" : "Add slot"}
          </button>
        </div>
      </form>
    </ModalShell>
  );
}

// ---------------------------------------------------------------------
// Per-asset: assign a Device Profile + fill in the asset-level mapping
// fields for each of its slots. Launched from admin-assets-page.jsx's
// UnitRow ("Device profile" button).
// ---------------------------------------------------------------------
function AssetDeviceProfileModal({ unit, isReadOnly, onCancel, onChanged }) {
  const [state, setState] = useState({ status: "loading", profiles: [], deviceProfile: null, slots: [] });
  const [selectedProfileId, setSelectedProfileId] = useState("");
  const [assignBusy, setAssignBusy] = useState(false);
  const [assignError, setAssignError] = useState("");

  const load = () => {
    Promise.all([
      fetch("/api/admin/device-profiles", { credentials: "same-origin" }).then((r) => (r.ok ? r.json() : Promise.reject())),
      fetch(`/api/admin/assets/${unit.id}/device-mappings`, { credentials: "same-origin" }).then((r) => (r.ok ? r.json() : Promise.reject())),
    ])
      .then(([profilesData, mappingData]) => {
        setState({
          status: "ready",
          profiles: (profilesData.profiles || []).filter((p) => p.is_active),
          deviceProfile: mappingData.device_profile,
          slots: mappingData.slots || [],
        });
        setSelectedProfileId(mappingData.device_profile ? String(mappingData.device_profile.id) : "");
      })
      .catch(() => setState((s) => ({ ...s, status: "error" })));
  };
  useEffect(load, [unit.id]); // eslint-disable-line react-hooks/exhaustive-deps

  const handleAssign = async () => {
    setAssignBusy(true);
    setAssignError("");
    try {
      const res = await fetch(`/api/admin/assets/${unit.id}/device-profile`, {
        method: "PATCH", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ device_profile_id: selectedProfileId ? Number(selectedProfileId) : null }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setAssignError(data.error || "Something went wrong."); return; }
      load();
      onChanged();
    } finally { setAssignBusy(false); }
  };

  return (
    <ModalShell title={`Device profile — ${unit.serial_number}`} onCancel={onCancel} wide>
      {state.status === "loading" && (
        <div style={{ fontFamily: "var(--font-body)", fontSize: 13, color: "rgba(0,0,0,0.5)" }}>Loading…</div>
      )}
      {state.status === "error" && <ModalError>Couldn't load device profile data.</ModalError>}
      {state.status === "ready" && (
        <React.Fragment>
          {assignError && <ModalError>{assignError}</ModalError>}
          <MiniField label="Assigned device profile">
            <div style={{ display: "flex", gap: 10 }}>
              <select style={{ ...adminSelectStyle, flex: 1 }} value={selectedProfileId}
                onChange={(e) => setSelectedProfileId(e.target.value)} disabled={isReadOnly}>
                <option value="">— None —</option>
                {state.profiles.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
              </select>
              <ActionButton
                disabled={assignBusy || String(state.deviceProfile ? state.deviceProfile.id : "") === selectedProfileId}
                readOnly={isReadOnly} onClick={handleAssign} testId="dp-assign-save">
                Save
              </ActionButton>
            </div>
          </MiniField>

          {!state.deviceProfile ? (
            <EmptyNote>Assign a device profile above to map this asset's hardware.</EmptyNote>
          ) : (
            <React.Fragment>
              <SectionLabel>Devices ({state.slots.length})</SectionLabel>
              {state.slots.length === 0 ? (
                <EmptyNote>This profile has no enabled slots yet — add some from Device Profiles &gt; Manage slots.</EmptyNote>
              ) : (
                <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                  {state.slots.map((slot) => (
                    <AssetSlotMappingRow key={slot.slot_id} unitId={unit.id} slot={slot} isReadOnly={isReadOnly}
                      onSaved={load} />
                  ))}
                </div>
              )}
            </React.Fragment>
          )}
        </React.Fragment>
      )}

      <div style={{ display: "flex", justifyContent: "flex-end", marginTop: 18 }}>
        <ActionButton onClick={onCancel}>Close</ActionButton>
      </div>
    </ModalShell>
  );
}

function AssetSlotMappingRow({ unitId, slot, isReadOnly, onSaved }) {
  const [externalDeviceId, setExternalDeviceId] = useState(slot.external_device_id || "");
  const [serialNumber, setSerialNumber] = useState(slot.serial_number || "");
  const [imei, setImei] = useState(slot.imei || "");
  const [mac, setMac] = useState(slot.mac || "");
  const [notes, setNotes] = useState(slot.notes || "");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");

  const save = async () => {
    setBusy(true);
    setError("");
    try {
      const res = await fetch(`/api/admin/assets/${unitId}/device-mappings/${slot.slot_id}`, {
        method: "PUT", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ external_device_id: externalDeviceId, serial_number: serialNumber, imei, mac, notes }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setError(data.error || "Something went wrong."); return; }
      onSaved();
    } finally { setBusy(false); }
  };

  const clear = async () => {
    if (!window.confirm(`Clear the mapping for "${slot.display_name}"?`)) return;
    setBusy(true);
    try {
      await fetch(`/api/admin/assets/${unitId}/device-mappings/${slot.slot_id}`, { method: "DELETE", credentials: "same-origin" });
      setExternalDeviceId(""); setSerialNumber(""); setImei(""); setMac(""); setNotes("");
      onSaved();
    } finally { setBusy(false); }
  };

  return (
    <div style={{ background: "rgba(0,0,0,0.03)", border: "1px solid rgba(0,0,0,0.12)", padding: "14px 16px" }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 10 }}>
        <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, fontWeight: 600, color: "#000" }}>
          {slot.display_name} <span style={{ color: "rgba(0,0,0,0.45)", fontWeight: 400 }}>· {slot.device_category} / {slot.device_type}</span>
        </div>
        {slot.mapping_id ? (
          <span style={{ fontSize: 10.5, letterSpacing: "0.1em", textTransform: "uppercase", color: "rgba(20,140,60,0.9)" }}>Mapped</span>
        ) : (
          <span style={{ fontSize: 10.5, letterSpacing: "0.1em", textTransform: "uppercase", color: "rgba(0,0,0,0.4)" }}>Unmapped</span>
        )}
      </div>
      {error && <ModalError>{error}</ModalError>}
      <div style={{ display: "flex", gap: 10, flexWrap: "wrap", marginBottom: 10 }}>
        <div style={{ flex: "1 1 160px" }}>
          <MiniField label="External device ID">
            <input style={adminInputStyle} value={externalDeviceId} onChange={(e) => setExternalDeviceId(e.target.value)} disabled={isReadOnly} />
          </MiniField>
        </div>
        <div style={{ flex: "1 1 160px" }}>
          <MiniField label="Serial number">
            <input style={adminInputStyle} value={serialNumber} onChange={(e) => setSerialNumber(e.target.value)} disabled={isReadOnly} />
          </MiniField>
        </div>
        <div style={{ flex: "1 1 160px" }}>
          <MiniField label="IMEI">
            <input style={adminInputStyle} value={imei} onChange={(e) => setImei(e.target.value)} disabled={isReadOnly} />
          </MiniField>
        </div>
        <div style={{ flex: "1 1 160px" }}>
          <MiniField label="MAC">
            <input style={adminInputStyle} value={mac} onChange={(e) => setMac(e.target.value)} disabled={isReadOnly} />
          </MiniField>
        </div>
      </div>
      <MiniField label="Notes">
        <input style={adminInputStyle} value={notes} onChange={(e) => setNotes(e.target.value)} disabled={isReadOnly} />
      </MiniField>
      <div style={{ display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 4 }}>
        {slot.mapping_id && (
          <ActionButton disabled={busy} danger readOnly={isReadOnly} onClick={clear}>Clear</ActionButton>
        )}
        <ActionButton disabled={busy} readOnly={isReadOnly} onClick={save}>Save</ActionButton>
      </div>
    </div>
  );
}

Object.assign(window, { AdminDeviceProfilesPage, AssetDeviceProfileModal });
