/** Connection status values exposed by WebSocketProvider. */
export const ConnectionStatus = Object.freeze({
  CONNECTED: "connected",
  CONNECTING: "connecting",
  RECONNECTING: "reconnecting",
  DISCONNECTED: "disconnected",
});

export const DEFAULT_RECONNECT_CONFIG = Object.freeze({
  /** Base delay before first retry (ms). */
  baseDelayMs: 1000,
  /** Max delay between retries (ms). */
  maxDelayMs: 30000,
  /** Hard cap on reconnect attempts before staying disconnected. */
  maxAttempts: 12,
  /** Refresh access token if it expires within this window (seconds). */
  tokenRefreshSkewSeconds: 60,
  /** App-level keepalive interval (ms). Stay under common proxy idle timeouts (~60s). */
  heartbeatIntervalMs: 25000,
});

/**
 * Exponential backoff with full jitter.
 * delay = random_between(0, min(maxDelay, base * 2^attempt))
 *
 * @param {number} attempt zero-based attempt index
 * @param {{ baseDelayMs?: number, maxDelayMs?: number }} [config]
 * @param {() => number} [random] injectable RNG for tests
 * @returns {number} delay in milliseconds
 */
export function computeBackoffDelay(
  attempt,
  config = {},
  random = Math.random,
) {
  const baseDelayMs = config.baseDelayMs ?? DEFAULT_RECONNECT_CONFIG.baseDelayMs;
  const maxDelayMs = config.maxDelayMs ?? DEFAULT_RECONNECT_CONFIG.maxDelayMs;
  const exp = Math.min(maxDelayMs, baseDelayMs * 2 ** Math.max(0, attempt));
  return Math.floor(random() * exp);
}

/**
 * @param {number|undefined|null} expSeconds JWT exp claim (seconds since epoch)
 * @param {number} nowSeconds current unix time in seconds
 * @param {number} [skewSeconds]
 * @returns {boolean}
 */
export function shouldRefreshAccessToken(
  expSeconds,
  nowSeconds,
  skewSeconds = DEFAULT_RECONNECT_CONFIG.tokenRefreshSkewSeconds,
) {
  if (expSeconds == null || Number.isNaN(expSeconds)) {
    return false;
  }
  return expSeconds - nowSeconds <= skewSeconds;
}

/**
 * Decode JWT payload without verifying signature (client-side expiry check only).
 * @param {string|null|undefined} token
 * @returns {{ exp?: number } | null}
 */
export function decodeJwtPayload(token) {
  if (!token || typeof token !== "string") {
    return null;
  }
  const parts = token.split(".");
  if (parts.length < 2) {
    return null;
  }
  try {
    const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/");
    const json = atob(normalized);
    return JSON.parse(json);
  } catch {
    return null;
  }
}

/**
 * Refresh access token via REST if expired/near-expiry.
 * Coordinates with JWT auth used by axios (#22); WS itself still uses account email.
 *
 * @param {{
 *   getAccessToken?: () => string|null,
 *   getRefreshToken?: () => string|null,
 *   setTokens?: (access: string, refresh: string) => void,
 *   refreshRequest?: (refresh: string) => Promise<{ access: string, refresh?: string }>,
 *   nowSeconds?: () => number,
 *   skewSeconds?: number,
 * }} [deps]
 * @returns {Promise<string|null>} access token to use, or null if unavailable
 */
export async function ensureFreshAccessToken(deps = {}) {
  const getAccessToken =
    deps.getAccessToken ?? (() => localStorage.getItem("access_token"));
  const getRefreshToken =
    deps.getRefreshToken ?? (() => localStorage.getItem("refresh_token"));
  const setTokens =
    deps.setTokens ??
    ((access, refresh) => {
      localStorage.setItem("access_token", access);
      if (refresh) {
        localStorage.setItem("refresh_token", refresh);
      }
    });
  const nowSeconds = deps.nowSeconds ?? (() => Math.ceil(Date.now() / 1000));
  const skewSeconds =
    deps.skewSeconds ?? DEFAULT_RECONNECT_CONFIG.tokenRefreshSkewSeconds;

  const access = getAccessToken();
  const refresh = getRefreshToken();
  if (!access) {
    return null;
  }

  const payload = decodeJwtPayload(access);
  if (
    !shouldRefreshAccessToken(payload?.exp, nowSeconds(), skewSeconds) ||
    !refresh
  ) {
    return access;
  }

  const refreshPayload = decodeJwtPayload(refresh);
  if (
    refreshPayload?.exp != null &&
    refreshPayload.exp <= nowSeconds()
  ) {
    return null;
  }

  if (!deps.refreshRequest) {
    return access;
  }

  try {
    const data = await deps.refreshRequest(refresh);
    if (data?.access) {
      setTokens(data.access, data.refresh ?? refresh);
      return data.access;
    }
  } catch (err) {
    console.log("Token refresh before WS connect failed", err);
  }
  return access;
}

/**
 * Subscribe to browser + Capacitor lifecycle events that imply the socket may be dead.
 * Capacitor plugins are resolved via window.Capacitor.Plugins when present (#20/#21);
 * web visibility/online listeners always register.
 *
 * @param {{
 *   onResume: () => void,
 *   onOnline: () => void,
 *   onOffline?: () => void,
 * }} handlers
 * @returns {() => void} cleanup
 */
export function subscribeLifecycleEvents(handlers) {
  const { onResume, onOnline, onOffline } = handlers;
  const cleanups = [];

  const onVisibility = () => {
    if (document.visibilityState === "visible") {
      onResume();
    }
  };
  const onWindowOnline = () => onOnline();
  const onWindowOffline = () => onOffline?.();

  document.addEventListener("visibilitychange", onVisibility);
  window.addEventListener("online", onWindowOnline);
  window.addEventListener("offline", onWindowOffline);
  cleanups.push(() => {
    document.removeEventListener("visibilitychange", onVisibility);
    window.removeEventListener("online", onWindowOnline);
    window.removeEventListener("offline", onWindowOffline);
  });

  const Cap = typeof window !== "undefined" ? window.Capacitor : undefined;
  const plugins = Cap?.Plugins;
  if (plugins?.App?.addListener) {
    const handle = plugins.App.addListener("appStateChange", (state) => {
      if (state?.isActive) {
        onResume();
      }
    });
    cleanups.push(() => {
      if (handle?.remove) {
        handle.remove();
      } else if (typeof handle?.then === "function") {
        handle.then((h) => h?.remove?.());
      }
    });
  }
  if (plugins?.Network?.addListener) {
    const handle = plugins.Network.addListener(
      "networkStatusChange",
      (status) => {
        if (status?.connected) {
          onOnline();
        } else {
          onOffline?.();
        }
      },
    );
    cleanups.push(() => {
      if (handle?.remove) {
        handle.remove();
      } else if (typeof handle?.then === "function") {
        handle.then((h) => h?.remove?.());
      }
    });
  }

  return () => {
    cleanups.forEach((fn) => {
      try {
        fn();
      } catch {
        /* ignore plugin cleanup errors */
      }
    });
  };
}

/**
 * Heartbeat payload ignored by chat handlers that key on `message`.
 * Keeps proxies from idling out `/ws/` connections.
 */
export function buildHeartbeatPayload(email) {
  return JSON.stringify({
    type: "ping",
    email: email ?? null,
  });
}

export function isSocketOpen(socket) {
  return Boolean(socket && socket.readyState === WebSocket.OPEN);
}
