/* global React, TABLES */
// Client side of the claim system — talks to the Cloudflare Worker.
// Exposes:
//   window.AS_CLAIMS = {
//     fetchClaims(): Promise<Record<tableId, ClaimPatch>>
//     fetchMe(): Promise<{ session, editableTables } | null>
//     putClaim(tableId, body): Promise<Claim>
//     startOauth(provider, { table?, handleHint? }): void
//     logout(): Promise<void>
//     workerOrigin(): string | ''
//     applyClaimsToTables(tables, claims): void  // mutates in place
//   }
//
// Also exposes a React hook `useClaims()` and the `ClaimsProvider` via window.AS
// so React components don't need to know about the transport layer.

const { createContext, useContext, useEffect, useState, useCallback } = React;

const WORKER = () => (window.AS_CONFIG && window.AS_CONFIG.WORKER_ORIGIN) || '';

async function apiGet(path) {
  const origin = WORKER();
  if (!origin) return null;
  const res = await fetch(origin + path, { credentials: 'include' });
  if (!res.ok) return null;
  return res.json();
}

async function apiPut(path, body) {
  const origin = WORKER();
  if (!origin) throw new Error('WORKER_ORIGIN not configured');
  const res = await fetch(origin + path, {
    method: 'PUT',
    credentials: 'include',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body),
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(data.error || `PUT ${path} failed (${res.status})`);
  return data.claim;
}

async function apiPost(path, body) {
  const origin = WORKER();
  if (!origin) throw new Error('WORKER_ORIGIN not configured');
  const res = await fetch(origin + path, {
    method: 'POST',
    credentials: 'include',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body),
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(data.error || `POST ${path} failed (${res.status})`);
  return data;
}

async function apiDelete(path) {
  const origin = WORKER();
  if (!origin) return;
  await fetch(origin + path, { method: 'DELETE', credentials: 'include' });
}

function fetchClaims() {
  return apiGet('/claims').then(d => (d && d.claims) || {});
}
function fetchMe() {
  return apiGet('/me');
}
function putClaim(tableId, body) {
  return apiPut(`/claims/${encodeURIComponent(tableId)}`, body);
}
function submitManualClaim(body) {
  return apiPost('/manual-claims', body).then(d => d.request);
}
function logout() { return apiDelete('/me'); }
function workerOrigin() { return WORKER(); }

/**
 * Start an OAuth round-trip in a small browser window. The worker redirects the
 * popup back to this app with `?claim=...&result=...`; that page then notifies
 * the original tab to refresh `/me` and closes itself.
 */
function startOauth(provider, opts = {}) {
  const origin = WORKER();
  if (!origin) return;
  const params = new URLSearchParams();
  const returnUrl = new URL(window.location.href);
  returnUrl.searchParams.set('claim_popup', '1');
  params.set('return_to', returnUrl.toString());
  if (opts.table) params.set('table', opts.table);
  if (opts.handleHint && provider === 'bsky') params.set('handle', opts.handleHint);
  const href = `${origin}/oauth/${provider}/start?${params}`;
  const popup = window.open(
    href,
    `alleysearcher-${provider}-oauth`,
    'popup=yes,width=560,height=720,menubar=no,toolbar=no,location=yes,status=no'
  );
  if (!popup) window.location.href = href;
  else popup.focus?.();
}

/**
 * Overlays claim data on top of the in-memory TABLES. Only fields the artist
 * submitted are overwritten; the rest (id, geometry, type) are untouched.
 */
function applyClaimsToTables(tables, claims) {
  if (!claims) return;
  for (const t of tables) {
    const c = claims[t.id];
    if (!c) continue;
    const a = t.artists[0];
    if (c.displayName) a.name = c.displayName;
    if (Array.isArray(c.fandoms))  a.fandoms  = c.fandoms;
    if (Array.isArray(c.products)) a.niches   = c.products;
    if ('catalogUrl' in c) {
      t.hasCatalog = !!c.catalogUrl;
      t.catalogPostUrl = c.catalogUrl || '';
    }
    if (c.socials && typeof c.socials === 'object') {
      a.socials = { ...(a.socials || {}), ...c.socials };
    }
    // Remember which handle verified this claim so the detail panel can
    // highlight / link the verified social account.
    if (c.verifiedHandle && c.verifiedVia) {
      a.socials = { ...(a.socials || {}), [c.verifiedVia]: c.verifiedHandle };
    }
  }
}

// --- admin helpers (operator only; bearer-token auth) ----------------------
async function adminFetch(path, token, init = {}) {
  const origin = WORKER();
  if (!origin) throw new Error('WORKER_ORIGIN not configured');
  const headers = { ...(init.headers || {}) };
  if (token) headers.authorization = `Bearer ${token}`;
  const res = await fetch(origin + path, {
    ...init,
    credentials: 'include',
    headers,
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(data.error || `${init.method || 'GET'} ${path} failed (${res.status})`);
  return data;
}

window.AS_CLAIMS = {
  fetchClaims, fetchMe, putClaim, logout, startOauth, workerOrigin,
  submitManualClaim,
  applyClaimsToTables,
  adminListClaims:   (token) => adminFetch('/admin/claims', token).then(d => d.claims || {}),
  adminListManualClaims: (token) => adminFetch('/admin/manual-claims', token).then(d => d.requests || []),
  adminDeleteManualClaim: (token, id) => adminFetch(
    `/admin/manual-claims/${encodeURIComponent(id)}`, token, { method: 'DELETE' }
  ),
  adminGetDirectory: (token) => adminFetch('/admin/directory', token).then(d => d.directory || {}),
  adminPutDirectory: (token, dir) => adminFetch('/admin/directory', token, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(dir),
  }),
  adminDeleteClaim: (token, tableId) => adminFetch(
    `/admin/claims/${encodeURIComponent(tableId)}`, token, { method: 'DELETE' }
  ),
  adminPutClaim: (token, tableId, body) => adminFetch(`/admin/claims/${encodeURIComponent(tableId)}`, token, {
    method: 'PUT',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body),
  }).then(d => d.claim),
};

// --- React glue -------------------------------------------------------------

const ClaimsCtx = createContext(null);

function ClaimsProvider({ children }) {
  const [me, setMe] = useState(null);     // { session, editableTables } | null
  const [claims, setClaims] = useState(null); // { [tableId]: claim } | null
  const [loading, setLoading] = useState(true);

  const refresh = useCallback(async () => {
    const [meResult, claimsResult] = await Promise.allSettled([fetchMe(), fetchClaims()]);
    const m = meResult.status === 'fulfilled' ? meResult.value : null;
    const c = claimsResult.status === 'fulfilled' ? claimsResult.value : {};
    setMe(m);
    setClaims(c || {});
    if (Array.isArray(window.TABLES) && c) {
      applyClaimsToTables(window.TABLES, c);
    }
  }, []);

  useEffect(() => {
    let alive = true;
    refresh().finally(() => { if (alive) setLoading(false); });
    return () => { alive = false; };
  }, [refresh]);

  // When returning from OAuth, the worker redirects back with ?claim=…&result=…
  // We auto-refresh to pick up the new session, then strip the query so the
  // URL is clean for sharing.
  useEffect(() => {
    const onMessage = (e) => {
      if (e.origin !== window.location.origin) return;
      if (e.data?.type === 'alleysearcher:oauth-complete') refresh();
    };
    window.addEventListener('message', onMessage);
    return () => window.removeEventListener('message', onMessage);
  }, [refresh]);

  useEffect(() => {
    const u = new URL(window.location.href);
    if (u.searchParams.has('claim')) {
      const result = u.searchParams.get('result');
      if (result === 'ok') refresh();
      if (u.searchParams.get('claim_popup') === '1' && window.opener) {
        window.opener.postMessage({
          type: 'alleysearcher:oauth-complete',
          provider: u.searchParams.get('claim'),
          result,
          reason: u.searchParams.get('reason'),
        }, window.location.origin);
        window.close();
      }
      u.searchParams.delete('claim');
      u.searchParams.delete('result');
      u.searchParams.delete('handle');
      u.searchParams.delete('reason');
      u.searchParams.delete('claim_popup');
      window.history.replaceState(null, '', u.toString());
    }
  }, [refresh]);

  const value = {
    me, claims, loading,
    refresh,
    workerReady: !!WORKER(),
    async save(tableId, patch) {
      const updated = await putClaim(tableId, patch);
      setClaims(prev => ({ ...(prev || {}), [tableId]: updated }));
      applyClaimsToTables(window.TABLES, { [tableId]: updated });
      return updated;
    },
    async signOut() {
      await logout();
      setMe(null);
    },
    startOauth,
  };

  return <ClaimsCtx.Provider value={value}>{children}</ClaimsCtx.Provider>;
}

function useClaims() {
  return useContext(ClaimsCtx);
}

window.AS = Object.assign(window.AS || {}, { ClaimsProvider, useClaims });
