// ── Org (company) Slack connector — live data layer ─────────────────────────
// The org console's first REAL connector, against the org-scoped backend
// surface:
//
//   POST   /org/connectors/slack/login/start          → { authorize_url }
//   GET    /org/connectors/slack/connection           → { is_active, status, sync_job }
//   POST   /org/connectors/slack/sync                 → JobResponse (parks at awaiting_selection)
//   GET    /org/connectors/slack/sync-items?job_id=…  → channel picker list
//   POST   /org/connectors/slack/sync/{job}/ingest    → ingest the selected channels
//   DELETE /org/connectors/slack/connection           → revoke token + clear record
//   DELETE /org/connectors/slack/data                 → purge ingested channels
//
// The signed-in console session IS the org account (org-only console), so no
// target_account_id is sent — the backend defaults the scope target to the
// caller. Slack OAuth completes on the backend's session-less callback (the
// signed state 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 OS_SYNC_ITEMS_LIMIT = 500;      // server clamps to 500 — one page covers a workspace
const OS_CONNECT_POLL_MS = 2000;      // poll cadence while the OAuth popup is open
const OS_CONNECT_TIMEOUT_MS = 180_000;
const OS_JOB_POLL_MS = 2000;          // poll cadence while a sync/ingest job runs

// Announced whenever the Slack 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 OS_CHANGED_EVENT = 'parcle:slack-changed';

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

function osLive() {
  try { return !!(typeof parcleApiBase === 'function' && parcleApiBase()); }
  catch (e) { return false; }
}

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

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

// A sync job is still moving while in one of these states; 'awaiting_selection'
// is a stable resting state (the picker), 'done'/'failed' are terminal.
function osJobActive(job) {
  return !!job && !['done', 'failed', 'cancelled', 'awaiting_selection'].includes(job.status);
}

// ── Hook ────────────────────────────────────────────────────────────────────
// Single state object the card renders from:
//   phase: 'loading' | 'disconnected' | 'connecting' | 'connected'
//   syncJob: latest JobResponse (or null) — status/progress/payload.counts
//   items / itemsJobId: channel rows for the picker (awaiting_selection)
function useOrgSlack() {
  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);
  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 OrgSlackAPI.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 ? OS_JOB_POLL_MS : OS_JOB_POLL_MS * 5);
    };
    tick();
    return () => { stopped = true; if (timer) clearTimeout(timer); };
  }, [refresh]);

  // When the latest job parks at awaiting_selection, load its channel manifest.
  useEffect(() => {
    const job = syncJob;
    if (!job || job.status !== 'awaiting_selection') return;
    if (itemsJobId === String(job.id) && items) return;
    let cancelled = false;
    OrgSlackAPI.syncItems(job.id).then(page => {
      if (cancelled || !alive.current) return;
      setItems(page.items || []);
      setItemsJobId(String(job.id));
    }).catch(e => { if (!cancelled && alive.current) setError(String(e.message || e)); });
    return () => { cancelled = true; };
  }, [syncJob && syncJob.id, syncJob && syncJob.status]);

  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 startLogin() is commonly blocked
    // by browsers because it is then classified as an unsolicited popup.
    const popup = window.open('', 'parcle-slack-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 Slack…</p>');
    try {
      const { authorize_url } = await OrgSlackAPI.startLogin();
      popup.location = authorize_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 Slack leaves no state either, so `cancelConnect`
      // (flipping connecting.current) is the client-side way out of the wait.
      const deadline = Date.now() + OS_CONNECT_TIMEOUT_MS;
      while (Date.now() < deadline && connecting.current) {
        await new Promise(r => setTimeout(r, OS_CONNECT_POLL_MS));
        if (!alive.current) return;
        if (!connecting.current) return; // cancelled while sleeping
        const s = await OrgSlackAPI.status().catch(() => null);
        if (s && s.is_active) {
          connecting.current = false;
          try { popup.close(); } catch (e) {}
          setPhase('connected');
          setSyncJob(s.sync_job || null);
          osAnnounceChange();
          return;
        }
      }
      if (!connecting.current) return; // cancelled — cancelConnect already reset the UI
      connecting.current = false;
      setPhase('disconnected');
      setError('Slack 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));
    }
  }, []);

  // Bail out of a pending OAuth wait (e.g. the user closed the Slack 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 OrgSlackAPI.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 OrgSlackAPI.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 OrgSlackAPI.disconnect();
      if (!alive.current) return;
      setItems(null); setItemsJobId(null); setSyncJob(null);
      setPhase('disconnected');
      osAnnounceChange();
    } catch (e) { if (alive.current) setError(String(e.message || e)); }
  }, []);

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

// osApi is exported for the other org connector data layers (Notion) that
// speak to the same org backend surface with the same auth/error handling.
Object.assign(window, { osApi, osLive, osJobActive, OrgSlackAPI, useOrgSlack, OS_CHANGED_EVENT });
