// ── Org (company) Google connectors — shared live data layer ────────────────
// One Google OAuth (Composio's umbrella "google" connection on the backend)
// authorizes the whole toolkit; Drive, Gmail and Calendar are separate sync
// PRODUCTS riding that single connection. The backend maps every product name
// to the "google" connection internally, so the three cards share connection
// state — connecting from any card connects all three, and disconnecting any
// revokes all three. What actually becomes org knowledge stays a per-product
// choice: each product runs its own sync → awaiting_selection → ingest flow,
// and nothing is ever ingested without an explicit selection.
//
// The client speaks the product name on every route (X = google_drive |
// google_gmail | google_calendar):
//
//   POST   /org/connectors/X/connect              → { redirect_url }
//   GET    /org/connectors/X/connection           → { is_active, status, sync_job }
//   POST   /org/connectors/X/sync                 → JobResponse (parks at awaiting_selection)
//   GET    /org/connectors/X/sync-items?job_id=…  → item picker list
//   POST   /org/connectors/X/sync/{job}/ingest    → ingest the selected items
//   DELETE /org/connectors/X/connection           → revoke the shared Google connection
//   DELETE /org/connectors/X/data                 → purge this product's ingested items
//
// 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 the product whose card started the connect) and
// auto-kicks that product's first sync, so after the popup the client only has
// to poll the connection until it reads active and a sync_job appears.

const GP_SYNC_ITEMS_LIMIT = 500;      // server clamps to 500 — one page covers most accounts
const GP_CONNECT_POLL_MS = 2000;      // poll cadence while the OAuth popup is open
const GP_CONNECT_TIMEOUT_MS = 180_000;
const GP_JOB_POLL_MS = 2500;          // poll cadence while a sync/ingest job runs
// Like Notion (and unlike Slack), the connection status round-trips Composio
// on the backend — keep the idle poll slow to keep that traffic modest.
const GP_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 GP_ITEMS_RETRY_MS = 5000;

// Announced whenever the shared Google connection is made or dropped, or a
// product's data changes. Every Google card listens (a connect from the Gmail
// card must flip the Drive and Calendar cards to connected at once instead of
// waiting out their 15s idle poll), and so does the Connectors hero strip.
const GOOGLE_CHANGED_EVENT = 'parcle:google-changed';

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

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

const OrgGoogleDriveAPI = makeOrgGoogleProductAPI('google_drive');
const OrgGmailAPI = makeOrgGoogleProductAPI('google_gmail');
const OrgGCalendarAPI = makeOrgGoogleProductAPI('google_calendar');

// ── Hook ────────────────────────────────────────────────────────────────────
// Single state object a Google product card renders from, mirroring
// useOrgNotion:
//   phase: 'loading' | 'disconnected' | 'connecting' | 'connected'
//   syncJob: latest JobResponse (or null) — status/progress/payload.counts
//   items / itemsJobId: item rows for the picker (awaiting_selection)
function useOrgGoogleProduct(api) {
  const { useState, useEffect, useRef, useCallback } = React;
  const [phase, setPhase] = useState('loading');
  const [syncJob, setSyncJob] = useState(null);
  const [items, setItems] = useState(null);
  // Server-side row count for the picker's job — larger than items.length when
  // the manifest outgrew the single page the client fetches (GP_SYNC_ITEMS_LIMIT).
  const [itemsTotal, setItemsTotal] = 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 api.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 ? GP_JOB_POLL_MS : GP_IDLE_POLL_MS);
    };
    tick();
    return () => { stopped = true; if (timer) clearTimeout(timer); };
  }, [refresh]);

  // The three product cards share one Google connection: when a sibling card
  // announces a connect/disconnect, catch up now instead of a poll later.
  useEffect(() => {
    const onChanged = () => { refresh().catch(() => {}); };
    window.addEventListener(GOOGLE_CHANGED_EVENT, onChanged);
    return () => window.removeEventListener(GOOGLE_CHANGED_EVENT, onChanged);
  }, [refresh]);

  // When the latest job parks at awaiting_selection, load its item manifest.
  // A failed read schedules its own retry (see GP_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;
    api.syncItems(job.id).then(page => {
      if (cancelled || !alive.current) return;
      setItems(page.items || []);
      setItemsTotal(typeof page.total === 'number' ? page.total : null);
      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);
      }, GP_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-google-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 Google…</p>');
    try {
      const { redirect_url } = await api.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 Google leaves no state either, so `cancelConnect`
      // (flipping connecting.current) is the client-side way out of the wait.
      const deadline = Date.now() + GP_CONNECT_TIMEOUT_MS;
      while (Date.now() < deadline && connecting.current) {
        await new Promise(r => setTimeout(r, GP_CONNECT_POLL_MS));
        if (!alive.current) return;
        if (!connecting.current) return; // cancelled while sleeping
        const s = await api.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);
          }
          gpAnnounceChange();
          return;
        }
      }
      if (!connecting.current) return; // cancelled — cancelConnect already reset the UI
      connecting.current = false;
      setPhase('disconnected');
      setError('Google 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 Google 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 api.sync();
      if (!alive.current) return;
      setItems(null); setItemsTotal(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 api.ingest(syncJob.id, selection);
      if (!alive.current) return;
      setItems(null); setItemsTotal(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 api.disconnect();
      if (!alive.current) return;
      setItems(null); setItemsTotal(null); setItemsJobId(null); setSyncJob(null);
      setPhase('disconnected');
      gpAnnounceChange();
    } catch (e) { if (alive.current) setError(String(e.message || e)); }
  }, []);

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

Object.assign(window, {
  OrgGoogleDriveAPI, OrgGmailAPI, OrgGCalendarAPI,
  useOrgGoogleProduct, GOOGLE_CHANGED_EVENT,
});
