// Solo staff admin area — customer records: amend an existing (already
// approved, or any-status) company's invoice/company address, region/
// country, tier, and account-manager details, plus manage that specific
// customer's asset register (add/assign/move/unassign/history) without
// having to cross-reference serials on the main Assets page.
//
// Reached via the sidebar nav in AdminShell (see admin-shell.jsx). Reuses
// FieldBlock/DarkInput/LiveDot/CornerHairlines from reseller-login-page.jsx,
// and MiniField/ActionButton/ModalShell/etc + UnitRow/AddUnitModal/
// AssignUnitModal/HistoryModal from admin-assets-page.jsx (both loaded
// earlier — see index.html) so the per-customer asset register looks and
// behaves identically to the main Assets page.

function AdminCompaniesPage({ onNavigate }) {
  const [state, setState] = useState({ status: "loading", admin: null, companies: [], products: [] });
  const [search, setSearch] = useState("");
  const [statusFilter, setStatusFilter] = useState("");
  const [openCompanyId, setOpenCompanyId] = useState(null);
  const [addOpen, setAddOpen] = useState(false);

  const load = () => {
    Promise.all([
      fetch("/api/admin/me", { credentials: "same-origin" }).then((r) => (r.ok ? r.json() : Promise.reject())),
      fetch("/api/admin/companies", { credentials: "same-origin" }).then((r) => (r.ok ? r.json() : Promise.reject())),
      fetch("/api/admin/products", { credentials: "same-origin" }).then((r) => (r.ok ? r.json() : Promise.reject())),
    ])
      .then(([me, companies, products]) => {
        setState({
          status: "ready", admin: me.admin,
          companies: companies.companies || [], products: products.products || [],
        });
      })
      .catch(() => onNavigate("admin-login"));
  };

  useEffect(load, []); // eslint-disable-line react-hooks/exhaustive-deps -- mount-only

  const visibleCompanies = state.companies.filter((c) => {
    if (statusFilter && c.status !== statusFilter) return false;
    if (search.trim() && !c.name.toLowerCase().includes(search.trim().toLowerCase())) return false;
    return true;
  });

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

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

  return (
    <AdminShell admin={state.admin} page="admin-companies" onNavigate={onNavigate}
      subtitle="Staff only" title="Customer records.">
      <div style={{ display: "flex", gap: 12, flexWrap: "wrap", marginBottom: 24, alignItems: "flex-end", justifyContent: "space-between" }}>
        <div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
          <MiniField label="Search by name">
            <input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="e.g. Test Company" style={inputStyle} data-testid="company-search-input" />
          </MiniField>
          <MiniField label="Status">
            <select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)} style={selectStyle}>
              <option value="">All statuses</option>
              <option value="pending">Pending</option>
              <option value="approved">Approved</option>
              <option value="rejected">Rejected</option>
              <option value="suspended">Suspended</option>
            </select>
          </MiniField>
        </div>
        <ActionButton readOnly={isReadOnly} onClick={() => setAddOpen(true)} testId="add-customer-open">
          + Add customer
        </ActionButton>
      </div>

      {addOpen && (
        <AddCustomerModal
          onCancel={() => setAddOpen(false)}
          onCreated={() => { setAddOpen(false); load(); }}
        />
      )}

      <SectionLabel>Customers ({visibleCompanies.length})</SectionLabel>
      {visibleCompanies.length === 0 ? (
        <EmptyNote>No customers match these filters.</EmptyNote>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          {visibleCompanies.map((c) => (
            <CompanyRecordRow
              key={c.id} company={c} products={state.products} companies={state.companies}
              open={openCompanyId === c.id} isReadOnly={isReadOnly}
              onToggle={() => setOpenCompanyId(openCompanyId === c.id ? null : c.id)}
              onSaved={load}
            />
          ))}
        </div>
      )}
    </AdminShell>
  );
}

// ---------------------------------------------------------------------------
// Add customer — lets staff create a company record directly (skipping the
// public signup + approve flow entirely, e.g. for a reseller already
// vetted offline). Created straight into 'approved' status, so it needs
// the same fields /approve normally requires: tier, region, country.
//
// Every company created here must now be linked to a Xero Contact from
// the start (no more free-text "type a name" creation) — the admin
// searches Xero (via the shared XeroContactPicker from
// admin-assets-page.jsx) and picks a contact; name/invoice/shipping
// address are pulled straight from that Contact server-side (see
// POST /api/admin/companies), not typed here. Adding this company's
// first login user is still done separately, via the existing public
// signup form + the normal user-approval step — this modal only creates
// the company record itself.
// ---------------------------------------------------------------------------

function AddCustomerModal({ onCancel, onCreated }) {
  const [xeroContact, setXeroContact] = useState(null); // { contactId, name }
  const [tier, setTier] = useState("gold");
  const [region, setRegion] = useState("UK");
  const [country, setCountry] = useState(window.GEO_DATA.countriesForRegion("UK")[0] || "");
  const [amName, setAmName] = useState("");
  const [amEmail, setAmEmail] = useState("");
  const [amPhone, setAmPhone] = useState("");
  const [status, setStatus] = useState("idle"); // idle | saving
  const [error, setError] = useState("");
  const [xeroStatus, setXeroStatus] = useState({ status: "loading", connected: false });

  const loadXeroStatus = () => {
    fetch("/api/admin/xero/status", { credentials: "same-origin" })
      .then((r) => r.json())
      .then((data) => setXeroStatus({ status: "ready", connected: !!data.connected }))
      .catch(() => setXeroStatus({ status: "error", connected: false }));
  };
  useEffect(loadXeroStatus, []); // eslint-disable-line react-hooks/exhaustive-deps -- mount-only

  const countryOptions = window.GEO_DATA.countriesForRegion(region);
  const handleRegionChange = (nextRegion) => {
    setRegion(nextRegion);
    setCountry(window.GEO_DATA.countriesForRegion(nextRegion)[0] || "");
  };

  const canSave = !!xeroContact && region && country && status !== "saving";

  const save = async () => {
    if (!canSave) return;
    setStatus("saving");
    setError("");
    try {
      const res = await fetch("/api/admin/companies", {
        method: "POST", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          xeroContactId: xeroContact.contactId,
          tier, region, country,
          accountManagerName: amName.trim() || null,
          accountManagerEmail: amEmail.trim() || null,
          accountManagerPhone: amPhone.trim() || null,
        }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setError(data.error || "Something went wrong. Please try again.");
        setStatus("idle");
        return;
      }
      onCreated();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
      setStatus("idle");
    }
  };

  return (
    <ModalShell title="Add customer." onCancel={onCancel} wide>
      {error && <ModalError>{error}</ModalError>}

      <MiniField label="Xero contact">
        {xeroStatus.status === "ready" && !xeroStatus.connected ? (
          <window.XeroNotConnectedWarning onRecheck={loadXeroStatus} />
        ) : xeroContact ? (
          <div style={{
            display: "flex", justifyContent: "space-between", alignItems: "center",
            background: "rgba(20,140,60,0.06)", border: "1px solid rgba(20,140,60,0.3)", padding: "10px 12px",
          }}>
            <span style={{ fontFamily: "var(--font-body)", fontSize: 13.5, color: "#000" }}>
              ✓ <strong>{xeroContact.name}</strong>
            </span>
            <button
              type="button" onClick={() => setXeroContact(null)} data-testid="add-customer-xero-change"
              style={{ background: "none", border: "none", padding: 0, color: "#000", textDecoration: "underline", cursor: "pointer", fontFamily: "inherit", fontSize: 12 }}
            >
              Change
            </button>
          </div>
        ) : (
          <window.XeroContactPicker onSelect={(contact) => setXeroContact({ contactId: contact.contactId, name: contact.name })} />
        )}
      </MiniField>

      <div style={{ display: "grid", gap: 14, gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", marginTop: 14, marginBottom: 4 }}>
        <MiniField label="Tier">
          <select value={tier} onChange={(e) => setTier(e.target.value)} style={selectStyle} data-testid="add-customer-tier">
            <option value="gold">Gold</option>
            <option value="platinum">Platinum</option>
            <option value="regional_reseller">Regional Reseller</option>
          </select>
        </MiniField>
        <MiniField label="Region">
          <select value={region} onChange={(e) => handleRegionChange(e.target.value)} style={selectStyle} data-testid="add-customer-region">
            {window.GEO_DATA.REGIONS.map((r) => <option key={r} value={r}>{r}</option>)}
          </select>
        </MiniField>
        <MiniField label="Country">
          <select value={country} onChange={(e) => setCountry(e.target.value)} style={selectStyle} data-testid="add-customer-country">
            {countryOptions.map((cName) => <option key={cName} value={cName}>{cName}</option>)}
          </select>
        </MiniField>
      </div>
      <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(0,0,0,0.5)", marginBottom: 14 }}>
        Company name and invoice/shipping addresses are pulled from the selected Xero contact.
      </div>
      <div style={{ display: "grid", gap: 14, gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))" }}>
        <MiniField label="Account manager name">
          <input value={amName} onChange={(e) => setAmName(e.target.value)} placeholder="e.g. Sam Manager" style={inputStyle} data-testid="add-customer-am-name" />
        </MiniField>
        <MiniField label="Account manager email">
          <input value={amEmail} onChange={(e) => setAmEmail(e.target.value)} placeholder="sam@solosecure.group" style={inputStyle} data-testid="add-customer-am-email" />
        </MiniField>
        <MiniField label="Account manager phone">
          <input value={amPhone} onChange={(e) => setAmPhone(e.target.value)} placeholder="+44 1234 567890" style={inputStyle} data-testid="add-customer-am-phone" />
        </MiniField>
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 14, marginTop: 16 }}>
        <ActionButton disabled={!canSave} onClick={save} testId="add-customer-save">
          {status === "saving" ? "Creating…" : "Create customer"}
        </ActionButton>
        <ActionButton onClick={onCancel} testId="add-customer-cancel">
          Cancel
        </ActionButton>
      </div>
    </ModalShell>
  );
}

function CompanyRecordRow({ company, products, companies, open, isReadOnly, onToggle, onSaved }) {
  const statusColor = company.status === "approved" ? "rgba(20,140,60,0.95)"
    : company.status === "rejected" ? "rgba(200,40,40,0.9)"
    : company.status === "suspended" ? "rgba(200,120,0,0.95)"
    : "rgba(0,0,0,0.5)";

  return (
    <div data-testid={`customer-row-${company.id}`} style={{
      background: "rgba(0,0,0,0.03)", border: "1px solid rgba(0,0,0,0.14)",
    }}>
      <button
        type="button" onClick={onToggle} data-testid={`customer-toggle-${company.id}`}
        style={{
          width: "100%", background: "none", border: "none", cursor: "pointer",
          padding: "18px 22px", display: "flex", justifyContent: "space-between",
          alignItems: "center", flexWrap: "wrap", gap: 12, textAlign: "left",
        }}>
        <div>
          <div style={{
            fontFamily: "var(--font-display)", fontWeight: 700, fontSize: 17,
            textTransform: "uppercase", color: "#000",
          }}>{company.name}</div>
          <div style={{ fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.55)", marginTop: 4 }}>
            <span style={{ color: statusColor, textTransform: "uppercase", letterSpacing: "0.08em", fontSize: 11 }}>{company.status}</span>
            {" · "}{TIER_LABELS[company.tier] || company.tier || "no tier"}
            {(company.region || company.country) && <> · {[company.country, company.region].filter(Boolean).join(", ")}</>}
            {" · "}{company.user_count} user{company.user_count === 1 ? "" : "s"}
          </div>
        </div>
        <span style={{
          fontFamily: "var(--font-body)", fontSize: 11, letterSpacing: "0.14em",
          textTransform: "uppercase", color: "rgba(0,0,0,0.55)",
        }}>{open ? "Close ↑" : "Amend →"}</span>
      </button>

      {open && (
        <div style={{ borderTop: "1px solid rgba(0,0,0,0.14)", padding: "22px" }}>
          {/* Keyed on the fields Xero-pull can overwrite so that a bind's
              refetch (via onSaved -> load()) remounts this form with the
              freshly-pulled name/addresses instead of leaving the
              already-initialized useState values stale on screen. */}
          <CompanyEditForm
            key={`${company.id}-${company.name}-${company.invoice_address || ""}-${company.shipping_address || ""}`}
            company={company} isReadOnly={isReadOnly} onSaved={onSaved}
          />
          <div style={{ height: 1, background: "rgba(0,0,0,0.1)", margin: "26px 0" }} />
          <CompanyUsers companyId={company.id} companyName={company.name} isReadOnly={isReadOnly} />
          <div style={{ height: 1, background: "rgba(0,0,0,0.1)", margin: "26px 0" }} />
          {company.region === "North America" ? (
            <CompanyQuickBooksLink companyId={company.id} companyName={company.name} isReadOnly={isReadOnly} onSaved={onSaved} />
          ) : (
            <CompanyXeroLink companyId={company.id} companyName={company.name} isReadOnly={isReadOnly} onSaved={onSaved} />
          )}
          <div style={{ height: 1, background: "rgba(0,0,0,0.1)", margin: "26px 0" }} />
          <CompanyAssetRegister companyId={company.id} companyName={company.name} products={products} companies={companies} isReadOnly={isReadOnly} />
        </div>
      )}
    </div>
  );
}

// ---------------------------------------------------------------------------
// Editable customer record: invoice/company address, region/country, tier,
// account-manager details. Saved via a single PATCH — server validates
// tier + region/country against the fixed taxonomies (lib/tiers.ts,
// lib/geo.ts) and rejects anything invalid before it's written.
// ---------------------------------------------------------------------------

function CompanyEditForm({ company, isReadOnly, onSaved }) {
  const [name, setName] = useState(company.name || "");
  const [invoiceAddress, setInvoiceAddress] = useState(company.invoice_address || "");
  const [shippingAddress, setShippingAddress] = useState(company.shipping_address || "");
  const [tier, setTier] = useState(company.tier && TIER_LABELS[company.tier] ? company.tier : "gold");
  const [region, setRegion] = useState(company.region || "UK");
  const [country, setCountry] = useState(company.country || window.GEO_DATA.countriesForRegion(company.region || "UK")[0] || "");
  const [amName, setAmName] = useState(company.account_manager_name || "");
  const [amEmail, setAmEmail] = useState(company.account_manager_email || "");
  const [amPhone, setAmPhone] = useState(company.account_manager_phone || "");
  const [status, setStatus] = useState("idle"); // idle | saving | saved
  const [error, setError] = useState("");

  const countryOptions = window.GEO_DATA.countriesForRegion(region);
  const handleRegionChange = (nextRegion) => {
    setRegion(nextRegion);
    setCountry(window.GEO_DATA.countriesForRegion(nextRegion)[0] || "");
  };

  const canSave = name.trim() && region && country && status !== "saving";

  const save = async () => {
    if (!canSave) return;
    setStatus("saving");
    setError("");
    try {
      const res = await fetch(`/api/admin/companies/${company.id}`, {
        method: "PATCH", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          name: name.trim(),
          invoiceAddress: invoiceAddress.trim() || null,
          shippingAddress: shippingAddress.trim() || null,
          tier, region, country,
          accountManagerName: amName.trim() || null,
          accountManagerEmail: amEmail.trim() || null,
          accountManagerPhone: amPhone.trim() || null,
        }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setError(data.error || "Something went wrong. Please try again.");
        setStatus("idle");
        return;
      }
      setStatus("saved");
      onSaved();
      setTimeout(() => setStatus("idle"), 2000);
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
      setStatus("idle");
    }
  };

  return (
    <div>
      <SubSectionLabel>Customer record</SubSectionLabel>
      {error && <ModalError>{error}</ModalError>}
      <div style={{ display: "grid", gap: 14, gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", marginBottom: 4 }}>
        <MiniField label="Company name">
          <input value={name} onChange={(e) => setName(e.target.value)} style={inputStyle} data-testid={`edit-name-${company.id}`} />
        </MiniField>
        <MiniField label="Tier">
          <select value={tier} onChange={(e) => setTier(e.target.value)} style={selectStyle} data-testid={`edit-tier-${company.id}`}>
            <option value="gold">Gold</option>
            <option value="platinum">Platinum</option>
            <option value="regional_reseller">Regional Reseller</option>
          </select>
        </MiniField>
        <MiniField label="Region">
          <select value={region} onChange={(e) => handleRegionChange(e.target.value)} style={selectStyle} data-testid={`edit-region-${company.id}`}>
            {window.GEO_DATA.REGIONS.map((r) => <option key={r} value={r}>{r}</option>)}
          </select>
        </MiniField>
        <MiniField label="Country">
          <select value={country} onChange={(e) => setCountry(e.target.value)} style={selectStyle} data-testid={`edit-country-${company.id}`}>
            {countryOptions.map((cName) => <option key={cName} value={cName}>{cName}</option>)}
          </select>
        </MiniField>
      </div>
      <div style={{ display: "grid", gap: 14, gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))" }}>
        <AddressLookupField
          label="Invoice address"
          value={invoiceAddress} onChange={setInvoiceAddress}
          placeholder="e.g. 1 Test Street, London, EC1A 1AA"
          testId={`edit-invoice-address-${company.id}`}
        />
        <AddressLookupField
          label="Shipping address"
          value={shippingAddress} onChange={setShippingAddress}
          placeholder="Where physical units/deliveries are sent — search or type"
          testId={`edit-shipping-address-${company.id}`}
        />
      </div>
      <div style={{ display: "grid", gap: 14, gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))" }}>
        <MiniField label="Account manager name">
          <input value={amName} onChange={(e) => setAmName(e.target.value)} placeholder="e.g. Sam Manager" style={inputStyle} />
        </MiniField>
        <MiniField label="Account manager email">
          <input value={amEmail} onChange={(e) => setAmEmail(e.target.value)} placeholder="sam@solosecure.group" style={inputStyle} />
        </MiniField>
        <MiniField label="Account manager phone">
          <input value={amPhone} onChange={(e) => setAmPhone(e.target.value)} placeholder="+44 1234 567890" style={inputStyle} />
        </MiniField>
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 14, marginTop: 16, flexWrap: "wrap" }}>
        <ActionButton disabled={!canSave} readOnly={isReadOnly} onClick={save} testId={`save-company-${company.id}`}>
          {status === "saving" ? "Saving…" : "Save changes"}
        </ActionButton>
        {status === "saved" && (
          <span style={{
            fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(20,140,60,0.95)",
            textTransform: "uppercase", letterSpacing: "0.1em",
          }}>Saved ✓</span>
        )}
        <div style={{ flex: 1 }} />
        <CompanyBlockDeleteControls company={company} isReadOnly={isReadOnly} onSaved={onSaved} />
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
// Block / Reactivate / Delete controls for a company. "Block" suspends the
// company (and every one of its users' sessions immediately) without
// touching any order/invoice/asset history — see POST
// /companies/:id/suspend in routes/admin.ts. "Delete" is a hard row
// removal that the server refuses (409, with a clear reason) if any
// history is attached, so it only ever succeeds for a genuine mistake
// (e.g. a duplicate/test company with nothing on it yet).
// ---------------------------------------------------------------------------
function CompanyBlockDeleteControls({ company, isReadOnly, onSaved }) {
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");

  const call = async (path, method = "POST") => {
    setBusy(true);
    setError("");
    try {
      const res = await fetch(`/api/admin/companies/${company.id}${path}`, { method, credentials: "same-origin" });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setError(data.error || "Something went wrong. Please try again.");
        setBusy(false);
        return;
      }
      onSaved();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  const handleDelete = () => {
    if (!window.confirm(`Permanently delete "${company.name}"? This can't be undone, and only succeeds if this customer has no orders, invoices, tickets or assets on record.`)) return;
    call("", "DELETE");
  };

  return (
    <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
      {error && (
        <span style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(200,40,40,0.9)", maxWidth: 320 }}>{error}</span>
      )}
      {company.status === "suspended" ? (
        <ActionButton disabled={busy} readOnly={isReadOnly} onClick={() => call("/reactivate")} testId={`reactivate-company-${company.id}`}>
          {busy ? "Working…" : "Reactivate"}
        </ActionButton>
      ) : (
        <ActionButton disabled={busy} readOnly={isReadOnly} onClick={() => call("/suspend")} testId={`block-company-${company.id}`} danger>
          {busy ? "Working…" : "Block access"}
        </ActionButton>
      )}
      <ActionButton disabled={busy} readOnly={isReadOnly} onClick={handleDelete} testId={`delete-company-${company.id}`} danger>
        Delete
      </ActionButton>
    </div>
  );
}

// ---------------------------------------------------------------------------
// Xero contact link — admin-only search + explicit accept/bind. Nothing
// here ever auto-creates a Xero Contact or binds anything automatically:
// the admin types a query, reviews the returned candidates, and clicks
// "Bind" on the one they want. Orders placed before a company is linked
// simply sit unsynced (see admin-orders-page.jsx / orders API) until an
// admin links it here. See routes/admin-xero.ts for the endpoints.
// ---------------------------------------------------------------------------

function CompanyXeroLink({ companyId, companyName, isReadOnly, onSaved }) {
  const [status, setStatus] = useState("loading"); // loading | ready | error
  const [link, setLink] = useState(null); // { linked, xeroContactId, updatedAt }
  const [query, setQuery] = useState(companyName || "");
  const [searchState, setSearchState] = useState("idle"); // idle | searching | error
  const [results, setResults] = useState(null); // null = no search run yet
  const [searchError, setSearchError] = useState("");
  const [bindingId, setBindingId] = useState(null);
  const [bindError, setBindError] = useState("");
  const [unbinding, setUnbinding] = useState(false);
  // Result of the most recent bind's Xero detail pull -- shown as a
  // one-off confirmation/warning under the "Linked" line. Cleared on
  // unlink/re-search so it never shows stale info about a different bind.
  const [syncNote, setSyncNote] = useState(null); // { ok, message }

  const loadLink = () => {
    setStatus((s) => (s === "ready" ? "ready" : "loading"));
    return fetch(`/api/admin/companies/${companyId}/xero-link`, { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => { setLink(data); setStatus("ready"); })
      .catch(() => setStatus("error"));
  };

  useEffect(() => { loadLink(); }, [companyId]); // eslint-disable-line react-hooks/exhaustive-deps

  const runSearch = async () => {
    if (!query.trim()) return;
    setSearchState("searching");
    setSearchError("");
    setBindError("");
    try {
      const res = await fetch(`/api/admin/xero/contacts/search?q=${encodeURIComponent(query.trim())}`, { credentials: "same-origin" });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setSearchError(data.error || "Search failed. Please try again.");
        setResults(null);
        setSearchState("error");
        return;
      }
      setResults(data.contacts || []);
      setSearchState("idle");
    } catch {
      setSearchError("Couldn't reach the server. Check your connection and try again.");
      setResults(null);
      setSearchState("error");
    }
  };

  const bind = async (contact) => {
    setBindingId(contact.contactId);
    setBindError("");
    setSyncNote(null);
    try {
      const res = await fetch(`/api/admin/companies/${companyId}/xero-link`, {
        method: "POST", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ xeroContactId: contact.contactId }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setBindError(data.error || "Couldn't bind that contact. Please try again.");
        setBindingId(null);
        return;
      }
      setResults(null);
      // Bind also pulls the Contact's name + invoice/shipping addresses
      // from Xero onto the company record (see routes/admin-xero.ts +
      // lib/xero.ts bindCompanyToContact) — surface what happened, then
      // refresh both this card and the customer-record form above so the
      // newly-pulled fields show immediately without a manual page reload.
      if (data.synced && data.pulled) {
        const parts = [`Name "${data.pulled.name}"`];
        if (data.pulled.invoiceAddress) parts.push("invoice address");
        if (data.pulled.shippingAddress) parts.push("shipping address");
        setSyncNote({ ok: true, message: `Pulled from Xero: ${parts.join(", ")}.` });
      } else if (data.syncError) {
        setSyncNote({ ok: false, message: `Linked, but couldn't pull company details from Xero: ${data.syncError}` });
      }
      loadLink();
      if (onSaved) onSaved();
    } catch {
      setBindError("Couldn't reach the server. Check your connection and try again.");
    }
    setBindingId(null);
  };

  const unbind = async () => {
    if (!window.confirm(`Unlink ${companyName} from Xero? Future orders won't push a quote until this company is linked again.`)) return;
    setUnbinding(true);
    setSyncNote(null);
    try {
      await fetch(`/api/admin/companies/${companyId}/xero-link`, { method: "DELETE", credentials: "same-origin" });
      loadLink();
    } catch {
      // best-effort — loadLink() below still runs so the UI stays honest even on a network blip
    }
    setUnbinding(false);
  };

  return (
    <div>
      <SubSectionLabel>Xero link</SubSectionLabel>

      {status === "loading" && <EmptyNote>Loading…</EmptyNote>}
      {status === "error" && <EmptyNote>Couldn't load this company's Xero link status.</EmptyNote>}

      {status === "ready" && link?.linked && (
        <div style={{ display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap", marginBottom: 18 }}>
          <span style={{
            fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(20,140,60,0.95)",
            textTransform: "uppercase", letterSpacing: "0.08em",
          }}>Linked ✓</span>
          <span style={{ fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.55)" }}>
            Xero contact ID: {link.xeroContactId}
          </span>
          <ActionButton disabled={unbinding} readOnly={isReadOnly} onClick={unbind} danger testId={`unlink-xero-${companyId}`}>
            {unbinding ? "Working…" : "Unlink"}
          </ActionButton>
        </div>
      )}

      {syncNote && (
        <div
          data-testid={`xero-sync-note-${companyId}`}
          style={{
            fontFamily: "var(--font-body)", fontSize: 12.5, marginBottom: 14, padding: "8px 12px",
            background: syncNote.ok ? "rgba(20,140,60,0.08)" : "rgba(200,120,0,0.1)",
            border: `1px solid ${syncNote.ok ? "rgba(20,140,60,0.3)" : "rgba(200,120,0,0.35)"}`,
            color: syncNote.ok ? "rgba(20,100,50,0.95)" : "rgba(150,90,0,0.95)",
          }}
        >
          {syncNote.message}
        </div>
      )}

      {status === "ready" && !link?.linked && (
        <EmptyNote>Not linked yet — orders for {companyName} won't push a quote to Xero until an admin links it below.</EmptyNote>
      )}

      <div style={{ display: "flex", gap: 10, flexWrap: "wrap", alignItems: "flex-end", marginBottom: 14 }}>
        <MiniField label="Search Xero contacts by name">
          <input
            value={query} onChange={(e) => setQuery(e.target.value)}
            onKeyDown={(e) => { if (e.key === "Enter") runSearch(); }}
            placeholder="e.g. Acme Reseller Ltd" style={inputStyle}
            data-testid={`xero-search-input-${companyId}`}
          />
        </MiniField>
        <ActionButton disabled={searchState === "searching" || !query.trim()} readOnly={isReadOnly} onClick={runSearch} testId={`xero-search-button-${companyId}`}>
          {searchState === "searching" ? "Searching…" : "Search Xero"}
        </ActionButton>
      </div>

      {searchError && <ModalError>{searchError}</ModalError>}
      {bindError && <ModalError>{bindError}</ModalError>}

      {results !== null && (
        results.length === 0 ? (
          <EmptyNote>No Xero contacts matched "{query.trim()}".</EmptyNote>
        ) : (
          <div style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 4 }}>
            {results.map((contact) => (
              <div key={contact.contactId} style={{
                display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap",
                background: "rgba(0,0,0,0.03)", border: "1px solid rgba(0,0,0,0.14)", padding: "10px 14px",
              }}>
                <div>
                  <div style={{ fontFamily: "var(--font-body)", fontWeight: 500, fontSize: 13.5, color: "#000" }}>{contact.name}</div>
                  {contact.emailAddress && (
                    <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(0,0,0,0.5)" }}>{contact.emailAddress}</div>
                  )}
                </div>
                <ActionButton
                  disabled={bindingId === contact.contactId}
                  readOnly={isReadOnly}
                  onClick={() => bind(contact)}
                  testId={`xero-bind-${companyId}-${contact.contactId}`}
                >
                  {bindingId === contact.contactId ? "Binding…" : (link?.xeroContactId === contact.contactId ? "Re-bind" : "Bind")}
                </ActionButton>
              </div>
            ))}
          </div>
        )
      )}
    </div>
  );
}

// ---------------------------------------------------------------------------
// QuickBooks Online customer link — the North America equivalent of
// CompanyXeroLink above, only ever rendered for companies with
// region === "North America" (see CompanyRecordRow). Deliberately does
// NOT pull/overwrite this company's name or address the way Xero's bind
// does (see routes/admin-quickbooks.ts's POST /companies/:id/quickbooks-link
// + lib/quickbooks.ts's bindCompanyToCustomer) — the customer only asked
// for a read-only pull of customer details + invoices for display, not
// for QuickBooks to become a source that overwrites local company data.
// Once linked, this also shows a read-only "Customer details on file in
// QuickBooks" snapshot (address/phone/email) fetched fresh on each load,
// purely for display — again, never written back onto the company
// record.
// ---------------------------------------------------------------------------

function CompanyQuickBooksLink({ companyId, companyName, isReadOnly, onSaved }) {
  const [status, setStatus] = useState("loading"); // loading | ready | error
  const [link, setLink] = useState(null); // { linked, quickbooksCustomerId, updatedAt }
  const [detail, setDetail] = useState(null); // { name, email, phone, billingAddress } | null
  const [detailError, setDetailError] = useState("");
  const [query, setQuery] = useState(companyName || "");
  const [searchState, setSearchState] = useState("idle"); // idle | searching | error
  const [results, setResults] = useState(null); // null = no search run yet
  const [searchError, setSearchError] = useState("");
  const [bindingId, setBindingId] = useState(null);
  const [bindError, setBindError] = useState("");
  const [unbinding, setUnbinding] = useState(false);

  const loadDetail = (quickbooksCustomerId) => {
    setDetail(null);
    setDetailError("");
    fetch(`/api/admin/quickbooks/customers/${encodeURIComponent(quickbooksCustomerId)}`, { credentials: "same-origin" })
      .then((r) => r.json().then((data) => ({ ok: r.ok, data })))
      .then(({ ok, data }) => {
        if (!ok) { setDetailError(data.error || "Couldn't load this customer's QuickBooks details."); return; }
        setDetail(data.customer);
      })
      .catch(() => setDetailError("Couldn't reach the server. Check your connection and try again."));
  };

  const loadLink = () => {
    setStatus((s) => (s === "ready" ? "ready" : "loading"));
    return fetch(`/api/admin/companies/${companyId}/quickbooks-link`, { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => {
        setLink(data);
        setStatus("ready");
        if (data.linked && data.quickbooksCustomerId) loadDetail(data.quickbooksCustomerId);
      })
      .catch(() => setStatus("error"));
  };

  useEffect(() => { loadLink(); }, [companyId]); // eslint-disable-line react-hooks/exhaustive-deps

  const runSearch = async () => {
    if (!query.trim()) return;
    setSearchState("searching");
    setSearchError("");
    setBindError("");
    try {
      const res = await fetch(`/api/admin/quickbooks/customers/search?q=${encodeURIComponent(query.trim())}`, { credentials: "same-origin" });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setSearchError(data.error || "Search failed. Please try again.");
        setResults(null);
        setSearchState("error");
        return;
      }
      setResults(data.customers || []);
      setSearchState("idle");
    } catch {
      setSearchError("Couldn't reach the server. Check your connection and try again.");
      setResults(null);
      setSearchState("error");
    }
  };

  const bind = async (customer) => {
    setBindingId(customer.customerId);
    setBindError("");
    try {
      const res = await fetch(`/api/admin/companies/${companyId}/quickbooks-link`, {
        method: "POST", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ quickbooksCustomerId: customer.customerId }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setBindError(data.error || "Couldn't bind that customer. Please try again.");
        setBindingId(null);
        return;
      }
      setResults(null);
      loadLink();
      if (onSaved) onSaved();
    } catch {
      setBindError("Couldn't reach the server. Check your connection and try again.");
    }
    setBindingId(null);
  };

  const unbind = async () => {
    if (!window.confirm(`Unlink ${companyName} from QuickBooks? Its invoice data won't load until this company is linked again.`)) return;
    setUnbinding(true);
    try {
      await fetch(`/api/admin/companies/${companyId}/quickbooks-link`, { method: "DELETE", credentials: "same-origin" });
      setDetail(null);
      loadLink();
    } catch {
      // best-effort — loadLink() below still runs so the UI stays honest even on a network blip
    }
    setUnbinding(false);
  };

  return (
    <div>
      <SubSectionLabel>QuickBooks link</SubSectionLabel>

      {status === "loading" && <EmptyNote>Loading…</EmptyNote>}
      {status === "error" && <EmptyNote>Couldn't load this company's QuickBooks link status.</EmptyNote>}

      {status === "ready" && link?.linked && (
        <div style={{ display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap", marginBottom: 14 }}>
          <span style={{
            fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(20,140,60,0.95)",
            textTransform: "uppercase", letterSpacing: "0.08em",
          }}>Linked ✓</span>
          <span style={{ fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.55)" }}>
            QuickBooks customer ID: {link.quickbooksCustomerId}
          </span>
          <ActionButton disabled={unbinding} readOnly={isReadOnly} onClick={unbind} danger testId={`unlink-quickbooks-${companyId}`}>
            {unbinding ? "Working…" : "Unlink"}
          </ActionButton>
        </div>
      )}

      {status === "ready" && link?.linked && (
        detailError ? (
          <div style={{ fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.5)", marginBottom: 14 }}>
            Couldn't load customer details from QuickBooks: {detailError}
          </div>
        ) : detail ? (
          <div style={{
            fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.65)",
            background: "rgba(0,0,0,0.03)", border: "1px solid rgba(0,0,0,0.1)",
            padding: "10px 14px", marginBottom: 14, lineHeight: 1.6,
          }}>
            <strong>On file in QuickBooks:</strong> {detail.name}
            {detail.email && <> · {detail.email}</>}
            {detail.phone && <> · {detail.phone}</>}
            {detail.billingAddress && <><br />{detail.billingAddress}</>}
          </div>
        ) : (
          <EmptyNote>Loading customer details from QuickBooks…</EmptyNote>
        )
      )}

      {status === "ready" && !link?.linked && (
        <EmptyNote>Not linked yet — {companyName}'s invoice data won't load from QuickBooks until an admin links it below.</EmptyNote>
      )}

      <div style={{ display: "flex", gap: 10, flexWrap: "wrap", alignItems: "flex-end", marginBottom: 14 }}>
        <MiniField label="Search QuickBooks customers by name">
          <input
            value={query} onChange={(e) => setQuery(e.target.value)}
            onKeyDown={(e) => { if (e.key === "Enter") runSearch(); }}
            placeholder="e.g. Acme Reseller Inc" style={inputStyle}
            data-testid={`quickbooks-search-input-${companyId}`}
          />
        </MiniField>
        <ActionButton disabled={searchState === "searching" || !query.trim()} readOnly={isReadOnly} onClick={runSearch} testId={`quickbooks-search-button-${companyId}`}>
          {searchState === "searching" ? "Searching…" : "Search QuickBooks"}
        </ActionButton>
      </div>

      {searchError && <ModalError>{searchError}</ModalError>}
      {bindError && <ModalError>{bindError}</ModalError>}

      {results !== null && (
        results.length === 0 ? (
          <EmptyNote>No QuickBooks customers matched "{query.trim()}".</EmptyNote>
        ) : (
          <div style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 4 }}>
            {results.map((customer) => (
              <div key={customer.customerId} style={{
                display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap",
                background: "rgba(0,0,0,0.03)", border: "1px solid rgba(0,0,0,0.14)", padding: "10px 14px",
              }}>
                <div>
                  <div style={{ fontFamily: "var(--font-body)", fontWeight: 500, fontSize: 13.5, color: "#000" }}>{customer.name}</div>
                  {customer.email && (
                    <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(0,0,0,0.5)" }}>{customer.email}</div>
                  )}
                </div>
                <ActionButton
                  disabled={bindingId === customer.customerId}
                  readOnly={isReadOnly}
                  onClick={() => bind(customer)}
                  testId={`quickbooks-bind-${companyId}-${customer.customerId}`}
                >
                  {bindingId === customer.customerId ? "Binding…" : (link?.quickbooksCustomerId === customer.customerId ? "Re-bind" : "Bind")}
                </ActionButton>
              </div>
            ))}
          </div>
        )
      )}
    </div>
  );
}

// ---------------------------------------------------------------------------
// Real-world address lookup — as an admin types, this searches
// GET /api/admin/address-lookup (OpenStreetMap Nominatim, server-side, no
// API key required — see routes/admin-geo-lookup.ts) and shows matching
// real addresses to pick from, so the stored address is the exact,
// correctly-formatted real one rather than whatever was free-typed.
// Picking a suggestion fills the field and closes the list; admins can
// still just type/edit freely if the address they want isn't found.
// ---------------------------------------------------------------------------

function AddressLookupField({ label, value, onChange, placeholder, testId }) {
  const [suggestions, setSuggestions] = useState([]);
  const [loading, setLoading] = useState(false);
  const [open, setOpen] = useState(false);
  const [error, setError] = useState("");
  const debounceRef = useRef(null);
  const blurTimerRef = useRef(null);

  const search = (q) => {
    if (!q || q.trim().length < 3) {
      setSuggestions([]);
      setLoading(false);
      return;
    }
    setLoading(true);
    setError("");
    fetch(`/api/admin/address-lookup?q=${encodeURIComponent(q.trim())}`, { credentials: "same-origin" })
      .then((r) => r.json().then((data) => ({ ok: r.ok, data })))
      .then(({ ok, data }) => {
        if (!ok) { setError(data.error || "Lookup failed."); setSuggestions([]); return; }
        setSuggestions(data.suggestions || []);
      })
      .catch(() => setError("Couldn't reach the address lookup service."))
      .finally(() => setLoading(false));
  };

  const handleChange = (e) => {
    const next = e.target.value;
    onChange(next);
    setOpen(true);
    if (debounceRef.current) clearTimeout(debounceRef.current);
    debounceRef.current = setTimeout(() => search(next), 400);
  };

  const pick = (formatted) => {
    onChange(formatted);
    setSuggestions([]);
    setOpen(false);
  };

  return (
    <MiniField label={label}>
      <div style={{ position: "relative" }}>
        <textarea
          value={value}
          onChange={handleChange}
          onFocus={() => setOpen(true)}
          onBlur={() => { blurTimerRef.current = setTimeout(() => setOpen(false), 150); }}
          rows={2}
          placeholder={placeholder}
          style={{ ...inputStyle, resize: "vertical", fontFamily: "var(--font-body)", width: "100%" }}
          data-testid={testId}
        />
        {open && (loading || suggestions.length > 0 || error) && (
          <div
            data-testid={`${testId}-suggestions`}
            style={{
              position: "absolute", top: "100%", left: 0, right: 0, zIndex: 20,
              background: "#fff", border: "1px solid rgba(0,0,0,0.2)", boxShadow: "0 12px 40px rgba(0,0,0,0.25)",
              maxHeight: 220, overflowY: "auto", marginTop: 2,
            }}
          >
            {loading && (
              <div style={{ padding: "8px 12px", fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.5)" }}>
                Searching…
              </div>
            )}
            {!loading && error && (
              <div style={{ padding: "8px 12px", fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(190,40,40,0.9)" }}>
                {error}
              </div>
            )}
            {!loading && !error && suggestions.map((s, idx) => (
              <button
                key={idx} type="button"
                onMouseDown={(e) => { e.preventDefault(); pick(s.formatted); }}
                data-testid={`${testId}-suggestion-${idx}`}
                style={{
                  display: "block", width: "100%", textAlign: "left", background: "none", border: "none",
                  borderBottom: idx < suggestions.length - 1 ? "1px solid rgba(0,0,0,0.1)" : "none",
                  padding: "9px 12px", cursor: "pointer",
                  fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.8)",
                }}
              >
                {s.formatted}
              </button>
            ))}
          </div>
        )}
      </div>
    </MiniField>
  );
}

// ---------------------------------------------------------------------------
// Reseller users — the login credentials (email, name, role, status, and
// optionally password) that staff at this company actually sign into the
// portal with. Previously the Customer records page only showed a bare
// `user_count` number; this fetches the real rows (GET /companies/:id
// already returns them) and lets admin staff amend each one directly,
// via the new PATCH /users/:id endpoint (routes/admin.ts).
// ---------------------------------------------------------------------------

function CompanyUsers({ companyId, companyName, isReadOnly }) {
  const [status, setStatusState] = useState("loading"); // loading | ready | error
  const [users, setUsers] = useState([]);

  const load = () => {
    setStatusState((s) => (s === "ready" ? "ready" : "loading"));
    return fetch(`/api/admin/companies/${companyId}`, { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => { setUsers(data.users || []); setStatusState("ready"); })
      .catch(() => setStatusState("error"));
  };

  useEffect(() => { load(); }, [companyId]); // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <div>
      <SubSectionLabel>Reseller users / login credentials ({users.length})</SubSectionLabel>
      {status === "loading" && <EmptyNote>Loading…</EmptyNote>}
      {status === "error" && <EmptyNote>Couldn't load {companyName}'s users.</EmptyNote>}
      {status === "ready" && users.length === 0 && (
        <EmptyNote>No users have signed up under {companyName} yet.</EmptyNote>
      )}
      {status === "ready" && users.length > 0 && (
        <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
          {users.map((u) => (
            <UserEditRow key={u.id} user={u} isReadOnly={isReadOnly} onSaved={load} />
          ))}
        </div>
      )}
    </div>
  );
}

const USER_ROLE_LABELS = { company_admin: "Company admin", member: "Member" };
const USER_STATUS_COLORS = {
  active: "rgba(20,140,60,0.95)",
  pending: "rgba(0,0,0,0.55)",
  rejected: "rgba(190,40,40,0.9)",
  suspended: "rgba(180,110,0,0.95)",
};

// ---------------------------------------------------------------------------
// Block / Reactivate / Delete for a single reseller user login. "Block" is
// a one-click equivalent of setting status to "suspended" via the Amend
// form's dropdown, but exposed directly since that's the common case.
// "Delete" hard-removes the login row, refused (409) if the user has any
// asset-assignment or ticket history attached.
// ---------------------------------------------------------------------------
function UserBlockDeleteControls({ user, isReadOnly, onSaved }) {
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");

  const call = async (path, method = "POST") => {
    setBusy(true);
    setError("");
    try {
      const res = await fetch(`/api/admin/users/${user.id}${path}`, { method, credentials: "same-origin" });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setError(data.error || "Something went wrong. Please try again.");
        setBusy(false);
        return;
      }
      onSaved();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  const handleDelete = () => {
    if (!window.confirm(`Permanently delete the login for "${user.name}"? This can't be undone, and only succeeds if they have no asset or ticket history on record.`)) return;
    call("", "DELETE");
  };

  return (
    <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
      {error && (
        <span style={{ fontFamily: "var(--font-body)", fontSize: 11, color: "rgba(200,40,40,0.9)", maxWidth: 260 }}>{error}</span>
      )}
      {user.status === "suspended" ? (
        <ActionButton disabled={busy} readOnly={isReadOnly} onClick={() => call("/reactivate")} testId={`reactivate-user-${user.id}`}>
          {busy ? "Working…" : "Reactivate"}
        </ActionButton>
      ) : (
        <ActionButton disabled={busy} readOnly={isReadOnly} onClick={() => call("/block")} testId={`block-user-${user.id}`} danger>
          {busy ? "Working…" : "Block"}
        </ActionButton>
      )}
      <ActionButton disabled={busy} readOnly={isReadOnly} onClick={handleDelete} testId={`delete-user-${user.id}`} danger>
        Delete
      </ActionButton>
    </div>
  );
}

function UserEditRow({ user, isReadOnly, onSaved }) {
  const [editing, setEditing] = useState(false);
  const [name, setName] = useState(user.name || "");
  const [email, setEmail] = useState(user.email || "");
  const [role, setRole] = useState(user.role || "member");
  const [userStatus, setUserStatus] = useState(user.status || "pending");
  const [password, setPassword] = useState("");
  const [saveStatus, setSaveStatus] = useState("idle"); // idle | saving | saved
  const [error, setError] = useState("");

  const startEdit = () => {
    setName(user.name || "");
    setEmail(user.email || "");
    setRole(user.role || "member");
    setUserStatus(user.status || "pending");
    setPassword("");
    setError("");
    setEditing(true);
  };

  const canSave = name.trim() && email.trim() && (!password || password.length >= 8) && saveStatus !== "saving";

  const save = async () => {
    if (!canSave) return;
    setSaveStatus("saving");
    setError("");
    try {
      const body = { name: name.trim(), email: email.trim(), role, status: userStatus };
      if (password) body.password = password;
      const res = await fetch(`/api/admin/users/${user.id}`, {
        method: "PATCH", 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. Please try again.");
        setSaveStatus("idle");
        return;
      }
      setSaveStatus("saved");
      setEditing(false);
      setPassword("");
      onSaved();
      setTimeout(() => setSaveStatus("idle"), 2000);
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
      setSaveStatus("idle");
    }
  };

  if (!editing) {
    return (
      <div data-testid={`user-row-${user.id}`} style={{
        display: "flex", justifyContent: "space-between", alignItems: "center",
        flexWrap: "wrap", gap: 10, padding: "12px 14px",
        background: "rgba(0,0,0,0.02)", border: "1px solid rgba(0,0,0,0.12)",
      }}>
        <div>
          <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, color: "#000" }}>
            {user.name} <span style={{ color: "rgba(0,0,0,0.55)" }}>· {user.email}</span>
          </div>
          <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(0,0,0,0.55)", marginTop: 3 }}>
            <span style={{ color: USER_STATUS_COLORS[user.status] || "rgba(0,0,0,0.55)", textTransform: "uppercase", letterSpacing: "0.06em", fontSize: 10.5 }}>
              {user.status}
            </span>
            {" · "}{USER_ROLE_LABELS[user.role] || user.role}
          </div>
        </div>
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
          <ActionButton readOnly={isReadOnly} onClick={startEdit} testId={`edit-user-${user.id}`}>Amend</ActionButton>
          <UserBlockDeleteControls user={user} isReadOnly={isReadOnly} onSaved={onSaved} />
        </div>
      </div>
    );
  }

  return (
    <div data-testid={`user-edit-${user.id}`} style={{
      padding: "16px", background: "rgba(0,0,0,0.04)", border: "1px solid rgba(0,0,0,0.2)",
    }}>
      {error && <ModalError>{error}</ModalError>}
      <div style={{ display: "grid", gap: 12, gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", marginBottom: 12 }}>
        <MiniField label="Name">
          <input value={name} onChange={(e) => setName(e.target.value)} style={inputStyle} data-testid={`user-name-${user.id}`} />
        </MiniField>
        <MiniField label="Login email">
          <input value={email} onChange={(e) => setEmail(e.target.value)} style={inputStyle} data-testid={`user-email-${user.id}`} />
        </MiniField>
        <MiniField label="Role">
          <select value={role} onChange={(e) => setRole(e.target.value)} style={selectStyle} data-testid={`user-role-${user.id}`}>
            <option value="company_admin">Company admin</option>
            <option value="member">Member</option>
          </select>
        </MiniField>
        <MiniField label="Status">
          <select value={userStatus} onChange={(e) => setUserStatus(e.target.value)} style={selectStyle} data-testid={`user-status-${user.id}`}>
            <option value="pending">Pending</option>
            <option value="active">Active</option>
            <option value="rejected">Rejected</option>
            <option value="suspended">Suspended</option>
          </select>
        </MiniField>
      </div>
      <MiniField label="Reset password (optional — leave blank to keep current password)">
        <input
          type="password" value={password} onChange={(e) => setPassword(e.target.value)}
          placeholder="Min. 8 characters" style={inputStyle} data-testid={`user-password-${user.id}`}
        />
      </MiniField>
      <div style={{ display: "flex", alignItems: "center", gap: 12, marginTop: 14 }}>
        <ActionButton disabled={!canSave} readOnly={isReadOnly} onClick={save} testId={`save-user-${user.id}`}>
          {saveStatus === "saving" ? "Saving…" : "Save changes"}
        </ActionButton>
        <ActionButton onClick={() => { setEditing(false); setError(""); }}>Cancel</ActionButton>
        {saveStatus === "saved" && (
          <span style={{
            fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(20,140,60,0.95)",
            textTransform: "uppercase", letterSpacing: "0.1em",
          }}>Saved ✓</span>
        )}
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
// Per-customer asset register — same units/add/assign/unassign/history
// behaviour as the main Assets page, filtered down to this one company,
// with the target company locked (no accidental cross-company move).
// ---------------------------------------------------------------------------

function CompanyAssetRegister({ companyId, companyName, products, companies, isReadOnly }) {
  const [units, setUnits] = useState([]);
  const [status, setStatusState] = useState("loading");
  const [busyId, setBusyId] = useState(null);
  const [showAddUnit, setShowAddUnit] = useState(false);
  const [assigningUnit, setAssigningUnit] = useState(null);
  const [historyUnit, setHistoryUnit] = useState(null);

  const loadUnits = () => {
    return fetch(`/api/admin/units?companyId=${companyId}`, { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => { setUnits(data.units || []); setStatusState("ready"); })
      .catch(() => setStatusState("error"));
  };

  useEffect(() => { loadUnits(); }, [companyId]); // eslint-disable-line react-hooks/exhaustive-deps

  const handleAddUnit = async (form) => {
    setBusyId("add-unit");
    try {
      const res = await fetch("/api/admin/units", {
        method: "POST", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(form),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) return data.error || "Something went wrong.";
      setShowAddUnit(false);
      await loadUnits();
      return null;
    } finally { setBusyId(null); }
  };

  const handleAssign = async (unitId, form) => {
    setBusyId(`assign-${unitId}`);
    try {
      const res = await fetch(`/api/admin/units/${unitId}/assign`, {
        method: "POST", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(form),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) return data.error || "Something went wrong.";
      setAssigningUnit(null);
      await loadUnits();
      return null;
    } finally { setBusyId(null); }
  };

  const handleUnassign = async (unitId) => {
    setBusyId(`unassign-${unitId}`);
    try { await fetch(`/api/admin/units/${unitId}/unassign`, { method: "POST", credentials: "same-origin" }); await loadUnits(); }
    finally { setBusyId(null); }
  };

  const openHistory = async (unitId) => {
    const res = await fetch(`/api/admin/units/${unitId}`, { credentials: "same-origin" });
    const data = await res.json().catch(() => null);
    if (data) setHistoryUnit(data);
  };

  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 14, flexWrap: "wrap", gap: 12 }}>
        <SubSectionLabel>Asset register ({units.length})</SubSectionLabel>
        <ActionButton readOnly={isReadOnly} onClick={() => setShowAddUnit(true)} testId={`open-add-unit-${companyId}`}>+ Add unit</ActionButton>
      </div>

      {status === "loading" && <EmptyNote>Loading…</EmptyNote>}
      {status === "error" && <EmptyNote>Couldn't load this customer's assets.</EmptyNote>}
      {status === "ready" && units.length === 0 && (
        <EmptyNote>No units currently registered to {companyName}.</EmptyNote>
      )}
      {status === "ready" && units.length > 0 && (
        <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
          {units.map((u) => (
            <UnitRow
              key={u.id} unit={u} isReadOnly={isReadOnly}
              busy={busyId === `assign-${u.id}` || busyId === `unassign-${u.id}`}
              onAssign={() => setAssigningUnit(u)}
              onUnassign={() => handleUnassign(u.id)}
              onHistory={() => openHistory(u.id)}
            />
          ))}
        </div>
      )}

      {showAddUnit && (
        <AddUnitModal
          products={products} companies={companies} lockCompanyId={companyId} isReadOnly={isReadOnly}
          busy={busyId === "add-unit"}
          onCancel={() => setShowAddUnit(false)}
          onSubmit={handleAddUnit}
        />
      )}

      {assigningUnit && (
        <AssignUnitModal
          unit={assigningUnit} companies={companies} lockCompanyId={companyId} isReadOnly={isReadOnly}
          busy={busyId === `assign-${assigningUnit.id}`}
          onCancel={() => setAssigningUnit(null)}
          onSubmit={(form) => handleAssign(assigningUnit.id, form)}
        />
      )}

      {historyUnit && (
        <HistoryModal data={historyUnit} onClose={() => setHistoryUnit(null)} />
      )}
    </div>
  );
}

function SubSectionLabel({ children }) {
  return (
    <div style={{
      fontFamily: "var(--font-body)", fontSize: 10,
      letterSpacing: "0.24em", textTransform: "uppercase",
      color: "rgba(180,110,0,0.95)", fontWeight: 500, marginBottom: 14,
    }}>{children}</div>
  );
}

const TIER_LABELS = { gold: "Gold", platinum: "Platinum", regional_reseller: "Regional Reseller" };

const inputStyle = {
  width: "100%", boxSizing: "border-box",
  background: "rgba(0,0,0,0.03)", border: "1px solid rgba(0,0,0,0.25)",
  color: "#000", fontFamily: "var(--font-body)", fontSize: 13.5, padding: "9px 10px",
  outline: "none",
};
const selectStyle = { ...inputStyle, appearance: "auto" };

Object.assign(window, { AdminCompaniesPage });
