import { authStorage } from "./auth-storage";

import { BASE_API_URL } from "@ema-platform/api";

interface RefreshResponse {
  token: string;
  refreshToken: string;
}

/**
 * Marks the one failure that actually ends a session: the server rejecting the
 * refresh token. Read structurally by the API layer's 401 handler — a shared
 * error class would mean `libs/api` importing `libs/auth`, which already
 * imports `libs/api`.
 */
const sessionExpired = (message: string) =>
  Object.assign(new Error(message), { sessionExpired: true });

let inFlight: Promise<string> | null = null;

let signedOut = false;

/**
 * Set on logout, cleared on login. A refresh that was already in flight when
 * the user (or the idle timer) signed out must not write its response back
 * into storage — that would silently re-authenticate an unattended desk.
 */
export function setSignedOut(v: boolean) {
  signedOut = v;
}

/**
 * Concurrent 401s must share one refresh. A page fires several requests at
 * once; without this each one POSTs the same refresh token, the server rotates
 * on the first and rejects the rest, and the losers tear down the session the
 * winner just renewed.
 */
export function refreshAccessToken(): Promise<string> {
  inFlight ??= acquireAndRefresh().finally(() => {
    inFlight = null;
  });
  return inFlight;
}

/**
 * Cross-tab guard on top of the in-tab one: cookies are shared per origin, so
 * two tabs expiring together would both POST the same rotating refresh token
 * and the loser would tear down the session the winner just renewed. A Web
 * Lock makes the second tab wait; if the first tab already refreshed while it
 * waited, the fresh token is sitting in storage and no request is needed.
 */
async function acquireAndRefresh(): Promise<string> {
  if (typeof navigator === "undefined" || !navigator.locks) {
    // Old Safari / test env — in-tab de-dup still applies.
    return runRefresh();
  }
  const tokenBefore = authStorage.getToken();
  return navigator.locks.request("ema-token-refresh", async () => {
    const current = authStorage.getToken();
    if (current && current !== tokenBefore) return current;
    return runRefresh();
  });
}

async function runRefresh(): Promise<string> {
  if (signedOut) throw new Error("Signed out");
  const refreshToken = authStorage.getRefreshToken();
  if (!refreshToken) throw sessionExpired("No refresh token available");

  // The IAM service exposes this as `refresh-token`; posting to `/auth/refresh`
  // 404s, which the caller would turn into a silent logout.
  const response = await fetch(`${BASE_API_URL}/auth/refresh-token`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ refreshToken }),
  });

  if (
    response.status === 400 ||
    response.status === 401 ||
    response.status === 403
  ) {
    throw sessionExpired("Refresh token rejected");
  }

  // Anything else is the API having a bad minute — a 502, a proxy timeout. The
  // session is still valid, so leave it alone and let the screen report it.
  if (!response.ok) {
    throw new Error(`Token refresh failed (${response.status})`);
  }

  const data: RefreshResponse = await response.json();
  // Deliberately NOT sessionExpired: the user already signed out, so there is
  // no session left to end — just refuse to resurrect it.
  if (signedOut) throw new Error("Signed out during refresh");
  authStorage.setToken(data.token);
  if (data.refreshToken) authStorage.setRefreshToken(data.refreshToken);
  return data.token;
}
