import { useGlobal } from './useGlobal';
let theToken: string | null = null;

export const createClient = (baseUrl: string = '/api') => {
  const lsToken =
    window.officerBearerToken ||
    document.body.dataset['officerBearerToken'] ||
    document.body.dataset['bearerToken'] ||
    new URL(window.location.href).searchParams.get('officerToken') ||
    localStorage.getItem('PERTENTO_EDITOR_AUTH_TOKEN') ||
    localStorage.getItem('BEARER_TOKEN') ||
    sessionStorage.getItem('BEARER_TOKEN');

  theToken = lsToken;

  return {
    baseUrl,
    token: theToken,
    get: <T>(url: string) => get<T>(url, baseUrl),
    getText: (url: string) => getText(url, baseUrl),
    getBlob: (url: string) => getBlob(url, baseUrl),
    // getStream: (url) => getStream(url, baseUrl),
    post: <T>(url: string, payload?: any) => post<T>(url, payload, baseUrl),
    put: <T>(url: string, payload?: any) => put<T>(url, payload, baseUrl),
    patch: <T>(url: string, payload?: any) => patch<T>(url, payload, baseUrl),
    delete: <T>(url: string, payload?: any) => DELETE<T>(url, payload, baseUrl),
  };
};

export const useClient = (baseUrl: string = '/api') => {
  const [apiError, setApiError] = useGlobal<ErrorDetails | null>('API_ERROR', null);
  useClient.config.onError = (error) => {
    setApiError(error);
  };

  const client = createClient(baseUrl);
  return { ...client, apiError, setApiError };
};

export const getHeaders = (isText: boolean = false) => {
  const headers: Record<string, string> = {};

  if (theToken) {
    headers['Authorization'] = `Bearer ${theToken}`;
  }

  headers['Content-Type'] = isText ? 'text/plain' : 'application/json';

  if (isText) {
    headers['Accept'] = 'text/plain';
  }

  return headers;
};

// 2xx responses can legitimately carry no body (201 Created / 204 No Content). res.json() throws on an
// empty body, so read text first and only parse when there's something — otherwise resolve undefined.
const parseBody = async <T>(res: Response): Promise<T> => {
  const text = await res.text();
  return (text ? JSON.parse(text) : undefined) as T;
};

export const getText = async (uri: string, baseUrl = '') => {
  const headers = getHeaders(true);
  const theUrl = baseUrl ? `${baseUrl}/${uri.replace(/^\//, '')}` : uri;
  const res = await fetch(theUrl, { headers });
  await validateResponse(res);
  const text = await res.text();
  return text;
};

export const get = async <T>(uri: string, baseUrl = '') => {
  const headers = getHeaders();
  const theUrl = baseUrl ? `${baseUrl}/${uri.replace(/^\//, '')}` : uri;
  const res = await fetch(theUrl, { headers });
  await validateResponse(res);
  return parseBody<T>(res);
};

export const getBlob = async (uri: string, baseUrl = '') => {
  const headers = { Authorization: `Bearer ${theToken!}` };
  const theUrl = baseUrl ? `${baseUrl}/${uri.replace(/^\//, '')}` : uri;
  const res = await fetch(theUrl, { headers });
  await validateResponse(res);
  const blob = await res.blob();
  return blob;
};

// export const getStream = (uri: string, baseUrl = '') => {
//   const headers = getHeaders() as EventSourceInit;
//   const theUrl = baseUrl ? `${baseUrl}/${uri.replace(/^\//, '')}` : uri;
//   const stream = new EventSource(theUrl, { headers });
//   return stream;
// };

export const post = async <T>(uri: string, payload?: any, baseUrl = '') => {
  const headers: Record<string, string> = getHeaders();
  const theUrl = baseUrl ? `${baseUrl}/${uri.replace(/^\//, '')}` : uri;
  const body = payload instanceof FormData ? payload : JSON.stringify(payload);
  if (payload instanceof FormData) {
    delete headers['Content-Type'];
  }
  const res = await fetch(theUrl, {
    method: 'post',
    body,
    headers,
  });
  await validateResponse(res);
  return parseBody<T>(res);
};

export const put = async <T>(uri: string, payload?: any, baseUrl = '') => {
  const headers: any = getHeaders();
  const theUrl = baseUrl ? `${baseUrl}/${uri.replace(/^\//, '')}` : uri;
  const body = payload instanceof FormData ? payload : JSON.stringify(payload);

  if (payload instanceof FormData) {
    delete headers['Content-Type'];
  }

  const res = await fetch(theUrl, {
    method: 'put',
    body: body,
    headers,
  });
  await validateResponse(res);
  return parseBody<T>(res);
};

export const patch = async <T>(uri: string, payload?: any, baseUrl = '') => {
  const headers = getHeaders();
  const theUrl = baseUrl ? `${baseUrl}/${uri.replace(/^\//, '')}` : uri;
  const res = await fetch(theUrl, {
    method: 'PATCH',
    body: JSON.stringify(payload),
    headers,
  });
  await validateResponse(res);
  return parseBody<T>(res);
};

export const DELETE = async <T>(uri: string, payload?: any, baseUrl = '') => {
  const headers = getHeaders();
  const theUrl = baseUrl ? `${baseUrl}/${uri.replace(/^\//, '')}` : uri;
  const res = await fetch(theUrl, {
    method: 'delete',
    body: JSON.stringify(payload),
    headers,
  });
  await validateResponse(res);
  return parseBody<T>(res);
};

/** Thrown by every verb on a 4xx/5xx. A real `Error` subclass, because ~33 call sites narrow with
 * `err instanceof Error ? err.message : <fallback>` — against the plain object this used to throw that test
 * was always false, so every one of them showed its generic fallback and discarded what the server said.
 * Keeps `status` and `message`, so code reading either is unaffected. */
export class ApiError extends Error {
  constructor(
    public status: number,
    message: string,
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

/**
 * A 401 on an authenticated call means the token is no longer good — deleted account, blocked account,
 * changed password, explicit signout elsewhere. Drop it so the app stops presenting a session that is over.
 *
 * Until this existed, nothing in the client reacted to a 401 at all: `onError` fed the bug-report form and
 * that was the end of it. So an account deleted from the dashboard kept its window rendering happily off
 * cached React Query data, and a refresh looked like a live session. Observed 2026-08-11.
 *
 * ── The reload loop this caused, and the three guards that make it impossible ──
 *
 * The first version ended with `location.replace('/')`, guarded by "unless the path starts with /signin".
 * There is no /signin route: the sign-in screen IS `path="/"`. So every 401 on the signed-out landing page
 * navigated to the page it was already on, which fetched again, which 401'd again — a hard refresh loop with
 * no way out of the tab. Shipped and hit within the hour.
 *
 * The reload is not what fixes anything — `useAuth` already renders the sign-in screen when there is no
 * token. It exists only to drop a stale query cache. So it is now the last thing tried, and bounded three
 * separate ways, any ONE of which breaks a loop on its own:
 *
 *   1. no token → return. A 401 while already signed out is expected, not a revocation. This alone ends the
 *      loop, because the reloaded document has nothing to clear.
 *   2. once per document, via a module flag.
 *   3. once per TAB, via a sessionStorage marker — which also covers a host that re-injects the token on
 *      every load (`window.officerBearerToken`, `document.body.dataset`), where clearing storage cannot help
 *      and guard 1 would not fire.
 *
 * `/auth/…` stays exempt, and that exemption is load-bearing rather than tidy: a wrong password IS a 401,
 * and reloading the sign-in page in response would wipe the form and look like a crash.
 */
let revocationHandled = false;
const RELOAD_MARKER = 'officer:session-revoked-reload';

const handleRevokedSession = (url: string) => {
  if (url.includes('/auth/')) return;
  // Guard 1, and the important one: nothing to revoke means this 401 is the signed-out app asking a
  // question it was always going to be refused.
  if (!theToken) return;
  // Guard 2.
  if (revocationHandled) return;
  revocationHandled = true;

  theToken = null;
  // Every key `createClient` reads, or the next load signs straight back in with the dead token.
  for (const key of ['PERTENTO_EDITOR_AUTH_TOKEN', 'BEARER_TOKEN']) {
    localStorage.removeItem(key);
    sessionStorage.removeItem(key);
  }
  window.officerBearerToken = undefined;

  // Guard 3. Set BEFORE the reload and read after, so a token-injecting host reloads at most once ever.
  try {
    if (sessionStorage.getItem(RELOAD_MARKER)) return;
    sessionStorage.setItem(RELOAD_MARKER, '1');
  } catch {
    // Private mode with storage disabled: skip the reload entirely rather than risk an unbounded one.
    return;
  }
  window.location.reload();
};

const validateResponse = async (res: Response) => {
  if (res.status >= 400) {
    const { onError } = useClient.config;
    const message = await res.text();
    if (onError) {
      onError({ status: res.status, message });
    }
    if (res.status === 401) handleRevokedSession(res.url);
    throw new ApiError(res.status, message);
  }
};

export interface ErrorDetails {
  status: number;
  message: string;
}

interface UseClientConfig {
  onError: (error: ErrorDetails) => void;
}

// Assign the interface to the config property of useClient
useClient.config = {
  onError: () => null,
} as UseClientConfig;
