export const DEFAULT_API = "http://127.0.0.1:8000";
export const API_STORAGE_KEY = "stateHubApiBase";
const API_QUERY_PARAMS = ["api_base", "apiBase"];

function cleanApiBase(value) {
  if (typeof value !== "string") return null;
  const cleaned = value.trim().replace(/\/+$/, "");
  return cleaned || null;
}

function getStorageApiBase(storage) {
  if (!storage?.getItem) return null;
  try {
    return cleanApiBase(storage.getItem(API_STORAGE_KEY));
  } catch {
    return null;
  }
}

function urlFromLocation(location) {
  if (!location) return null;
  try {
    return new URL(location.href ?? String(location));
  } catch {
    return null;
  }
}

function getQueryApiBase(url) {
  if (!url) return null;
  for (const name of API_QUERY_PARAMS) {
    const value = cleanApiBase(url.searchParams.get(name));
    if (value) return value;
  }
  return null;
}

function inferApiBase(url) {
  if (!url || !["http:", "https:"].includes(url.protocol)) return DEFAULT_API;
  if (url.hostname === "::1" || url.hostname === "[::1]") return DEFAULT_API;
  const apiUrl = new URL(url.href);
  apiUrl.port = globalThis.STATE_HUB_API_PORT || "8000";
  apiUrl.pathname = "";
  apiUrl.search = "";
  apiUrl.hash = "";
  return apiUrl.origin;
}

export function resolveApiBase({
  location = globalThis.location,
  storage = globalThis.localStorage,
} = {}) {
  const url = urlFromLocation(location);
  return (
    getQueryApiBase(url)
    || cleanApiBase(globalThis.STATE_HUB_API_BASE)
    || getStorageApiBase(storage)
    || inferApiBase(url)
  );
}

export const API = resolveApiBase();
export const POLL = 15_000;
export const POLL_HEAVY = 60_000;
export const FETCH_TIMEOUT = 12_000;

export function pollDelay({ok = true, base = POLL, failures = 0} = {}) {
  return ok ? base : Math.min(base * 2 ** Math.min(failures, 4), 300_000);
}

export function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// Waits `ms` if the tab is visible; pauses until the tab becomes visible if hidden,
// then returns immediately so the next poll fires as soon as the user returns.
export async function waitForVisible(ms) {
  if (typeof document === "undefined") return sleep(ms);
  if (document.visibilityState === "visible") return sleep(ms);
  return new Promise(resolve => {
    const handler = () => {
      document.removeEventListener("visibilitychange", handler);
      resolve();
    };
    document.addEventListener("visibilitychange", handler);
  });
}

export async function apiFetch(path, options = {}) {
  const url = path.startsWith("http") ? path : `${API}${path}`;
  const timeout = options.timeout ?? FETCH_TIMEOUT;
  const {timeout: _timeout, cache = "no-store", ...fetchOptions} = options;
  const ctrl = new AbortController();
  let timedOut = false;
  const timer = setTimeout(() => {
    timedOut = true;
    ctrl.abort();
  }, timeout);
  try {
    return await fetch(url, {cache, ...fetchOptions, signal: ctrl.signal});
  } catch (error) {
    if (timedOut || error?.name === "AbortError") {
      const message = `Request timed out after ${Math.round(timeout / 1000)}s: ${url}`;
      const timeoutError = new Error(message);
      timeoutError.name = "TimeoutError";
      throw timeoutError;
    }
    throw error;
  } finally {
    clearTimeout(timer);
  }
}
