// Databricks connector — real OAuth U2M flow against the backend
// (/org/connectors/databricks/*), replacing the mock AddConnectorFlow for
// this vendor.
//
// Flow: setup form (workspace host + OAuth app client id) → backend returns an
// authorize URL → popup → the backend callback completes the token exchange
// while we poll GET /connection → pick a SQL Warehouse + catalogs/schemas →
// PUT /selection kicks off a schema scan job that lands tables/columns in the
// knowledge graph; from then on the agent can query the selected schemas
// directly with SQL.

const { useState: useDbx, useEffect: useDbxEffect, useRef: useDbxRef } = React;

// On a first connect we pre-check every schema we found, so the user reviews and
// saves instead of hunting through a tree. Above this many we leave the tree
// unchecked: each selected schema is scanned with an LLM pass, so silently
// enrolling a large workspace would be slow and expensive. Keep at or below the
// backend's DATABRICKS_MAX_SELECTED_SCHEMAS (default 20).
const DBX_AUTOSELECT_MAX = 20;

// A Databricks personal access token can be *scoped* to a subset of the REST
// API, and the connector needs exactly two scopes: `sql` (list warehouses, run
// queries) and `unity-catalog` (list catalogs and schemas). The token dialog's
// "BI Tools" preset grants only `sql` — and the backend verifies a pasted token
// by listing warehouses, so such a token CONNECTS FINE and only falls over
// later, here in the picker. Left unexplained that reads as "my Databricks user
// can't see any data", sending people to hunt through grants for a problem that
// is in the token.
function dbxIsPat(conn) {
  const method = conn && (conn.auth_method || (conn.settings && conn.settings.auth_method));
  return method === 'pat';
}

// Missing scope is refused, not silently filtered — so a REJECTED listing is
// the case worth blaming on scope, while an empty-but-successful one is much
// more likely to be a user with no grants. Everything else (429, 5xx, a dropped
// connection) must NOT be dressed up as an authorization problem, or the next
// transient blip sends the user off to regenerate a perfectly good token.
function dbxLooksLikeAuthFailure(message) {
  return /\b401\b|\b403\b|unauthor|forbidden|permission|scope|denied|invalid.token|expired|revoked/i
    .test(String(message || ''));
}

// The remedy is identical wherever it appears; the sentence that diagnoses the
// failure is not, so each call site supplies its own and this covers only the fix.
function DbxScopeRemedy() {
  return (
    <span>
      Generate a new token with <span className="mono-sm mono">Scope</span> set
      to <span className="mono-sm mono">Other APIs</span> and
      both <span className="mono-sm mono">sql</span> and <span className="mono-sm mono">unity-catalog</span> added
      (a token with no scopes at all also works), then disconnect and reconnect here.
    </span>
  );
}

// Announced whenever this drawer changes the connection, so the Connectors-page
// card (org_databricks.jsx) reflects it at once instead of waiting for its poll.
function dbxAnnounceChange() {
  try { window.dispatchEvent(new CustomEvent('parcle:databricks-changed')); }
  catch (e) { /* very old browser — the card's poll still catches up */ }
}

function dbxApi(method, path, body) {
  const auth = (typeof parcleAuthHeaders === 'function') ? parcleAuthHeaders() : {};
  return fetch(parcleApiBase() + path, {
    method,
    headers: { 'Content-Type': 'application/json', ...auth },
    body: body ? JSON.stringify(body) : undefined,
  }).then(async (res) => {
    if (res.status === 401 && typeof parcleNotifyUnauthorized === 'function') parcleNotifyUnauthorized();
    if (!res.ok) {
      let detail = '';
      try { detail = (await res.json()).detail || ''; } catch (e) {}
      throw new Error(detail || `${method} ${path} → ${res.status}`);
    }
    if (res.status === 204) return {};
    try { return await res.json(); } catch (e) { return {}; }
  });
}

const DatabricksAPI = {
  // Both fields are optional — omitted, the server falls back to this account's
  // previous connection and then to the deployment defaults.
  connectStart(workspaceHost, clientId) {
    const body = {};
    if (workspaceHost) body.workspace_host = workspaceHost;
    if (clientId) body.client_id = clientId;
    return dbxApi('POST', '/org/connectors/databricks/connect/start', body);
  },
  connection() { return dbxApi('GET', '/org/connectors/databricks/connection'); },
  disconnect() { return dbxApi('DELETE', '/org/connectors/databricks/connection'); },
  warehouses() { return dbxApi('GET', '/org/connectors/databricks/warehouses'); },
  catalogs() { return dbxApi('GET', '/org/connectors/databricks/catalogs'); },
  schemas(catalog) {
    return dbxApi('GET', `/org/connectors/databricks/schemas?catalog=${encodeURIComponent(catalog)}`);
  },
  // Personal-access-token connect — no OAuth app has to exist in the customer's
  // Databricks account, so a user can do this without their account admin.
  connectWithToken(workspaceHost, token) {
    return dbxApi('POST', '/org/connectors/databricks/connect/token',
      { workspace_host: workspaceHost, token });
  },
  getSelection() { return dbxApi('GET', '/org/connectors/databricks/selection'); },
  putSelection(warehouseId, schemas) {
    return dbxApi('PUT', '/org/connectors/databricks/selection', { warehouse_id: warehouseId, schemas });
  },
  scanStatus() { return dbxApi('GET', '/org/connectors/databricks/schema-scan'); },
  // Drops the schema's ingest record server-side and starts a scan that
  // re-ingests it; the other selected schemas stay skipped.
  rescanSchema(catalog, schema) {
    return dbxApi('POST', '/org/connectors/databricks/schema-scan/rescan', { catalog, schema });
  },
};

// Tolerant response readers — keep the UI stable across minor contract drift.
function dbxIsConnected(conn) {
  if (!conn) return false;
  return conn.connected === true || conn.is_active === true || conn.status === 'connected' || conn.status === 'active';
}
function dbxNeedsReconnect(conn) {
  return !!(conn && (conn.needs_reconnect || conn.status === 'needs_reconnect'));
}
function dbxList(resp, key) {
  if (Array.isArray(resp)) return resp;
  if (resp && Array.isArray(resp[key])) return resp[key];
  return [];
}
function dbxSelection(resp) {
  const sel = (resp && (resp.selection || resp)) || {};
  return {
    warehouse_id: sel.warehouse_id || null,
    schemas: Array.isArray(sel.schemas) ? sel.schemas : [],
  };
}

// Per-schema scan states from the job payload:
// {"catalog.schema": "pending"|"running"|"done"|"skipped"|"failed"}.
// Keyed lowercase here because Unity Catalog identifiers are case-insensitive.
// Null when the job predates per-schema reporting — callers fall back to the
// plain (stateless) chips.
function dbxSchemaStates(scan) {
  const raw = scan && scan.payload && scan.payload.schema_states;
  if (!raw || typeof raw !== 'object') return null;
  const map = {};
  Object.keys(raw).forEach(k => { map[k.toLowerCase()] = String(raw[k]); });
  return map;
}

// Chip decoration per state. "skipped" reads "already ingested": the schema
// was ingested by an earlier scan and this job deliberately did not redo it —
// exactly the distinction the user needs when they add a second schema and
// wonder whether the first is being re-run.
const DBX_STATE_META = {
  done:    { dot: 'live',    label: 'ingested' },
  skipped: { dot: 'live',    label: 'already ingested' },
  running: { dot: 'syncing', label: 'ingesting…' },
  pending: { dot: 'paused',  label: 'queued' },
  failed:  { dot: 'error',   label: 'failed' },
};

function DatabricksDrawer({ open, onClose }) {
  // view: 'loading' | 'setup' | 'authorizing' | 'select' | 'manage'
  const [view, setView] = useDbx('loading');
  const [error, setError] = useDbx(null);
  const [conn, setConn] = useDbx(null);

  // setup form. showForm is false whenever the server can authorize on its own,
  // so the default path is a single button rather than a form.
  const [host, setHost] = useDbx('');
  const [clientId, setClientId] = useDbx('');
  const [showForm, setShowForm] = useDbx(false);
  const [readyToAuthorize, setReadyToAuthorize] = useDbx(false);
  // Whether the server would supply a client id if the field is left blank.
  const [clientIdKnown, setClientIdKnown] = useDbx(false);
  // 'oauth' | 'token' — which credential the setup form is collecting.
  const [authMode, setAuthMode] = useDbx('oauth');
  const [patToken, setPatToken] = useDbx('');
  // The server verifies a pasted token by listing warehouses, so a token scoped
  // to `unity-catalog` alone is rejected right here — and the rejection reads
  // "check it hasn't expired or been revoked", pointing away from the real
  // cause. It never reaches the picker where the scope hints live, so this
  // screen has to make the case itself.
  const [tokenAuthFailed, setTokenAuthFailed] = useDbx(false);

  // selection state
  const [warehouses, setWarehouses] = useDbx([]);
  const [catalogs, setCatalogs] = useDbx([]);
  const [schemasByCatalog, setSchemasByCatalog] = useDbx({}); // name -> [{name}] | 'loading'
  const [openCatalogs, setOpenCatalogs] = useDbx({});         // name -> bool
  const [warehouseId, setWarehouseId] = useDbx('');
  const [picked, setPicked] = useDbx({});                     // "catalog.schema" -> {catalog, schema}
  const [autoPicked, setAutoPicked] = useDbx(0);              // how many we pre-checked, for the hint
  const [saving, setSaving] = useDbx(false);
  const [scan, setScan] = useDbx(null);                       // latest schema-scan job
  // {catalog, schema} awaiting the red force-rescan confirmation, if any.
  const [rescanTarget, setRescanTarget] = useDbx(null);
  const [rescanBusy, setRescanBusy] = useDbx(false);
  // Listing failures are held per-list rather than in `error`: one of the two
  // can fail on its own (a token scoped to `sql` lists warehouses and nothing
  // else), and a single banner would neither say which half is broken nor
  // leave the working half usable.
  const [warehousesError, setWarehousesError] = useDbx(null);
  const [catalogsError, setCatalogsError] = useDbx(null);
  // The select view renders before its listings arrive. Without this the tree is
  // briefly empty for EVERY user, and the empty state would spend that whole
  // round-trip accusing a perfectly good token of missing a scope.
  const [listsLoading, setListsLoading] = useDbx(false);

  const pollRef = useDbxRef(null);
  const popupRef = useDbxRef(null);

  const stopPoll = () => { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } };

  // On open: figure out where we are.
  useDbxEffect(() => {
    if (!open) { stopPoll(); return undefined; }
    let cancelled = false;
    setError(null);
    setRescanTarget(null);
    setView('loading');
    (async () => {
      try {
        const c = await DatabricksAPI.connection().catch(() => null);
        if (cancelled) return;
        setConn(c);
        // Prefill the setup form from the stored record so a reconnect doesn't
        // make the user dig out their workspace URL and client id again.
        const host0 = c && (c.workspace_host || (c.settings && c.settings.workspace_host));
        if (host0) setHost(host0);
        const ready = !!(c && c.ready_to_authorize);
        setReadyToAuthorize(ready);
        setClientIdKnown(!!(c && c.client_id_known));
        setShowForm(!ready);   // nothing to ask when the server can authorize
        // Default the form to whichever credential this user can actually
        // produce alone: OAuth only when a client id already exists.
        setAuthMode((c && c.client_id_known) ? 'oauth' : 'token');
        if (dbxIsConnected(c) || dbxNeedsReconnect(c)) {
          await enterManage(c, () => cancelled);
        } else {
          setView('setup');
        }
      } catch (err) {
        if (!cancelled) { setError(err.message); setView('setup'); }
      }
    })();
    return () => { cancelled = true; stopPoll(); };
  }, [open]);

  const enterManage = async (c, isCancelled) => {
    setView('manage');
    try {
      const [selResp, scanResp] = await Promise.all([
        DatabricksAPI.getSelection().catch(() => null),
        DatabricksAPI.scanStatus().catch(() => null),
      ]);
      if (isCancelled && isCancelled()) return;
      const sel = dbxSelection(selResp);
      setWarehouseId(sel.warehouse_id || '');
      const map = {};
      sel.schemas.forEach(s => { map[`${s.catalog}.${s.schema}`] = s; });
      setPicked(map);
      const job = scanResp && scanResp.id ? scanResp : (scanResp && scanResp.job) || null;
      setScan(job);
      // A scan still running when the drawer opens must resume live updates —
      // without this the status (and the per-schema chips) freeze at whatever
      // this first read happened to see.
      if (job && !['done', 'failed', 'cancelled'].includes(job.status)) trackScan();
      // Connected but nothing picked yet → go straight to the picker. A
      // needs-reconnect record can't list resources, so leave it on manage.
      if (!sel.schemas.length && !dbxNeedsReconnect(c)) await enterSelect();
    } catch (e) { /* manage view tolerates partial data */ }
  };

  // With {useForm:true} the typed workspace/client id win; otherwise we send
  // nothing and let the server use what it already knows (the account's previous
  // connection, else the deployment defaults).
  const startConnect = ({ useForm } = {}) => {
    setError(null);
    const h = useForm ? host.trim() : '';
    if (useForm && !h) { setError('Workspace URL is required'); return; }
    // Catch the missing client id here rather than letting the server reject the
    // connect after the popup has already opened.
    if (useForm && !clientIdKnown && !clientId.trim()) {
      setError('An OAuth client ID is required — copy it from the app connection registered in your Databricks account console.');
      return;
    }
    // Open the window SYNCHRONOUSLY inside the click handler and navigate it
    // once the backend hands back the authorize URL. Calling window.open after
    // an await puts it outside the user gesture, which pop-up blockers reject.
    const popup = window.open('', 'parcle-databricks-auth', 'width=560,height=720');
    if (!popup) {
      setError('Your browser blocked the Databricks window — allow pop-ups for this site and try again.');
      return;
    }
    popupRef.current = popup;
    popup.document.write('<p style="font:14px system-ui;padding:24px">Opening Databricks…</p>');
    _startConnectAsync({ useForm, host: h, popup });
  };

  const _startConnectAsync = async ({ useForm, host: h, popup }) => {
    try {
      const r = await DatabricksAPI.connectStart(
        h || undefined,
        useForm ? (clientId.trim() || undefined) : undefined,
      );
      const url = r && (r.authorize_url || r.authorizeUrl);
      if (!url) throw new Error('Backend did not return an authorize URL');
      setView('authorizing');
      popup.location = url;
      // Poll until the backend callback records the connection.
      const startedAt = Date.now();
      stopPoll();
      pollRef.current = setInterval(async () => {
        if (Date.now() - startedAt > 5 * 60_000) {
          stopPoll();
          setError('Authorization timed out — try again.');
          setView('setup');
          return;
        }
        try {
          const c = await DatabricksAPI.connection();
          if (dbxIsConnected(c)) {
            stopPoll();
            setConn(c);
            try { if (popupRef.current) popupRef.current.close(); } catch (e) {}
            dbxAnnounceChange();
            await enterSelect();
          }
        } catch (e) { /* keep polling */ }
      }, 2000);
    } catch (err) {
      try { popup.close(); } catch (e) { /* already gone */ }
      setError(err.message || 'Could not start authorization');
      setView('setup');
    }
  };

  // Everything that can be decided for the user IS decided: a lone warehouse is
  // chosen, every catalog is expanded and its schemas loaded up front, and — on
  // a first connect — everything found is pre-checked when it fits under the
  // server's cap. The user lands on a filled-in review screen, so the common
  // case is one click (Save) rather than a hunt through the tree.
  const submitToken = async () => {
    setError(null);
    const h = host.trim();
    if (!h) { setError('Workspace URL is required'); return; }
    if (!patToken.trim()) { setError('Paste the access token you generated in Databricks'); return; }
    setSaving(true);
    setTokenAuthFailed(false);
    try {
      const c = await DatabricksAPI.connectWithToken(h, patToken.trim());
      setSaving(false);
      setPatToken('');            // don't keep the secret in component state
      setConn(c);
      dbxAnnounceChange();
      await enterSelect();
    } catch (err) {
      setSaving(false);
      const msg = err.message || 'Could not connect with that token';
      setError(msg);
      setTokenAuthFailed(dbxLooksLikeAuthFailure(msg));
    }
  };

  const enterSelect = async () => {
    setView('select');
    setError(null);
    setRescanTarget(null);
    setWarehousesError(null);
    setCatalogsError(null);
    setListsLoading(true);
    try {
      // Each listing is caught on its own so a scope that covers one API but
      // not the other still renders the half that works, with the failure
      // reported next to the list it belongs to.
      let whErr = null;
      let catErr = null;
      const [whResp, catResp, selResp] = await Promise.all([
        DatabricksAPI.warehouses().catch((e) => { whErr = e; return []; }),
        DatabricksAPI.catalogs().catch((e) => { catErr = e; return []; }),
        DatabricksAPI.getSelection().catch(() => null),
      ]);
      setWarehousesError(whErr ? (whErr.message || 'Could not list SQL Warehouses') : null);
      setCatalogsError(catErr ? (catErr.message || 'Could not list catalogs') : null);
      const whs = dbxList(whResp, 'warehouses');
      const cats = dbxList(catResp, 'catalogs');
      setWarehouses(whs);
      setCatalogs(cats);
      // Both lists have landed — anything below only fills in the tree, so the
      // empty state can now be trusted to mean "empty", not "not here yet".
      setListsLoading(false);
      const sel = dbxSelection(selResp);
      if (sel.warehouse_id) setWarehouseId(sel.warehouse_id);
      else if (whs.length === 1) setWarehouseId(whs[0].id);

      // Every catalog opens expanded the moment the list lands, so nothing has
      // to be clicked open — expansion is set once, up front (not after the
      // schema loads), so the tree never sits collapsed while schemas stream in
      // and a catalog the user collapses mid-load stays collapsed.
      const names = cats.map(c => c.name || c);
      const expanded = {};
      const pending = {};
      names.forEach((name) => { expanded[name] = true; pending[name] = 'loading'; });
      setOpenCatalogs(expanded);
      setSchemasByCatalog(pending);
      // The stored selection is restored before the schema loads start: the
      // tree is already clickable while schemas stream in, so a later blanket
      // setPicked would silently wipe any box the user toggles during the load.
      if (sel.schemas.length) {
        const map = {};
        sel.schemas.forEach(s => { map[`${s.catalog}.${s.schema}`] = s; });
        setPicked(map);
      }
      // Each catalog's schemas render as soon as its own listing returns; a
      // catalog we can't read is left empty rather than failing the page.
      const loaded = await Promise.all(names.map(async (name) => {
        let schemas = [];
        try { schemas = dbxList(await DatabricksAPI.schemas(name), 'schemas'); }
        catch (e) { /* unreadable catalog renders as empty */ }
        setSchemasByCatalog(prev => ({ ...prev, [name]: schemas }));
        return [name, schemas];
      }));

      if (!sel.schemas.length) {
        // First connect: pre-check everything, but only when it fits the cap —
        // each selected schema costs an LLM-backed scan, so a huge workspace
        // gets an empty tree and an explicit choice instead. A hand-made pick
        // during the load window wins over the auto-selection.
        const all = [];
        loaded.forEach(([catalog, schemas]) => {
          schemas.forEach(s => all.push({ catalog, schema: s.name || s }));
        });
        if (all.length && all.length <= DBX_AUTOSELECT_MAX) {
          const map = {};
          all.forEach(s => { map[`${s.catalog}.${s.schema}`] = s; });
          setPicked(prev => (Object.keys(prev).length ? prev : map));
          setAutoPicked(all.length);
        }
      }
    } catch (err) {
      setError(err.message || 'Could not load Databricks resources');
    } finally {
      setListsLoading(false);
    }
  };

  const toggleCatalog = async (name) => {
    setOpenCatalogs(prev => ({ ...prev, [name]: !prev[name] }));
    if (schemasByCatalog[name]) return;
    setSchemasByCatalog(prev => ({ ...prev, [name]: 'loading' }));
    try {
      const resp = await DatabricksAPI.schemas(name);
      setSchemasByCatalog(prev => ({ ...prev, [name]: dbxList(resp, 'schemas') }));
    } catch (err) {
      setSchemasByCatalog(prev => ({ ...prev, [name]: [] }));
      setError(err.message);
    }
  };

  const catalogAllPicked = (catalog, schemas) =>
    schemas.every(s => picked[`${catalog}.${s.name || s}`]);

  const toggleCatalogAll = (catalog, schemas) => {
    const clearing = catalogAllPicked(catalog, schemas);
    setPicked(prev => {
      const next = { ...prev };
      schemas.forEach(s => {
        const name = s.name || s;
        const k = `${catalog}.${name}`;
        if (clearing) delete next[k];
        else next[k] = { catalog, schema: name };
      });
      return next;
    });
  };

  const togglePick = (catalog, schema) => {
    const k = `${catalog}.${schema}`;
    setPicked(prev => {
      const next = { ...prev };
      if (next[k]) delete next[k];
      else next[k] = { catalog, schema };
      return next;
    });
  };

  const saveSelection = async () => {
    setError(null);
    setRescanTarget(null);
    const schemas = Object.values(picked);
    if (!warehouseId) { setError('Pick a SQL Warehouse first'); return; }
    if (!schemas.length) { setError('Pick at least one schema'); return; }
    setSaving(true);
    try {
      // PUT /selection returns the schema-scan JobResponse directly.
      const r = await DatabricksAPI.putSelection(warehouseId, schemas);
      setSaving(false);
      const jobId = r && (r.id || r.job_id);
      // Keep the whole job — its payload carries the per-schema states the
      // manage view renders.
      setScan(jobId ? { ...r, id: jobId, status: r.status || 'running' } : null);
      setView('manage');
      dbxAnnounceChange();
      trackScan();
    } catch (err) {
      setSaving(false);
      setError(err.message || 'Could not save selection');
    }
  };

  // Track the scan job so the manage view shows live progress.
  const trackScan = () => {
    stopPoll();
    pollRef.current = setInterval(async () => {
      try {
        const s = await DatabricksAPI.scanStatus();
        const job = s && s.id ? s : (s && s.job) || null;
        setScan(job);
        if (job && ['done', 'failed', 'cancelled'].includes(job.status)) stopPoll();
      } catch (e) { /* transient */ }
    }, 2500);
  };

  // There is no dedicated rescan endpoint: re-saving the stored selection IS
  // the retry — PUT /selection starts a fresh scan job over the same schemas.
  // A manage view with nothing stored can't retry, so it falls through to the
  // picker where the user can save (and thereby scan) a selection.
  const retryScan = async () => {
    const schemas = Object.values(picked);
    if (!warehouseId || !schemas.length) { await enterSelect(); return; }
    setError(null);
    setRescanTarget(null);
    setSaving(true);
    try {
      const r = await DatabricksAPI.putSelection(warehouseId, schemas);
      setSaving(false);
      const jobId = r && (r.id || r.job_id);
      setScan(jobId ? { ...r, id: jobId, status: r.status || 'running' } : { status: 'running' });
      dbxAnnounceChange();
      trackScan();
    } catch (err) {
      setSaving(false);
      setError(err.message || 'Could not restart the schema scan');
    }
  };

  // Confirmed force rescan of one schema (the red warning's Rescan button).
  const doRescan = async () => {
    if (!rescanTarget) return;
    setError(null);
    setRescanBusy(true);
    try {
      const r = await DatabricksAPI.rescanSchema(rescanTarget.catalog, rescanTarget.schema);
      const jobId = r && (r.id || r.job_id);
      setScan(jobId ? { ...r, id: jobId, status: r.status || 'running' } : { status: 'running' });
      setRescanTarget(null);
      dbxAnnounceChange();
      trackScan();
    } catch (err) {
      setError(err.message || 'Could not start the rescan');
    } finally {
      setRescanBusy(false);
    }
  };

  const doDisconnect = async () => {
    if (!window.confirm('Disconnect Databricks? The agent will lose access to the selected schemas.')) return;
    try {
      await DatabricksAPI.disconnect();
      setConn(null);
      setPicked({});
      setWarehouseId('');
      // The stored workspace is gone; only deployment defaults remain, so the
      // one-click path survives a disconnect only if the server has them.
      const c = await DatabricksAPI.connection().catch(() => null);
      const ready = !!(c && c.ready_to_authorize);
      setConn(c);
      setReadyToAuthorize(ready);
      setShowForm(!ready);
      setView('setup');
      dbxAnnounceChange();
    } catch (err) {
      setError(err.message || 'Disconnect failed');
    }
  };

  if (!open) return null;
  const pickedList = Object.values(picked);
  // Per-schema scan states (null on jobs that predate them → plain chips).
  const schemaStates = dbxSchemaStates(scan);
  const stateValues = schemaStates ? Object.values(schemaStates) : [];
  const scanDoneCount = stateValues.filter(v => v === 'done' || v === 'skipped').length;
  const scanRunning = !!(scan && !['done', 'failed', 'cancelled'].includes(scan.status));

  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, background: 'rgba(0,0,0,.4)', zIndex: 95,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        width: 680, maxHeight: '84vh', display: 'flex', flexDirection: 'column',
        background: 'var(--surface-raised)', borderRadius: 12,
        border: '1px solid var(--border)', boxShadow: 'var(--shadow-2)', overflow: 'hidden',
        animation: 'slideUp 240ms cubic-bezier(.2,.8,.2,1)',
      }}>
        {/* Header */}
        <div style={{ padding: '16px 20px', borderBottom: '1px solid var(--border-subtle)', display: 'flex', alignItems: 'center', gap: 12 }}>
          <VendorLogo vendor="databricks" size={28}/>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 14, fontWeight: 500 }}>Databricks</div>
            <div className="t-micro text-tertiary" style={{ marginTop: 2 }}>
              {view === 'manage' ? 'Connected' : 'Query your lakehouse with the knowledge agent'}
            </div>
          </div>
          {view === 'manage' && dbxNeedsReconnect(conn) && (
            <span className="chip warning" style={{ padding: '2px 8px' }}>Reconnect needed</span>
          )}
          <button className="btn sm ghost" onClick={onClose}><Icons.X size={14}/></button>
        </div>

        {/* Body */}
        <div style={{ padding: 24, overflowY: 'auto', flex: 1 }}>
          {error && (
            <div className="t-small" style={{
              color: 'var(--error)', marginBottom: 14, padding: '8px 12px',
              border: '1px solid var(--error)', borderRadius: 8, opacity: .9,
            }}>{error}</div>
          )}

          {view === 'loading' && <div className="t-body text-secondary">Loading…</div>}

          {/* One-click: the server already knows the workspace and OAuth app, so
              there is nothing to ask — click through to Databricks' own login and
              consent screen. The form below is only for deployments where that
              isn't configured (or when connecting a different workspace). */}
          {view === 'setup' && !showForm && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 16, maxWidth: 480 }}>
              <div>
                <div className="t-h2">Connect Databricks</div>
                <div className="t-small text-secondary" style={{ marginTop: 4 }}>
                  You'll sign in to Databricks and approve read-only access.
                  Parcle never sees your password.
                </div>
              </div>
              {conn && conn.workspace_host && (
                <div>
                  <div className="t-micro text-tertiary">Workspace</div>
                  <div className="mono-sm mono" style={{ marginTop: 2 }}>{conn.workspace_host}</div>
                </div>
              )}
              <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
                <button className="btn accent" onClick={() => startConnect()}>
                  Continue to Databricks →
                </button>
                <button className="btn ghost sm" onClick={() => setShowForm(true)}>
                  Use a different workspace
                </button>
              </div>
            </div>
          )}

          {view === 'setup' && showForm && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 14, maxWidth: 520 }}>
              <div>
                <div className="t-h2">Connect your workspace</div>
                <div className="t-small text-secondary" style={{ marginTop: 4 }}>
                  Databricks serves sign-in from your own workspace domain, so we need to
                  know which workspace to reach either way.
                </div>
              </div>

              {/* Two credentials, very different prerequisites — let the user
                  pick rather than dead-ending them on the one they can't do. */}
              <div style={{ display: 'flex', gap: 8 }}>
                <button className={`btn sm${authMode === 'token' ? ' accent' : ''}`}
                  onClick={() => { setAuthMode('token'); setError(null); }}>
                  Access token
                </button>
                <button className={`btn sm${authMode === 'oauth' ? ' accent' : ''}`}
                  onClick={() => { setAuthMode('oauth'); setError(null); }}>
                  OAuth
                </button>
              </div>

              <label style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                <span className="t-micro text-tertiary">Workspace URL</span>
                <input className="input" placeholder="https://dbc-xxxxxxxx.cloud.databricks.com"
                  value={host} onChange={e => setHost(e.target.value)}/>
              </label>

              {authMode === 'token' ? (
                <React.Fragment>
                  <label style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                    <span className="t-micro text-tertiary">Personal access token</span>
                    <input className="input" type="password" autoComplete="off"
                      placeholder="dapi…"
                      value={patToken} onChange={e => setPatToken(e.target.value)}/>
                  </label>
                  <div className="t-small text-secondary">
                    In Databricks: <span className="mono-sm mono">Settings → Developer → Access
                    tokens → Generate new token</span>. Nothing has to be registered by an
                    administrator.
                  </div>
                  {/* Named before the token is minted, because a wrongly scoped one
                      still connects — the mistake only shows up two screens later as
                      an empty catalog tree. Phrased conditionally because the Scope
                      selector is absent in workspaces without token scoping, where
                      the token is unscoped and already correct. */}
                  <div className="t-small text-secondary">
                    If the token dialog offers a <span className="mono-sm mono">Scope</span> selector,
                    choose <span className="mono-sm mono">Other APIs</span> and
                    add <strong>both</strong> <span className="mono-sm mono">sql</span> and
                    <span> </span><span className="mono-sm mono">unity-catalog</span>. Neither one alone
                    is enough: <span className="mono-sm mono">sql</span> on its own (the
                    <span> </span><span className="mono-sm mono">BI Tools</span> preset) connects but can't list your
                    catalogs, and <span className="mono-sm mono">unity-catalog</span> on its own is
                    rejected here. No Scope selector means an unscoped token, which is fine.
                  </div>
                  {tokenAuthFailed && (
                    <div className="t-small text-secondary">
                      If that token is scoped, check it includes <span className="mono-sm mono">sql</span> —
                      we verify a token by listing your SQL Warehouses, so one limited
                      to <span className="mono-sm mono">unity-catalog</span> is refused at this step.
                    </div>
                  )}
                  <div className="t-small text-secondary">
                    The token carries your own permissions, so give it a read-only
                    identity if you can, and we'll store it encrypted.
                  </div>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
                    <button className="btn accent" onClick={submitToken} disabled={saving}>
                      {saving ? 'Verifying…' : 'Connect'}
                    </button>
                    {readyToAuthorize && (
                      <button className="btn ghost sm" onClick={() => setShowForm(false)}>Back</button>
                    )}
                  </div>
                </React.Fragment>
              ) : (
                <React.Fragment>
                  <label style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                    <span className="t-micro text-tertiary">OAuth client ID</span>
                    <input className="input"
                      placeholder={clientIdKnown
                        ? 'leave blank to use the default'
                        : 'from your Databricks OAuth app connection'}
                      value={clientId} onChange={e => setClientId(e.target.value)}/>
                  </label>
                  {!clientIdKnown && (
                    <div className="t-small text-secondary">
                      OAuth needs an app connection registered in your Databricks
                      <span> </span><span className="mono-sm mono">account console</span> by an
                      account admin, with our redirect URL. If you can't do that, use an
                      access token instead.
                    </div>
                  )}
                  <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
                    <button className="btn accent" onClick={() => startConnect({ useForm: true })}>
                      Continue to Databricks →
                    </button>
                    {readyToAuthorize && (
                      <button className="btn ghost sm" onClick={() => setShowForm(false)}>Back</button>
                    )}
                  </div>
                </React.Fragment>
              )}
            </div>
          )}

          {view === 'authorizing' && (
            <div style={{ textAlign: 'center', padding: '40px 0' }}>
              <div className="t-h2">Waiting for authorization…</div>
              <div className="t-small text-secondary" style={{ marginTop: 6 }}>
                Approve access in the Databricks window. This screen updates automatically.
              </div>
              <button className="btn ghost" style={{ marginTop: 18 }}
                onClick={() => { stopPoll(); setView('setup'); }}>Cancel</button>
            </div>
          )}

          {view === 'select' && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
              <div>
                <div className="t-h2">
                  {autoPicked ? 'Review what the agent can query' : 'Choose what the agent can query'}
                </div>
                <div className="t-small text-secondary" style={{ marginTop: 4 }}>
                  {autoPicked
                    ? `We selected all ${autoPicked} schema${autoPicked === 1 ? '' : 's'} we can see — uncheck anything you'd rather keep private. Queries run read-only.`
                    : 'Queries run read-only on the SQL Warehouse you pick, limited to the schemas you select.'}
                </div>
              </div>

              <div>
                <div className="t-micro text-tertiary" style={{ marginBottom: 6 }}>SQL Warehouse</div>
                <select className="input" value={warehouseId} onChange={e => setWarehouseId(e.target.value)}
                  style={{ maxWidth: 360 }}>
                  <option value="">Select a warehouse…</option>
                  {warehouses.map(w => (
                    <option key={w.id} value={w.id}>{w.name || w.id}{w.state ? ` · ${w.state}` : ''}</option>
                  ))}
                </select>
                {warehousesError && (
                  <div className="t-small" style={{ color: 'var(--error)', marginTop: 6 }}>
                    {warehousesError}
                    {dbxIsPat(conn) && dbxLooksLikeAuthFailure(warehousesError) && (
                      <div className="text-secondary" style={{ marginTop: 4 }}>
                        Listing warehouses needs the <span className="mono-sm mono">sql</span> scope
                        on your access token. <DbxScopeRemedy/>
                      </div>
                    )}
                  </div>
                )}
              </div>

              <div>
                <div className="t-micro text-tertiary" style={{ marginBottom: 6 }}>
                  Catalogs & schemas {pickedList.length > 0 && <span>· {pickedList.length} selected</span>}
                </div>
                <div style={{ border: '1px solid var(--border-subtle)', borderRadius: 8, overflow: 'hidden' }}>
                  {listsLoading && (
                    <div className="t-small text-tertiary" style={{ padding: 14 }}>Loading catalogs…</div>
                  )}
                  {!listsLoading && catalogs.length === 0 && (
                    <div style={{ padding: 14 }}>
                      <div className="t-small" style={{ color: catalogsError ? 'var(--error)' : undefined }}>
                        {catalogsError || 'No catalogs visible to your user.'}
                      </div>
                      {/* Rejected vs empty are different diagnoses and must not be
                          told the same story: Databricks REFUSES a call the token
                          isn't scoped for, so an empty-but-successful list is far
                          more likely to be a user with no grants. Naming the wrong
                          one confidently is the exact failure this hint exists to
                          prevent, so the empty case names both and the error case
                          — only when it looks like an authorization failure —
                          commits. */}
                      {dbxIsPat(conn) && catalogsError && dbxLooksLikeAuthFailure(catalogsError) && (
                        <div className="t-small text-secondary" style={{ marginTop: 6 }}>
                          Listing catalogs needs the <span className="mono-sm mono">unity-catalog</span> scope
                          on your access token. <DbxScopeRemedy/>
                        </div>
                      )}
                      {dbxIsPat(conn) && !catalogsError && (
                        <div className="t-small text-secondary" style={{ marginTop: 6 }}>
                          Either your Databricks user has not been granted <span className="mono-sm mono">USE
                          CATALOG</span> on anything, or the access token was created without
                          the <span className="mono-sm mono">unity-catalog</span> scope. Check the grants
                          first — if they look right, <DbxScopeRemedy/>
                        </div>
                      )}
                    </div>
                  )}
                  {catalogs.map(c => {
                    const name = c.name || c;
                    const schemas = schemasByCatalog[name];
                    const isOpen = !!openCatalogs[name];
                    return (
                      <div key={name} style={{ borderBottom: '1px solid var(--border-subtle)' }}>
                        <div style={{ display: 'flex', alignItems: 'center' }}>
                          <button onClick={() => toggleCatalog(name)} style={{
                            flex: 1, textAlign: 'left', padding: '10px 14px',
                            display: 'flex', alignItems: 'center', gap: 8, background: 'transparent',
                          }}>
                            <Icons.ChevronD size={11} style={{
                              transform: isOpen ? 'none' : 'rotate(-90deg)', transition: 'transform 120ms',
                              color: 'var(--text-tertiary)',
                            }}/>
                            <span className="t-small" style={{ fontWeight: 500 }}>{name}</span>
                          </button>
                          {Array.isArray(schemas) && schemas.length > 0 && (
                            <button className="btn ghost sm" style={{ marginRight: 10 }}
                              onClick={() => toggleCatalogAll(name, schemas)}>
                              {catalogAllPicked(name, schemas) ? 'Clear' : 'All'}
                            </button>
                          )}
                        </div>
                        {isOpen && (
                          <div style={{ padding: '2px 14px 10px 33px', display: 'flex', flexDirection: 'column', gap: 4 }}>
                            {schemas === 'loading' && <span className="t-small text-tertiary">Loading…</span>}
                            {Array.isArray(schemas) && schemas.length === 0 && (
                              <span className="t-small text-tertiary">No schemas</span>
                            )}
                            {Array.isArray(schemas) && schemas.map(s => {
                              const sName = s.name || s;
                              const k = `${name}.${sName}`;
                              return (
                                <label key={k} className="t-small" style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>
                                  <input type="checkbox" checked={!!picked[k]} onChange={() => togglePick(name, sName)}/>
                                  <span>{sName}</span>
                                </label>
                              );
                            })}
                          </div>
                        )}
                      </div>
                    );
                  })}
                </div>
              </div>
            </div>
          )}

          {view === 'manage' && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
              {dbxNeedsReconnect(conn) && (
                <div className="t-small" style={{
                  padding: '8px 12px', borderRadius: 8,
                  border: '1px solid var(--warning)', color: 'var(--warning)',
                }}>
                  Databricks revoked this authorization. The agent can't query until you reconnect —
                  your warehouse and schema selection are kept.
                </div>
              )}
              <div>
                <div className="t-micro text-tertiary">Workspace</div>
                <div className="t-body" style={{ marginTop: 2 }}>
                  {(conn && (conn.workspace_host || (conn.settings && conn.settings.workspace_host))) || '—'}
                </div>
              </div>
              <div>
                <div className="t-micro text-tertiary">Agent can query</div>
                {pickedList.length === 0
                  ? <div className="t-small text-secondary" style={{ marginTop: 2 }}>No schemas selected yet.</div>
                  : (
                    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 6 }}>
                      {pickedList.map(s => {
                        const key = `${s.catalog}.${s.schema}`;
                        const st = schemaStates ? schemaStates[key.toLowerCase()] : null;
                        // A failed or cancelled job never resumes its queue,
                        // so "queued" would promise progress that isn't coming.
                        const meta = st === 'pending' && scan && ['failed', 'cancelled'].includes(scan.status)
                          ? { dot: 'paused', label: 'not scanned' }
                          : (st ? DBX_STATE_META[st] : null);
                        return (
                          <span key={key} className="chip"
                            style={{ padding: '2px 8px', display: 'inline-flex', alignItems: 'center', gap: 6 }}>
                            {meta && <StatusDot kind={meta.dot} size={6}/>}
                            {key}
                            {meta && <span className="t-micro text-tertiary">{meta.label}</span>}
                            {!scanRunning && !dbxNeedsReconnect(conn) && (
                              <button title={`Force rescan ${key}`} onClick={() => setRescanTarget(s)}
                                style={{
                                  background: 'none', border: 'none', cursor: 'pointer', padding: 0,
                                  lineHeight: 1, color: 'var(--text-tertiary)', fontSize: 12,
                                }}>↻</button>
                            )}
                          </span>
                        );
                      })}
                    </div>
                  )}
              </div>
              {rescanTarget && !scanRunning && (
                <div className="t-small" style={{
                  padding: '8px 12px', borderRadius: 8,
                  border: '1px solid var(--error)', color: 'var(--error)',
                }}>
                  Force rescan {rescanTarget.catalog}.{rescanTarget.schema}? This re-runs the full
                  LLM scan over the schema — it takes time and costs money. Only needed when the
                  source schema itself changed.
                  <div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
                    <button className="btn sm" onClick={doRescan} disabled={rescanBusy}
                      style={{ borderColor: 'var(--error)', color: 'var(--error)' }}>
                      {rescanBusy ? 'Starting…' : 'Rescan'}
                    </button>
                    <button className="btn sm ghost" onClick={() => setRescanTarget(null)} disabled={rescanBusy}>
                      Cancel
                    </button>
                  </div>
                </div>
              )}
              {scan && (
                <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                  <StatusDot kind={scan.status === 'failed' ? 'error' : ['done', 'cancelled'].includes(scan.status) ? 'live' : 'syncing'} size={6}/>
                  <span className="mono-sm mono text-secondary" style={{ flex: 1 }}>
                    {scan.status === 'done' ? 'Schema scan complete — tables are in the knowledge graph'
                      : scan.status === 'failed' ? `Schema scan failed${scan.error ? ': ' + scan.error : ''}`
                      : scan.status === 'cancelled' ? 'Schema scan cancelled'
                      : `Scanning schemas into the knowledge graph…${
                          stateValues.length ? ` (${scanDoneCount}/${stateValues.length})` : ''}`}
                  </span>
                  {['failed', 'cancelled'].includes(scan.status) && !dbxNeedsReconnect(conn) && (
                    <button className="btn sm" onClick={retryScan} disabled={saving}>
                      {saving ? 'Restarting…' : 'Retry scan'}
                    </button>
                  )}
                </div>
              )}
            </div>
          )}
        </div>

        {/* Footer */}
        <div style={{ padding: '14px 20px', borderTop: '1px solid var(--border-subtle)', display: 'flex', gap: 8 }}>
          {view === 'select' && (
            <React.Fragment>
              <button className="btn ghost" onClick={() => (pickedList.length ? setView('manage') : onClose())}>Cancel</button>
              <div style={{ flex: 1 }}/>
              <button className="btn accent" onClick={saveSelection} disabled={saving}>
                {saving ? 'Saving…' : `Save selection (${pickedList.length})`}
              </button>
            </React.Fragment>
          )}
          {view === 'manage' && (
            <React.Fragment>
              <button className="btn ghost" style={{ color: 'var(--error)' }} onClick={doDisconnect}>Disconnect</button>
              <div style={{ flex: 1 }}/>
              {dbxNeedsReconnect(conn)
                ? <button className="btn accent"
                    onClick={() => { setShowForm(!readyToAuthorize); setView('setup'); }}>
                    Reconnect
                  </button>
                : <button className="btn" onClick={enterSelect}>Edit selection</button>}
              <button className="btn accent" onClick={onClose}>Done</button>
            </React.Fragment>
          )}
          {(view === 'setup' || view === 'loading' || view === 'authorizing') && (
            <React.Fragment>
              <div style={{ flex: 1 }}/>
              <button className="btn ghost" onClick={onClose}>Close</button>
            </React.Fragment>
          )}
        </div>
      </div>
    </div>
  );
}

// dbxIsConnected / dbxNeedsReconnect are exported for the Connectors-page card
// (org_databricks.jsx) so both read connection state the same tolerant way.
Object.assign(window, {
  DatabricksDrawer,
  DatabricksAPI,
  dbxIsConnected,
  dbxNeedsReconnect,
});
