/**
 * Authenticated fetch with refresh-on-401.
 *
 * Flow:
 *   1. Try the request with the current access token.
 *   2. On 401, attempt ONE refresh (POST /v1/auth/refresh with the
 *      refresh cookie — httpOnly + Secure + SameSite=Lax). If that
 *      succeeds, retry the original request once.
 *   3. On second 401 (refresh failed or refresh returned 401), clear
 *      local state and force a navigation to /login.
 *
 * Threading: all callers await fetchWithAuth; refresh is guarded by
 * an in-flight Promise so concurrent 401s coalesce into one refresh.
 */

import { getAccessToken, clearTokens, setTokens, getRefreshFailureUrl } from './auth-state';

let inflightRefresh: Promise<boolean> | null = null;

async function tryRefresh(): Promise<boolean> {
  if (inflightRefresh) return inflightRefresh;
  inflightRefresh = (async () => {
    try {
      const r = await fetch('/v1/auth/refresh', {
        method: 'POST',
        credentials: 'include',
      });
      if (!r.ok) return false;
      const data = (await r.json()) as { access_token?: string; refresh_token?: string };
      if (!data.access_token || !data.refresh_token) return false;
      setTokens({ access: data.access_token, refresh: data.refresh_token });
      return true;
    } catch {
      return false;
    } finally {
      inflightRefresh = null;
    }
  })();
  return inflightRefresh;
}

export interface FetchOptions extends RequestInit {
  /** Skip the auth refresh-on-401 retry once. Used for the login
   *  endpoint itself, which is public. */
  skipRefresh?: boolean;
}

export class ApiError extends Error {
  status: number;
  body: unknown;
  constructor(status: number, body: unknown, message: string) {
    super(message);
    this.status = status;
    this.body = body;
  }
}

export async function fetchWithAuth(
  url: string,
  opts: FetchOptions = {},
): Promise<Response> {
  const headers = new Headers(opts.headers);
  const token = getAccessToken();
  if (token && !headers.has('Authorization')) {
    headers.set('Authorization', `Bearer ${token}`);
  }

  const res = await fetch(url, { ...opts, headers, credentials: 'include' });

  if (res.status !== 401 || opts.skipRefresh) {
    return res;
  }

  const ok = await tryRefresh();
  if (!ok) {
    clearTokens();
    const target = getRefreshFailureUrl();
    if (target && typeof window !== 'undefined') {
      window.location.href = target;
    }
    return res;
  }

  const retryHeaders = new Headers(opts.headers);
  const newToken = getAccessToken();
  if (newToken && !retryHeaders.has('Authorization')) {
    retryHeaders.set('Authorization', `Bearer ${newToken}`);
  }
  return fetch(url, { ...opts, headers: retryHeaders, credentials: 'include' });
}

export async function apiGet<T>(url: string): Promise<T> {
  const res = await fetchWithAuth(url);
  if (!res.ok) {
    const body = await safeJson(res);
    throw new ApiError(res.status, body, `${res.status} ${res.statusText}`);
  }
  return res.json() as Promise<T>;
}

export async function apiSend<T>(
  method: 'POST' | 'PATCH' | 'PUT' | 'DELETE',
  url: string,
  body?: unknown,
): Promise<T> {
  const res = await fetchWithAuth(url, {
    method,
    headers: body ? { 'Content-Type': 'application/json' } : undefined,
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!res.ok) {
    const errBody = await safeJson(res);
    throw new ApiError(res.status, errBody, `${method} ${url} → ${res.status}`);
  }
  if (res.status === 204) return undefined as T;
  return res.json() as Promise<T>;
}

async function safeJson(res: Response): Promise<unknown> {
  try {
    return await res.json();
  } catch {
    return null;
  }
}
