// ── Org (company) Notion connector — live data layer ────────────────────────
// Second real connector on the org console, riding the same generic org
// surface as Slack. Notion authorizes through a Composio OAuth link instead of
// a Slack-style login/start route, so connect differs; everything after the
// connection (sync → awaiting_selection → pick pages → ingest) is the same
// engine and the same endpoints:
//
//   POST   /org/connectors/notion/connect              → { redirect_url }
//   GET    /org/connectors/notion/connection           → { is_active, status, sync_job }
//   POST   /org/connectors/notion/sync                 → JobResponse (parks at awaiting_selection)
//   GET    /org/connectors/notion/sync-items?job_id=…  → page/database picker list
//   POST   /org/connectors/notion/sync/{job}/ingest    → ingest the selected pages
//   DELETE /org/connectors/notion/connection           → revoke the Composio account + clear record
//   DELETE /org/connectors/notion/data                 → purge ingested pages
//
// The signed-in console session IS the org account (org-only console), so no
// target_account_id is sent. The OAuth completes on the backend's session-less
// Composio callback (the signed state in the link's callback URL carries the
// org account id) and auto-kicks the first sync, so after the popup the client
// only has to poll the connection until it reads active and a sync_job appears.

const ON_SYNC_ITEMS_LIMIT = 500;      // server clamps to 500 — one page covers a workspace
const ON_CONNECT_POLL_MS = 2000;      // poll cadence while the OAuth popup is open
const ON_CONNECT_TIMEOUT_MS = 180_000;
const ON_JOB_POLL_MS = 2500;          // poll cadence while a sync/ingest job runs
// Unlike Slack's status (a local record read), Notion's connection status
// round-trips Composio on the backend — so the idle poll is deliberately
// slower than Slack's to keep that traffic modest.
const ON_IDLE_POLL_MS = 15_000;
// A failed sync-items read while the job sits at awaiting_selection would
// otherwise strand the card (no picker, and the connected-state buttons are
// hidden while awaiting) — retry on this cadence until the manifest loads.
const ON_ITEMS_RETRY_MS = 5000;

// Announced whenever the Notion connection itself is made or dropped, so other
// live surfaces on the Connectors page (the hero stats strip) reflect it at
// once instead of waiting out their own poll.
const ON_CHANGED_EVENT = 'parcle:notion-changed';

function onAnnounceChange() {
  try { window.dispatchEvent(new CustomEvent(ON_CHANGED_EVENT)); }
  catch (e) { /* very old browser — the listeners' polls still catch up */ }
}

const OrgNotionAPI = {
  connect()  { return osApi('POST', '/org/connectors/notion/connect', { settings: {} }); },
  status()   { return osApi('GET', '/org/connectors/notion/connection'); },
  sync()     { return osApi('POST', '/org/connectors/notion/sync', { settings: {}, auto_ingest: false }); },
  syncItems(jobId) {
    const params = new URLSearchParams({ limit: String(ON_SYNC_ITEMS_LIMIT), offset: '0' });
    if (jobId) params.set('job_id', String(jobId));
    return osApi('GET', `/org/connectors/notion/sync-items?${params.toString()}`);
  },
  ingest(jobId, { itemIds = null, selectAll = false, exclude = null } = {}) {
    return osApi('POST', `/org/connectors/notion/sync/${encodeURIComponent(jobId)}/ingest`,
      { item_ids: itemIds, select_all: selectAll, exclude });
  },
  disconnect() { return osApi('DELETE', '/org/connectors/notion/connection'); },
  purgeData()  { return osApi('DELETE', '/org/connectors/notion/data'); },
};

// ── Hook ────────────────────────────────────────────────────────────────────
// Single state object the card renders from, mirroring useOrgSlack:
//   phase: 'loading' | 'disconnected' | 'connecting' | 'connected'
//   syncJob: latest JobResponse (or null) — status/progress/payload.counts
//   items / itemsJobId: page rows for the picker (awaiting_selection)
function useOrgNotion() {
  const { useState, useEffect, useRef, useCallback } = React;
  const [phase, setPhase] = useState('loading');
  const [syncJob, setSyncJob] = useState(null);
  const [items, setItems] = useState(null);
  const [itemsJobId, setItemsJobId] = useState(null);
  // Bumped after a failed sync-items read to re-run the loader effect.
  const [itemsRetry, setItemsRetry] = useState(0);
  const [error, setError] = useState(null);
  const alive = useRef(true);
  // While the OAuth popup is open the connection reads inactive — the
  // background poll must not flip the card out of its 'connecting' state.
  const connecting = useRef(false);
  useEffect(() => () => { alive.current = false; }, []);

  const refresh = useCallback(async () => {
    const s = await OrgNotionAPI.status();
    if (!alive.current) return null;
    if (s.is_active) setPhase('connected');
    else if (!connecting.current) setPhase('disconnected');
    setSyncJob(s.sync_job || null);
    return s;
  }, []);

  // Initial load + poll while a job is running (discover/export/ingest all
  // report through the job the connection endpoint already returns).
  useEffect(() => {
    let timer = null;
    let stopped = false;
    const tick = async () => {
      if (stopped) return;
      let s = null;
      try { s = await refresh(); } catch (e) { if (alive.current) setError(String(e.message || e)); }
      if (stopped) return;
      const again = s && osJobActive(s.sync_job);
      timer = setTimeout(tick, again ? ON_JOB_POLL_MS : ON_IDLE_POLL_MS);
    };
    tick();
    return () => { stopped = true; if (timer) clearTimeout(timer); };
  }, [refresh]);

  // When the latest job parks at awaiting_selection, load its page manifest.
  // A failed read schedules its own retry (see ON_ITEMS_RETRY_MS): without it
  // the effect's inputs never change again and the card would strand with an
  // error but no picker and no way out short of a page reload.
  useEffect(() => {
    const job = syncJob;
    if (!job || job.status !== 'awaiting_selection') return;
    if (itemsJobId === String(job.id) && items) return;
    let cancelled = false;
    let retryTimer = null;
    OrgNotionAPI.syncItems(job.id).then(page => {
      if (cancelled || !alive.current) return;
      setItems(page.items || []);
      setItemsJobId(String(job.id));
    }).catch(e => {
      if (cancelled || !alive.current) return;
      setError(String(e.message || e));
      retryTimer = setTimeout(() => {
        if (!cancelled && alive.current) setItemsRetry(n => n + 1);
      }, ON_ITEMS_RETRY_MS);
    });
    return () => { cancelled = true; if (retryTimer) clearTimeout(retryTimer); };
  }, [syncJob && syncJob.id, syncJob && syncJob.status, itemsRetry]);

  const connect = useCallback(async () => {
    setError(null);
    connecting.current = true;
    setPhase('connecting');
    // Open synchronously while this callback still runs inside the user's
    // click gesture. Opening after awaiting connect() is commonly blocked
    // by browsers because it is then classified as an unsolicited popup.
    const popup = window.open('', 'parcle-notion-oauth', 'width=560,height=720');
    if (!popup) {
      connecting.current = false;
      setPhase('disconnected');
      setError('Popup blocked — allow popups for this site and try again.');
      return;
    }
    popup.document.write('<p style="font:14px system-ui;padding:24px">Opening Notion…</p>');
    try {
      const { redirect_url } = await OrgNotionAPI.connect();
      popup.location = redirect_url;
      // The popup may be COOP-isolated — never read its closed state; the
      // backend connection state is the only authoritative signal. A user who
      // cancelled inside Notion leaves no state either, so `cancelConnect`
      // (flipping connecting.current) is the client-side way out of the wait.
      const deadline = Date.now() + ON_CONNECT_TIMEOUT_MS;
      while (Date.now() < deadline && connecting.current) {
        await new Promise(r => setTimeout(r, ON_CONNECT_POLL_MS));
        if (!alive.current) return;
        if (!connecting.current) return; // cancelled while sleeping
        const s = await OrgNotionAPI.status().catch(() => null);
        if (s && s.is_active) {
          connecting.current = false;
          try { popup.close(); } catch (e) {}
          setPhase('connected');
          setSyncJob(s.sync_job || null);
          // The callback confirms the connection BEFORE it submits the first
          // sync, so this read can land in between and miss the job — one
          // short follow-up catches it instead of waiting out the idle poll.
          if (!s.sync_job) {
            setTimeout(() => { if (alive.current) refresh().catch(() => {}); }, 3000);
          }
          onAnnounceChange();
          return;
        }
      }
      if (!connecting.current) return; // cancelled — cancelConnect already reset the UI
      connecting.current = false;
      setPhase('disconnected');
      setError('Notion authorization timed out — try again.');
    } catch (e) {
      try { popup.close(); } catch (closeErr) {}
      connecting.current = false;
      if (!alive.current) return;
      setPhase('disconnected');
      setError(String(e.message || e));
    }
  }, [refresh]);

  // Bail out of a pending OAuth wait (e.g. the user closed the Notion window
  // without authorizing — the backend records nothing in that case, so the
  // client can't detect it and must offer a manual way back).
  const cancelConnect = useCallback(() => {
    if (!connecting.current) return;
    connecting.current = false;
    setPhase('disconnected');
  }, []);

  const syncNow = useCallback(async () => {
    setError(null);
    try {
      const job = await OrgNotionAPI.sync();
      if (!alive.current) return;
      setItems(null); setItemsJobId(null);
      setSyncJob(job);
    } catch (e) { if (alive.current) setError(String(e.message || e)); }
  }, []);

  const ingest = useCallback(async (selection) => {
    if (!syncJob) return;
    setError(null);
    try {
      const job = await OrgNotionAPI.ingest(syncJob.id, selection);
      if (!alive.current) return;
      setItems(null); setItemsJobId(null);
      setSyncJob(job);
    } catch (e) { if (alive.current) setError(String(e.message || e)); }
  }, [syncJob && syncJob.id]);

  const disconnect = useCallback(async () => {
    setError(null);
    try {
      await OrgNotionAPI.disconnect();
      if (!alive.current) return;
      setItems(null); setItemsJobId(null); setSyncJob(null);
      setPhase('disconnected');
      onAnnounceChange();
    } catch (e) { if (alive.current) setError(String(e.message || e)); }
  }, []);

  return { phase, syncJob, items, error, connect, cancelConnect, syncNow, ingest, disconnect, refresh };
}

Object.assign(window, { OrgNotionAPI, useOrgNotion, ON_CHANGED_EVENT });
