import type { AuthSession } from "@rubis/shared";
import { env } from "./env";
import { authStore } from "./auth";

/**
 * Client HTTP — façade unique pour l'API REST. Centralise :
 *  - Header Authorization (Bearer token depuis authStore)
 *  - Cookie refresh httpOnly (`credentials: 'include'`)
 *  - Silent refresh sur 401 (rotation du token + retry une fois)
 *  - Gestion d'erreur uniforme via la classe ApiError
 *
 * Le contrat de réponse Adonis est `{ data: ... }` (cf. backend.md §6).
 * Les erreurs sont `{ errors: [{ code, message, field? }] }`.
 *
 * En dev avec VITE_USE_MOCKS=true, MSW intercepte transparently — ce
 * fichier fonctionne pareil dans les deux modes.
 */

export class ApiError extends Error {
  constructor(
    public readonly status: number,
    public readonly code: string,
    message: string,
    public readonly fieldErrors?: Record<string, string[]>,
  ) {
    super(message);
    this.name = "ApiError";
  }
}

type RequestOptions = {
  method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
  body?: unknown;
  signal?: AbortSignal;
  /** Si true, n'inclut pas le header Authorization (utile pour /auth/login). */
  anonymous?: boolean;
  /**
   * Si true, ne déroule pas l'enveloppe `{ data, meta }` — utile pour les
   * endpoints paginés qui ont besoin de meta.total / meta.page.
   */
  envelope?: boolean;
};

/**
 * Méta-info standard renvoyée par les endpoints paginés. Mappe le
 * `meta: { total, page }` que l'API Adonis renvoie aux côtés de `data`.
 */
export type PaginationMeta = {
  total: number;
  page: number;
};

export type ListResponse<T> = {
  data: T[];
  meta?: PaginationMeta;
};

/**
 * Refresh en cours partagé entre toutes les requêtes 401 simultanées :
 * si N requêtes reviennent 401 en même temps, on n'envoie qu'un seul
 * /auth/refresh et toutes attendent le même résultat.
 */
let pendingRefresh: Promise<void> | null = null;

async function performRefresh(): Promise<void> {
  if (!pendingRefresh) {
    pendingRefresh = (async () => {
      try {
        const session = await rawRequest<AuthSession>("/api/v1/auth/refresh", {
          method: "POST",
          anonymous: true,
        });
        authStore.setSession(session.accessToken, session.user);
      } catch (err) {
        authStore.clear();
        throw err;
      } finally {
        pendingRefresh = null;
      }
    })();
  }
  return pendingRefresh;
}

/**
 * Bas niveau : fait la requête sans tenter de silent refresh.
 * Utilisé par performRefresh elle-même (sinon boucle infinie sur
 * /auth/refresh qui revient 401).
 */
async function rawRequest<T>(path: string, options: RequestOptions = {}): Promise<T> {
  const { method = "GET", body, signal, anonymous = false } = options;
  const isFormData = body instanceof FormData;
  const headers: Record<string, string> = {
    Accept: "application/json",
  };
  if (body !== undefined && !isFormData) headers["Content-Type"] = "application/json";
  if (!anonymous && authStore.token) {
    headers.Authorization = `Bearer ${authStore.token}`;
  }

  const url = path.startsWith("http") ? path : `${env.VITE_API_URL}${path}`;

  const response = await fetch(url, {
    method,
    headers,
    credentials: "include",
    body:
      body === undefined
        ? undefined
        : isFormData
          ? body
          : JSON.stringify(body),
    signal,
  });

  if (response.status === 204) {
    return undefined as T;
  }

  const json = (await response.json().catch(() => null)) as
    | { data?: T; errors?: Array<{ code: string; message: string; field?: string }> }
    | null;

  if (!response.ok) {
    const firstError = json?.errors?.[0];
    const fieldErrors: Record<string, string[]> | undefined = json?.errors?.reduce(
      (acc, err) => {
        if (err.field) {
          acc[err.field] = [...(acc[err.field] ?? []), err.message];
        }
        return acc;
      },
      {} as Record<string, string[]>,
    );
    throw new ApiError(
      response.status,
      firstError?.code ?? "unknown",
      firstError?.message ?? `Requête échouée (${response.status})`,
      fieldErrors,
    );
  }

  // Convention de réponse Adonis : { data: ..., meta?: ... }. Par défaut on
  // extrait `data` (contrat documenté). Si le caller demande l'enveloppe
  // (`envelope: true`), on renvoie le json tel quel — utile pour récupérer
  // `meta` (total, page) sur les endpoints paginés.
  if (options.envelope) return json as T;
  return (json?.data ?? json) as T;
}

/**
 * Niveau public : ajoute le silent refresh sur 401.
 *
 * Si une requête authentifiée revient 401 :
 *  1. On lance (ou attend) un seul /auth/refresh
 *  2. Si le refresh réussit, on retry la requête originale avec le
 *     nouveau token
 *  3. Si le refresh échoue, l'authStore est cleared (le router guard
 *     redirige vers /login) et on propage le 401 original
 *
 * Les requêtes anonymes (signup, login, refresh lui-même) ne tentent
 * pas de refresh — elles n'ont pas de token à rafraîchir.
 */
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
  try {
    return await rawRequest<T>(path, options);
  } catch (err) {
    const isAuthEndpoint = path.startsWith("/api/v1/auth/");
    const shouldRefresh =
      err instanceof ApiError &&
      err.status === 401 &&
      !options.anonymous &&
      !isAuthEndpoint &&
      authStore.token !== null;

    if (!shouldRefresh) throw err;

    try {
      await performRefresh();
    } catch {
      // refresh KO → on propage le 401 original (le store est déjà cleared)
      throw err;
    }

    // Retry une seule fois avec le nouveau token.
    return rawRequest<T>(path, options);
  }
}

export const api = {
  get: <T>(path: string, options?: Omit<RequestOptions, "method" | "body">): Promise<T> =>
    request<T>(path, { ...options, method: "GET" }),
  /**
   * Variante GET qui renvoie l'enveloppe `{ data, meta }` complète — pour
   * les endpoints paginés où on a besoin de `meta.total` afin de rendre
   * un compteur "X factures" et des contrôles précédent/suivant.
   */
  getList: <T>(
    path: string,
    options?: Omit<RequestOptions, "method" | "body" | "envelope">,
  ): Promise<ListResponse<T>> =>
    request<ListResponse<T>>(path, { ...options, method: "GET", envelope: true }),
  post: <T>(
    path: string,
    body?: unknown,
    options?: Omit<RequestOptions, "method" | "body">,
  ): Promise<T> => request<T>(path, { ...options, method: "POST", body }),
  patch: <T>(
    path: string,
    body?: unknown,
    options?: Omit<RequestOptions, "method" | "body">,
  ): Promise<T> => request<T>(path, { ...options, method: "PATCH", body }),
  delete: <T>(path: string, options?: Omit<RequestOptions, "method" | "body">): Promise<T> =>
    request<T>(path, { ...options, method: "DELETE" }),

  /**
   * Fetch d'un binaire (PDF, image…) avec le Bearer auto-injecté. Renvoie
   * un Blob + le content-type. Pas de silent refresh sur 401 (cas rare
   * pour des assets longue durée), si besoin re-fetch côté caller.
   */
  fetchBlob: async (path: string, signal?: AbortSignal): Promise<{ blob: Blob; contentType: string }> => {
    const url = path.startsWith("http") ? path : `${env.VITE_API_URL}${path}`;
    const res = await fetch(url, {
      headers: authStore.token ? { Authorization: `Bearer ${authStore.token}` } : {},
      credentials: "include",
      signal,
    });
    if (!res.ok) {
      throw new ApiError(res.status, "blob_fetch_failed", `HTTP ${res.status} on ${path}`);
    }
    const blob = await res.blob();
    return { blob, contentType: res.headers.get("content-type") ?? blob.type };
  },
};
