// Solo staff admin area — Settings: this is the single home for every
// external API/data connection Solo Secure has (Xero accounting,
// QuickBooks Online accounting, the live asset-tracking Google Sheet,
// Victron VRM, and any future integration) — admins should always look
// here first, not on the individual feature pages, to see what's
// connected, its live status, and to trigger a manual sync or link.
// Company-level Xero *contact* linking still lives on the Customer record
// page (see CompanyXeroLink in admin-companies-page.jsx) since that's a
// per-customer mapping, not a connection itself — this page is about the
// connections/credentials. QuickBooks's per-company *customer* linking
// follows the exact same split (see CompanyQuickBooksLink in
// admin-companies-page.jsx, gated to North America region companies
// only). Victron VRM is the one exception to "link stays on the Customer
// page": every customer runs their own separate VRM account (confirmed
// with the customer), so per-company linking is deliberately kept inline
// HERE as a list, not split out to the Customer record page, per
// explicit instruction.
//
// QuickBooks is READ-ONLY (confirmed with the customer, unlike Xero):
// it never pushes orders/quotes, and binding a company to a QuickBooks
// Customer never overwrites that company's local name/address the way
// Xero's bind does — it's purely for pulling customer details and
// invoices on demand. QuickBooks only applies to "North America" region
// companies; UK/EU/Rest of the World continue to use Xero unchanged.
//
// Reached via the sidebar nav in AdminShell (see admin-shell.jsx). Reuses
// ModalError/SectionLabel/EmptyNote/ActionButton/ImportSheetModal/
// adminInputStyle from admin-assets-page.jsx (loaded earlier — see
// index.html).
//
// "Connect to Xero" is a plain <a href> to a GET route (routes/admin-xero.ts
// /xero/connect), not a fetch+redirect — Xero's OAuth authorize step is a
// real top-level browser navigation to login.xero.com, which only works as
// a direct link click, never an XHR. After the admin approves access on
// Xero's consent screen, Xero redirects back to /api/admin/xero/callback,
// which itself redirects into /admin?xero=connected|denied|error|state_mismatch
// — read once on mount below and shown as a dismissible banner.

function AdminSettingsPage({ onNavigate }) {
  const [state, setState] = useState({ status: "loading", admin: null, products: [], companies: [] });
  const [xero, setXero] = useState({ status: "loading", connected: false, tenantName: null, connectedAt: null });
  const [disconnecting, setDisconnecting] = useState(false);
  const [disconnectError, setDisconnectError] = useState("");
  const [quickbooks, setQuickbooks] = useState({ status: "loading", connected: false, companyName: null, connectedAt: null });
  const [qbDisconnecting, setQbDisconnecting] = useState(false);
  const [qbDisconnectError, setQbDisconnectError] = useState("");
  const [callbackNotice, setCallbackNotice] = useState(null); // { kind: "connected"|"denied"|"error"|"state_mismatch", provider: "xero"|"quickbooks", message? }
  const [sheetImport, setSheetImport] = useState({ status: "loading", lastRun: null });
  const [showSheetImport, setShowSheetImport] = useState(false);
  const [victron, setVictron] = useState({ status: "loading", connections: [] });
  const [ajax, setAjax] = useState({ status: "loading", connections: [] });
  const [teltonika, setTeltonika] = useState({ status: "loading", connections: [] });

  const loadXeroStatus = () => {
    setXero((s) => ({ ...s, status: s.status === "ready" ? "ready" : "loading" }));
    return fetch("/api/admin/xero/status", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setXero({ status: "ready", connected: !!data.connected, tenantName: data.tenantName || null, connectedAt: data.connectedAt || null }))
      .catch(() => setXero((s) => ({ ...s, status: "error" })));
  };

  // QuickBooks Online (North America accounting — read-only pull of
  // customer details + invoices, no order-push; see routes/admin-quickbooks.ts).
  const loadQuickbooksStatus = () => {
    setQuickbooks((s) => ({ ...s, status: s.status === "ready" ? "ready" : "loading" }));
    return fetch("/api/admin/quickbooks/status", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setQuickbooks({ status: "ready", connected: !!data.connected, companyName: data.companyName || null, connectedAt: data.connectedAt || null }))
      .catch(() => setQuickbooks((s) => ({ ...s, status: "error" })));
  };

  const loadSheetImportStatus = () => {
    setSheetImport((s) => ({ ...s, status: s.status === "ready" ? "ready" : "loading" }));
    return fetch("/api/admin/assets/import/last-run", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setSheetImport({ status: "ready", lastRun: data.lastRun || null }))
      .catch(() => setSheetImport((s) => ({ ...s, status: "error" })));
  };

  const loadVictronConnections = () => {
    setVictron((s) => ({ ...s, status: s.status === "ready" ? "ready" : "loading" }));
    return fetch("/api/admin/victron/connections", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setVictron({ status: "ready", connections: data.connections || [] }))
      .catch(() => setVictron((s) => ({ ...s, status: "error" })));
  };

  // Ajax Systems: same per-company shape as Victron/Teltonika (one row
  // per customer company, each with its own separate Ajax PRO account) --
  // see routes/admin-ajax.ts's GET /ajax/connections.
  const loadAjaxConnections = () => {
    setAjax((s) => ({ ...s, status: s.status === "ready" ? "ready" : "loading" }));
    return fetch("/api/admin/ajax/connections", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setAjax({ status: "ready", connections: data.connections || [] }))
      .catch(() => setAjax((s) => ({ ...s, status: "error" })));
  };

  // Teltonika RMS: same per-company shape as Victron (one row per
  // customer company, joined against whatever link exists) -- see
  // routes/admin-mission-control.ts's GET /teltonika/connections.
  const loadTeltonikaConnections = () => {
    setTeltonika((s) => ({ ...s, status: s.status === "ready" ? "ready" : "loading" }));
    return fetch("/api/admin/teltonika/connections", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setTeltonika({ status: "ready", connections: data.connections || [] }))
      .catch(() => setTeltonika((s) => ({ ...s, status: "error" })));
  };

  useEffect(() => {
    Promise.all([
      fetch("/api/admin/me", { 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())),
      fetch("/api/admin/companies", { credentials: "same-origin" }).then((r) => (r.ok ? r.json() : Promise.reject())),
    ])
      .then(([me, products, companies]) => setState({
        status: "ready", admin: me.admin,
        products: products.products || [], companies: companies.companies || [],
      }))
      .catch(() => onNavigate("admin-login"));
    loadXeroStatus();
    loadQuickbooksStatus();
    loadSheetImportStatus();
    loadVictronConnections();
    loadAjaxConnections();
    loadTeltonikaConnections();

    // Read + immediately strip the one-shot ?xero=... or ?quickbooks=...
    // flag left by each provider's OAuth callback redirect so a page
    // refresh doesn't re-show the banner. Only one of the two can be
    // present at a time (a single OAuth round-trip only ever touches one
    // provider), so checking xero first then falling back to quickbooks
    // is safe.
    const params = new URLSearchParams(window.location.search);
    const xeroFlag = params.get("xero");
    const quickbooksFlag = params.get("quickbooks");
    if (xeroFlag) {
      setCallbackNotice({ kind: xeroFlag, provider: "xero", message: params.get("xero_message") || "" });
      params.delete("xero");
      params.delete("xero_message");
    } else if (quickbooksFlag) {
      setCallbackNotice({ kind: quickbooksFlag, provider: "quickbooks", message: params.get("quickbooks_message") || "" });
      params.delete("quickbooks");
      params.delete("quickbooks_message");
    }
    if (xeroFlag || quickbooksFlag) {
      const cleanUrl = window.location.pathname + (params.toString() ? `?${params.toString()}` : "");
      window.history.replaceState(window.history.state, "", cleanUrl);
    }
  }, []); // eslint-disable-line react-hooks/exhaustive-deps -- mount-only

  const disconnect = async () => {
    if (!window.confirm("Disconnect Xero? Orders won't push quotes and invoice data won't load in the reseller portal until it's reconnected.")) return;
    setDisconnecting(true);
    setDisconnectError("");
    try {
      const res = await fetch("/api/admin/xero/disconnect", { method: "POST", credentials: "same-origin" });
      if (!res.ok) {
        const d = await res.json().catch(() => ({}));
        setDisconnectError(d.error || "Couldn't disconnect. Please try again.");
        setDisconnecting(false);
        return;
      }
      await loadXeroStatus();
    } catch {
      setDisconnectError("Couldn't reach the server. Check your connection and try again.");
    }
    setDisconnecting(false);
  };

  const disconnectQuickbooks = async () => {
    if (!window.confirm("Disconnect QuickBooks? North America customer details and invoice data won't load until it's reconnected.")) return;
    setQbDisconnecting(true);
    setQbDisconnectError("");
    try {
      const res = await fetch("/api/admin/quickbooks/disconnect", { method: "POST", credentials: "same-origin" });
      if (!res.ok) {
        const d = await res.json().catch(() => ({}));
        setQbDisconnectError(d.error || "Couldn't disconnect. Please try again.");
        setQbDisconnecting(false);
        return;
      }
      await loadQuickbooksStatus();
    } catch {
      setQbDisconnectError("Couldn't reach the server. Check your connection and try again.");
    }
    setQbDisconnecting(false);
  };

  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-settings" onNavigate={onNavigate}
      subtitle="Staff only" title="Settings.">

      {callbackNotice && (
        <CallbackBanner notice={callbackNotice} onDismiss={() => setCallbackNotice(null)} />
      )}

      <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6, marginBottom: 26 }}>
        <strong>Data Connections</strong> — every external system Solo Secure reads from or writes
        to lives here: Xero accounting, the live asset-tracking spreadsheet, and anything added
        later. Check this page first for connection status or to run a manual sync.
      </div>

      <SectionLabel>Asset spreadsheet</SectionLabel>

      {sheetImport.status === "loading" && <EmptyNote>Loading…</EmptyNote>}
      {sheetImport.status === "error" && <EmptyNote>Couldn't load the spreadsheet sync status.</EmptyNote>}

      {sheetImport.status === "ready" && (
        <div style={{
          border: "1px solid rgba(0,0,0,0.14)", background: "rgba(0,0,0,0.02)",
          padding: "20px 22px", marginBottom: 20, maxWidth: 560,
        }}>
          <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14 }}>
            <span style={{
              display: "inline-block", width: 8, height: 8, borderRadius: "50%",
              background: sheetImport.lastRun ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.3)",
            }} />
            <span style={{
              fontFamily: "var(--font-body)", fontSize: 12.5, fontWeight: 600,
              letterSpacing: "0.06em", textTransform: "uppercase",
              color: sheetImport.lastRun ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.55)",
            }}>
              {sheetImport.lastRun ? "Connected" : "Never synced"}
            </span>
          </div>

          {sheetImport.lastRun ? (
            <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, color: "rgba(0,0,0,0.75)", marginBottom: 18, lineHeight: 1.6 }}>
              Last synced {sheetImport.lastRun.created_at} by <strong>{sheetImport.lastRun.run_by_name || "an admin"}</strong>:{" "}
              {sheetImport.lastRun.imported_rows} new,{" "}
              {sheetImport.lastRun.skipped_conflict > 0 && <>{sheetImport.lastRun.skipped_conflict} already tracked (left untouched), </>}
              {sheetImport.lastRun.skipped_reserved} reserved skipped.
              <br />
              Pulls live from the master asset tracking spreadsheet and adds brand-new units, purchase
              orders, and invoice references to the Assets page — existing units are never overwritten.
            </div>
          ) : (
            <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, color: "rgba(0,0,0,0.6)", marginBottom: 18, lineHeight: 1.6 }}>
              Not synced yet. Run the import to pull units, purchase orders, and invoice references
              from the live asset tracking spreadsheet.
            </div>
          )}

          <ActionButton readOnly={isReadOnly} onClick={() => setShowSheetImport(true)} testId="settings-open-sheet-import">
            Update from spreadsheet
          </ActionButton>
        </div>
      )}

      <SectionLabel>Xero</SectionLabel>

      {xero.status === "loading" && <EmptyNote>Loading…</EmptyNote>}
      {xero.status === "error" && <EmptyNote>Couldn't load the Xero connection status.</EmptyNote>}
      {disconnectError && <ModalError>{disconnectError}</ModalError>}

      {xero.status === "ready" && (
        <div style={{
          border: "1px solid rgba(0,0,0,0.14)", background: "rgba(0,0,0,0.02)",
          padding: "20px 22px", marginBottom: 20, maxWidth: 560,
        }}>
          <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: xero.connected ? 14 : 4 }}>
            <span style={{
              display: "inline-block", width: 8, height: 8, borderRadius: "50%",
              background: xero.connected ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.3)",
            }} />
            <span style={{
              fontFamily: "var(--font-body)", fontSize: 12.5, fontWeight: 600,
              letterSpacing: "0.06em", textTransform: "uppercase",
              color: xero.connected ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.55)",
            }}>
              {xero.connected ? "Connected" : "Not connected"}
            </span>
          </div>

          {xero.connected ? (
            <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, color: "rgba(0,0,0,0.75)", marginBottom: 18, lineHeight: 1.6 }}>
              Connected to <strong>{xero.tenantName || "your Xero organisation"}</strong>.
              {xero.connectedAt && <> Since {xero.connectedAt}.</>}
              <br />
              Order quotes push to Xero automatically once a customer is linked to a Xero contact
              (see the Xero link section on each Customer record), and invoice data reads live from Xero.
            </div>
          ) : (
            <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, color: "rgba(0,0,0,0.6)", marginBottom: 18, lineHeight: 1.6 }}>
              Not connected yet. Order quotes won't push to Xero and reseller invoice data won't load
              until an admin connects this Xero organisation.
            </div>
          )}

          {xero.connected ? (
            <ActionButton danger disabled={disconnecting} readOnly={isReadOnly} onClick={disconnect} testId="xero-disconnect">
              {disconnecting ? "Disconnecting…" : "Disconnect"}
            </ActionButton>
          ) : isReadOnly ? (
            <span
              data-testid="xero-connect"
              title="Master admins only — you have read-only access"
              style={{
                display: "inline-block", background: "rgba(0,0,0,0.15)", color: "rgba(0,0,0,0.5)",
                border: "1px solid rgba(0,0,0,0.15)", padding: "10px 18px", cursor: "not-allowed",
                fontFamily: "var(--font-body)", fontSize: 11.5, fontWeight: 500,
                letterSpacing: "0.14em", textTransform: "uppercase",
              }}
            >
              Connect to Xero
            </span>
          ) : (
            <a
              href="/api/admin/xero/connect"
              data-testid="xero-connect"
              style={{
                display: "inline-block", background: "#000", color: "#fff",
                border: "1px solid #000", padding: "10px 18px", textDecoration: "none",
                fontFamily: "var(--font-body)", fontSize: 11.5, fontWeight: 500,
                letterSpacing: "0.14em", textTransform: "uppercase",
              }}
            >
              Connect to Xero
            </a>
          )}
        </div>
      )}

      <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6, marginBottom: 40 }}>
        Connecting takes you to Xero's own sign-in and consent screen. Solo Secure only ever
        connects to <strong>one</strong> Xero organisation at a time — connecting again while
        already connected replaces the existing connection.
      </div>

      <SectionLabel>QuickBooks (North America)</SectionLabel>

      <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6, marginBottom: 16 }}>
        North America customers use QuickBooks Online instead of Xero. This connection is{" "}
        <strong>read-only</strong> — Solo Secure Technologies USA Inc's QuickBooks company is
        never pushed to; it's only used to pull customer details and invoices for North America
        companies once each one is linked to a QuickBooks customer.
      </div>

      {quickbooks.status === "loading" && <EmptyNote>Loading…</EmptyNote>}
      {quickbooks.status === "error" && <EmptyNote>Couldn't load the QuickBooks connection status.</EmptyNote>}
      {qbDisconnectError && <ModalError>{qbDisconnectError}</ModalError>}

      {quickbooks.status === "ready" && (
        <div style={{
          border: "1px solid rgba(0,0,0,0.14)", background: "rgba(0,0,0,0.02)",
          padding: "20px 22px", marginBottom: 20, maxWidth: 560,
        }}>
          <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: quickbooks.connected ? 14 : 4 }}>
            <span style={{
              display: "inline-block", width: 8, height: 8, borderRadius: "50%",
              background: quickbooks.connected ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.3)",
            }} />
            <span style={{
              fontFamily: "var(--font-body)", fontSize: 12.5, fontWeight: 600,
              letterSpacing: "0.06em", textTransform: "uppercase",
              color: quickbooks.connected ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.55)",
            }}>
              {quickbooks.connected ? "Connected" : "Not connected"}
            </span>
          </div>

          {quickbooks.connected ? (
            <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, color: "rgba(0,0,0,0.75)", marginBottom: 18, lineHeight: 1.6 }}>
              Connected to <strong>{quickbooks.companyName || "your QuickBooks company"}</strong>.
              {quickbooks.connectedAt && <> Since {quickbooks.connectedAt}.</>}
              <br />
              Customer details and invoice data read live from QuickBooks once a North America
              customer is linked (see the QuickBooks link section on each Customer record).
            </div>
          ) : (
            <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, color: "rgba(0,0,0,0.6)", marginBottom: 18, lineHeight: 1.6 }}>
              Not connected yet. North America customer details and invoice data won't load
              until an admin connects the QuickBooks company for Solo Secure Technologies USA
              Inc.
            </div>
          )}

          {quickbooks.connected ? (
            <ActionButton danger disabled={qbDisconnecting} readOnly={isReadOnly} onClick={disconnectQuickbooks} testId="quickbooks-disconnect">
              {qbDisconnecting ? "Disconnecting…" : "Disconnect"}
            </ActionButton>
          ) : isReadOnly ? (
            <span
              data-testid="quickbooks-connect"
              title="Master admins only — you have read-only access"
              style={{
                display: "inline-block", background: "rgba(0,0,0,0.15)", color: "rgba(0,0,0,0.5)",
                border: "1px solid rgba(0,0,0,0.15)", padding: "10px 18px", cursor: "not-allowed",
                fontFamily: "var(--font-body)", fontSize: 11.5, fontWeight: 500,
                letterSpacing: "0.14em", textTransform: "uppercase",
              }}
            >
              Connect to QuickBooks
            </span>
          ) : (
            <a
              href="/api/admin/quickbooks/connect"
              data-testid="quickbooks-connect"
              style={{
                display: "inline-block", background: "#000", color: "#fff",
                border: "1px solid #000", padding: "10px 18px", textDecoration: "none",
                fontFamily: "var(--font-body)", fontSize: 11.5, fontWeight: 500,
                letterSpacing: "0.14em", textTransform: "uppercase",
              }}
            >
              Connect to QuickBooks
            </a>
          )}
        </div>
      )}

      <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6, marginBottom: 40 }}>
        Connecting takes you to Intuit's own sign-in and consent screen. Solo Secure only ever
        connects to <strong>one</strong> QuickBooks company at a time — connecting again while
        already connected replaces the existing connection.
      </div>

      <SectionLabel>Victron VRM</SectionLabel>

      <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6, marginBottom: 16 }}>
        Every customer runs their own separate VRM account, so each one is linked with its own
        access token below — there's no single shared Victron connection like there is for Xero.
      </div>

      {victron.status === "loading" && <EmptyNote>Loading…</EmptyNote>}
      {victron.status === "error" && <EmptyNote>Couldn't load Victron VRM connections.</EmptyNote>}

      {victron.status === "ready" && (
        <div style={{ border: "1px solid rgba(0,0,0,0.14)", maxWidth: 560, marginBottom: 20 }}>
          {victron.connections.length === 0 && (
            <div style={{ padding: "20px 22px" }}><EmptyNote>No customers yet.</EmptyNote></div>
          )}
          {victron.connections.map((conn, idx) => (
            <VictronCompanyRow
              key={conn.company_id} connection={conn}
              isLast={idx === victron.connections.length - 1}
              isReadOnly={isReadOnly}
              onChanged={loadVictronConnections}
            />
          ))}
        </div>
      )}

      <SectionLabel>Ajax Systems</SectionLabel>

      <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6, marginBottom: 16 }}>
        Every customer runs their own separate Ajax PRO account, so each one is linked with its
        own API key, Ajax Company ID, and Company Token below, same pattern as Victron VRM and
        Teltonika RMS.
      </div>

      {ajax.status === "loading" && <EmptyNote>Loading…</EmptyNote>}
      {ajax.status === "error" && <EmptyNote>Couldn't load Ajax Systems connections.</EmptyNote>}

      {ajax.status === "ready" && (
        <div style={{ border: "1px solid rgba(0,0,0,0.14)", maxWidth: 560, marginBottom: 20 }}>
          {ajax.connections.length === 0 && (
            <div style={{ padding: "20px 22px" }}><EmptyNote>No customers yet.</EmptyNote></div>
          )}
          {ajax.connections.map((conn, idx) => (
            <AjaxCompanyRow
              key={conn.company_id} connection={conn}
              isLast={idx === ajax.connections.length - 1}
              isReadOnly={isReadOnly}
              onChanged={loadAjaxConnections}
            />
          ))}
        </div>
      )}

      <SectionLabel>Teltonika RMS</SectionLabel>

      <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6, marginBottom: 16 }}>
        Every customer runs their own separate Teltonika RMS account, so each one is linked with
        its own access token below, same pattern as Victron VRM.
      </div>

      {teltonika.status === "loading" && <EmptyNote>Loading…</EmptyNote>}
      {teltonika.status === "error" && <EmptyNote>Couldn't load Teltonika RMS connections.</EmptyNote>}

      {teltonika.status === "ready" && (
        <div style={{ border: "1px solid rgba(0,0,0,0.14)", maxWidth: 560, marginBottom: 20 }}>
          {teltonika.connections.length === 0 && (
            <div style={{ padding: "20px 22px" }}><EmptyNote>No customers yet.</EmptyNote></div>
          )}
          {teltonika.connections.map((conn, idx) => (
            <TeltonikaCompanyRow
              key={conn.company_id} connection={conn}
              isLast={idx === teltonika.connections.length - 1}
              isReadOnly={isReadOnly}
              onChanged={loadTeltonikaConnections}
            />
          ))}
        </div>
      )}

      <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6, marginBottom: 40 }}>
        Once a company is connected to Victron, Ajax, or Teltonika above, use{" "}
        <strong>Link devices</strong> on each unit's row on the{" "}
        <a onClick={(e) => { e.preventDefault(); onNavigate("admin-assets"); }} href="#" style={{ color: "#000", textDecoration: "underline" }}>
          Assets
        </a>{" "}
        page to sync that company's devices and match one to this specific asset.
      </div>

      {showSheetImport && (
        <ImportSheetModal
          products={state.products} companies={state.companies}
          isReadOnly={isReadOnly}
          onCancel={() => setShowSheetImport(false)}
          onDone={() => { setShowSheetImport(false); loadSheetImportStatus(); }}
          onNavigate={onNavigate}
        />
      )}
    </AdminShell>
  );
}

// One row per company in the Ajax Systems list — same shape as
// VictronCompanyRow/TeltonikaCompanyRow (per-customer credentials, no
// OAuth), except Ajax needs three fields (API key + Ajax Company ID +
// Company Token) instead of one token. See routes/admin-ajax.ts's
// ajax-link/ajax-sync endpoints. Hub-level linking to a specific asset
// happens from the Assets page's "Link devices" picker (see
// admin-assets-page.jsx), once a company shows Connected here.
function AjaxCompanyRow({ connection, isLast, isReadOnly, onChanged }) {
  const isLinked = !!connection.company_id_ajax;
  const [editing, setEditing] = useState(false);
  const [apiKey, setApiKey] = useState("");
  const [companyIdAjax, setCompanyIdAjax] = useState("");
  const [companyToken, setCompanyToken] = useState("");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");

  const link = async () => {
    if (!apiKey.trim() || !companyIdAjax.trim() || !companyToken.trim()) {
      setError("API key, Company ID, and Company Token are all required.");
      return;
    }
    setBusy(true);
    setError("");
    try {
      const res = await fetch(`/api/admin/companies/${connection.company_id}/ajax-link`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "same-origin",
        body: JSON.stringify({ apiKey: apiKey.trim(), companyIdAjax: companyIdAjax.trim(), companyToken: companyToken.trim() }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setError(data.error || "Couldn't link this connection. Please try again.");
        setBusy(false);
        return;
      }
      setApiKey(""); setCompanyIdAjax(""); setCompanyToken("");
      setEditing(false);
      await onChanged();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  const unlink = async () => {
    if (!window.confirm(`Unlink ${connection.company_name} from Ajax Systems?`)) return;
    setBusy(true);
    try {
      await fetch(`/api/admin/companies/${connection.company_id}/ajax-link`, { method: "DELETE", credentials: "same-origin" });
      await onChanged();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  return (
    <div style={{
      padding: "16px 22px", borderBottom: isLast ? "none" : "1px solid rgba(0,0,0,0.08)",
    }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
          <span style={{
            display: "inline-block", width: 8, height: 8, borderRadius: "50%", flexShrink: 0,
            background: isLinked ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.3)",
          }} />
          <div style={{ minWidth: 0 }}>
            <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, fontWeight: 600, color: "rgba(0,0,0,0.85)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
              {connection.company_name}
            </div>
            <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(0,0,0,0.5)" }}>
              {isLinked
                ? <>Ajax Company ID <strong>{connection.company_id_ajax}</strong>{connection.linked_by_name && <> by {connection.linked_by_name}</>} · {connection.hub_count} hub{connection.hub_count === 1 ? "" : "s"}</>
                : "Not linked"}
            </div>
          </div>
        </div>

        <div style={{ display: "flex", gap: 8, flexShrink: 0 }}>
          {isLinked ? (
            <ActionButton disabled={busy} readOnly={isReadOnly} danger onClick={unlink} testId={`ajax-unlink-${connection.company_id}`}>
              Unlink
            </ActionButton>
          ) : (
            <ActionButton disabled={busy} readOnly={isReadOnly} onClick={() => setEditing((v) => !v)} testId={`ajax-link-toggle-${connection.company_id}`}>
              Link
            </ActionButton>
          )}
        </div>
      </div>

      {editing && !isLinked && (
        <div style={{ marginTop: 12, display: "flex", flexDirection: "column", gap: 8 }}>
          <input
            value={apiKey} onChange={(e) => setApiKey(e.target.value)}
            placeholder="API key" style={adminInputStyle}
            data-testid={`ajax-api-key-input-${connection.company_id}`}
          />
          <input
            value={companyIdAjax} onChange={(e) => setCompanyIdAjax(e.target.value)}
            placeholder="This customer's Ajax Company ID" style={adminInputStyle}
            data-testid={`ajax-company-id-input-${connection.company_id}`}
          />
          <input
            value={companyToken} onChange={(e) => setCompanyToken(e.target.value)}
            placeholder="Company Token" style={adminInputStyle}
            data-testid={`ajax-company-token-input-${connection.company_id}`}
          />
          <div>
            <ActionButton disabled={busy} readOnly={isReadOnly} onClick={link} testId={`ajax-confirm-link-${connection.company_id}`}>
              {busy ? "Linking…" : "Confirm"}
            </ActionButton>
          </div>
        </div>
      )}
      {error && <div style={{ marginTop: 8 }}><ModalError>{error}</ModalError></div>}
    </div>
  );
}

// One row per company in the Victron VRM list. Each customer has their
// own separate VRM account, so linking is just "paste this customer's
// token, we validate it against VRM and store it" -- no OAuth, no
// shared connection. Kept entirely inline on Settings per the
// customer's explicit instruction (unlike Xero, which also has a
// per-company UI on the Customer record page).
function VictronCompanyRow({ connection, isLast, isReadOnly, onChanged }) {
  const isLinked = !!connection.vrm_installation_id;
  const [editing, setEditing] = useState(false);
  const [token, setToken] = useState("");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");

  const link = async () => {
    if (!token.trim()) { setError("Paste this customer's VRM access token first."); return; }
    setBusy(true);
    setError("");
    try {
      const res = await fetch(`/api/admin/companies/${connection.company_id}/victron-link`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "same-origin",
        body: JSON.stringify({ apiToken: token.trim() }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setError(data.error || "Couldn't link this token. Please try again.");
        setBusy(false);
        return;
      }
      setToken("");
      setEditing(false);
      await onChanged();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  const unlink = async () => {
    if (!window.confirm(`Unlink ${connection.company_name} from Victron VRM?`)) return;
    setBusy(true);
    try {
      await fetch(`/api/admin/companies/${connection.company_id}/victron-link`, { method: "DELETE", credentials: "same-origin" });
      await onChanged();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  return (
    <div style={{
      padding: "16px 22px", borderBottom: isLast ? "none" : "1px solid rgba(0,0,0,0.08)",
    }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
          <span style={{
            display: "inline-block", width: 8, height: 8, borderRadius: "50%", flexShrink: 0,
            background: isLinked ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.3)",
          }} />
          <div style={{ minWidth: 0 }}>
            <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, fontWeight: 600, color: "rgba(0,0,0,0.85)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
              {connection.company_name}
            </div>
            <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(0,0,0,0.5)" }}>
              {isLinked
                ? <>Linked to <strong>{connection.installation_name}</strong>{connection.linked_by_name && <> by {connection.linked_by_name}</>}</>
                : "Not linked"}
            </div>
          </div>
        </div>

        <div style={{ display: "flex", gap: 8, flexShrink: 0 }}>
          {isLinked ? (
            <ActionButton disabled={busy} readOnly={isReadOnly} danger onClick={unlink} testId={`victron-unlink-${connection.company_id}`}>
              Unlink
            </ActionButton>
          ) : (
            <ActionButton disabled={busy} readOnly={isReadOnly} onClick={() => setEditing((v) => !v)} testId={`victron-link-toggle-${connection.company_id}`}>
              Link
            </ActionButton>
          )}
        </div>
      </div>

      {editing && !isLinked && (
        <div style={{ marginTop: 12, display: "flex", gap: 8 }}>
          <input
            value={token} onChange={(e) => setToken(e.target.value)}
            placeholder="Paste this customer's VRM access token"
            style={{ ...adminInputStyle, flex: 1 }}
            data-testid={`victron-token-input-${connection.company_id}`}
          />
          <ActionButton disabled={busy} readOnly={isReadOnly} onClick={link} testId={`victron-confirm-link-${connection.company_id}`}>
            {busy ? "Linking…" : "Confirm"}
          </ActionButton>
        </div>
      )}
      {error && <div style={{ marginTop: 8 }}><ModalError>{error}</ModalError></div>}
    </div>
  );
}

// One row per company in the Teltonika RMS list — same shape as
// VictronCompanyRow (per-customer access token, no OAuth). See
// routes/admin-mission-control.ts's teltonika-link/teltonika-sync
// endpoints. Device-level linking to a specific asset happens from the
// Assets page's "Link devices" picker (see admin-assets-page.jsx),
// once a company shows Connected here.
function TeltonikaCompanyRow({ connection, isLast, isReadOnly, onChanged }) {
  const isLinked = !!connection.rms_company_name;
  const [editing, setEditing] = useState(false);
  const [token, setToken] = useState("");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");

  const link = async () => {
    if (!token.trim()) { setError("Paste this customer's RMS access token first."); return; }
    setBusy(true);
    setError("");
    try {
      const res = await fetch(`/api/admin/companies/${connection.company_id}/teltonika-link`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "same-origin",
        body: JSON.stringify({ accessToken: token.trim() }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setError(data.error || "Couldn't link this token. Please try again.");
        setBusy(false);
        return;
      }
      setToken("");
      setEditing(false);
      await onChanged();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  const unlink = async () => {
    if (!window.confirm(`Unlink ${connection.company_name} from Teltonika RMS?`)) return;
    setBusy(true);
    try {
      await fetch(`/api/admin/companies/${connection.company_id}/teltonika-link`, { method: "DELETE", credentials: "same-origin" });
      await onChanged();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  return (
    <div style={{
      padding: "16px 22px", borderBottom: isLast ? "none" : "1px solid rgba(0,0,0,0.08)",
    }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
          <span style={{
            display: "inline-block", width: 8, height: 8, borderRadius: "50%", flexShrink: 0,
            background: isLinked ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.3)",
          }} />
          <div style={{ minWidth: 0 }}>
            <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, fontWeight: 600, color: "rgba(0,0,0,0.85)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
              {connection.company_name}
            </div>
            <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(0,0,0,0.5)" }}>
              {isLinked
                ? <>Linked to <strong>{connection.rms_company_name}</strong>{connection.linked_by_name && <> by {connection.linked_by_name}</>} · {connection.device_count} device{connection.device_count === 1 ? "" : "s"}</>
                : "Not linked"}
            </div>
          </div>
        </div>

        <div style={{ display: "flex", gap: 8, flexShrink: 0 }}>
          {isLinked ? (
            <ActionButton disabled={busy} readOnly={isReadOnly} danger onClick={unlink} testId={`teltonika-unlink-${connection.company_id}`}>
              Unlink
            </ActionButton>
          ) : (
            <ActionButton disabled={busy} readOnly={isReadOnly} onClick={() => setEditing((v) => !v)} testId={`teltonika-link-toggle-${connection.company_id}`}>
              Link
            </ActionButton>
          )}
        </div>
      </div>

      {editing && !isLinked && (
        <div style={{ marginTop: 12, display: "flex", gap: 8 }}>
          <input
            value={token} onChange={(e) => setToken(e.target.value)}
            placeholder="Paste this customer's RMS access token"
            style={{ ...adminInputStyle, flex: 1 }}
            data-testid={`teltonika-token-input-${connection.company_id}`}
          />
          <ActionButton disabled={busy} readOnly={isReadOnly} onClick={link} testId={`teltonika-confirm-link-${connection.company_id}`}>
            {busy ? "Linking…" : "Confirm"}
          </ActionButton>
        </div>
      )}
      {error && <div style={{ marginTop: 8 }}><ModalError>{error}</ModalError></div>}
    </div>
  );
}

function CallbackBanner({ notice, onDismiss }) {
  // provider defaults to "xero" so any old/leftover ?xero=... callback
  // (from a page that hasn't reloaded this bundle yet) still renders the
  // same text it always has -- see admin-quickbooks.ts's /quickbooks/callback
  // for the ?quickbooks=... equivalent flow.
  const providerLabel = notice.provider === "quickbooks" ? "QuickBooks" : "Xero";
  const variants = {
    connected: { bg: "rgba(20,140,60,0.08)", border: "rgba(20,140,60,0.35)", color: "rgba(15,100,45,0.95)", text: `${providerLabel} connected successfully.` },
    denied: { bg: "rgba(180,110,0,0.08)", border: "rgba(180,110,0,0.35)", color: "rgba(140,85,0,0.95)", text: `${providerLabel} connection was cancelled — access wasn't granted.` },
    state_mismatch: { bg: "rgba(190,40,40,0.08)", border: "rgba(190,40,40,0.35)", color: "#8a1f1f", text: `That ${providerLabel} connection attempt expired or was invalid. Please try again.` },
    error: { bg: "rgba(190,40,40,0.08)", border: "rgba(190,40,40,0.35)", color: "#8a1f1f", text: notice.message || `Couldn't connect to ${providerLabel}. Please try again.` },
  };
  const v = variants[notice.kind] || variants.error;
  return (
    <div style={{
      background: v.bg, border: `1px solid ${v.border}`, color: v.color,
      padding: "12px 14px", marginBottom: 24, display: "flex",
      justifyContent: "space-between", alignItems: "center", gap: 12,
      fontFamily: "var(--font-body)", fontSize: 13, lineHeight: 1.5,
    }}>
      <span>{v.text}</span>
      <button type="button" onClick={onDismiss} style={{
        background: "none", border: "none", color: "inherit", cursor: "pointer",
        fontFamily: "var(--font-body)", fontSize: 11, letterSpacing: "0.08em", textTransform: "uppercase",
      }}>Dismiss</button>
    </div>
  );
}

Object.assign(window, { AdminSettingsPage });
