import { Platform } from "react-native";
import {
  getRequestTimeoutMs,
  requireEnvironmentBaseUrl,
} from "../core/environmentService";
import { getMobileConfigForBaseUrl } from "../core/mobileConfigService";
import type { ApiEnvelope, RefreshData } from "../types/auth";
import { clearSession, readSession, saveSession } from "./tokenStorage";

type Primitive = string | number | boolean | null | undefined;

export type ApiRequestConfig = {
  headers?: Record<string, string>;
  params?: Record<string, Primitive>;
  signal?: AbortSignal;
};

type ApiResponse<T> = {
  data: T;
  status: number;
  headers: Headers;
};

type InternalRequestConfig = ApiRequestConfig & {
  _retry?: boolean;
};

export class ApiClientError extends Error {
  status?: number;
  // Code metier renvoye par le backend dans le body (ex: "SESSION_REVOKED").
  // Permet aux appelants de distinguer un 401 "evince" d'un 401 "expire".
  code?: string;

  constructor(message: string, status?: number, code?: string) {
    super(message);
    this.name = "ApiClientError";
    this.status = status;
    this.code = code;
  }
}

let hydrated = false;
let accessTokenCache: string | null = null;
let refreshTokenCache: string | null = null;
let refreshPromise: Promise<string | null> | null = null;

// Code envoye par le guard per-seat (Lot 5) quand le compte a ete reconnecte
// ailleurs (web ou autre appareil). Le backend l'expose dans le body du 401
// via le claim `sepoch` compare a chaque requete par MobileJwtMiddleware.
const SESSION_REVOKED_CODE = "SESSION_REVOKED";

// Handler global "session evincee" : l'apiClient etant decouple de l'UI, App
// l'enregistre au montage pour pouvoir ramener l'utilisateur a l'ecran de login
// avec un message dedie. Invoque UNE fois par detection de SESSION_REVOKED.
let sessionRevokedHandler: (() => void) | null = null;

export function registerSessionRevokedHandler(handler: (() => void) | null): void {
  sessionRevokedHandler = handler;
}

function buildDevicePayload() {
  return {
    device_id: `mobile-${Platform.OS}`,
    device_name: Platform.OS,
    platform: Platform.OS,
  };
}

function normalizeHeaders(headers?: Record<string, string>): Record<string, string> {
  return headers ? { ...headers } : {};
}

function getHeader(config: InternalRequestConfig, name: string): string {
  const headers = normalizeHeaders(config.headers);
  const matchKey = Object.keys(headers).find((key) => key.toLowerCase() === name.toLowerCase());
  return matchKey ? String(headers[matchKey]) : "";
}

function setHeader(config: InternalRequestConfig, name: string, value: string): void {
  const headers = normalizeHeaders(config.headers);
  headers[name] = value;
  config.headers = headers;
}

function deleteHeader(config: InternalRequestConfig, name: string): void {
  const headers = normalizeHeaders(config.headers);
  const matchKey = Object.keys(headers).find((key) => key.toLowerCase() === name.toLowerCase());
  if (matchKey) {
    delete headers[matchKey];
  }
  config.headers = headers;
}

async function buildUrl(path: string, params?: Record<string, Primitive>): Promise<string> {
  const baseUrl = await requireEnvironmentBaseUrl();
  const basePath = path.startsWith("http") ? path : `${baseUrl}${path}`;

  if (!params || Object.keys(params).length === 0) {
    return basePath;
  }

  const search = new URLSearchParams();
  Object.entries(params).forEach(([key, value]) => {
    if (value !== null && value !== undefined) {
      search.append(key, String(value));
    }
  });

  const queryString = search.toString();
  if (!queryString) {
    return basePath;
  }

  return `${basePath}${basePath.includes("?") ? "&" : "?"}${queryString}`;
}

// Renvoie un signal d'abort temporise + un disposer. Le disposer DOIT etre
// appele une fois la requete reglee (clearTimeout) : sinon le timer de 15 s
// survit a la reponse -> il garde l'event loop en vie (worker jest qui ne quitte
// pas) et, en prod, laisse un abort() fantome programme par requete.
function createTimeoutSignal(timeoutMs: number): { signal: AbortSignal; clear: () => void } {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  return { signal: controller.signal, clear: () => clearTimeout(timer) };
}

async function hydrateTokensFromStorage(): Promise<void> {
  if (hydrated) {
    return;
  }
  const session = await readSession();
  accessTokenCache = session.accessToken;
  refreshTokenCache = session.refreshToken;
  hydrated = true;
}

async function refreshAccessToken(): Promise<string | null> {
  try {
    const baseUrl = await requireEnvironmentBaseUrl();
    const refreshEndpoints = [`${baseUrl}/mobile/refresh.php`, `${baseUrl}/mobile/refresh`];

    let payload: ApiEnvelope<RefreshData> | null = null;
    let lastMessage: string | null = null;

    for (let index = 0; index < refreshEndpoints.length; index += 1) {
      const timeout = createTimeoutSignal(getRequestTimeoutMs());
      let response: Response;
      try {
        response = await fetch(refreshEndpoints[index], {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            refresh_token: refreshTokenCache,
            ...buildDevicePayload(),
          }),
          signal: timeout.signal,
        });
      } finally {
        timeout.clear();
      }

      try {
        payload = (await response.json()) as ApiEnvelope<RefreshData>;
      } catch {
        payload = null;
      }

      if (response.ok && payload?.success && payload.data) {
        break;
      }

      if (payload?.message) {
        lastMessage = payload.message;
      }

      const is404 = response.status === 404;
      const hasNext = index < refreshEndpoints.length - 1;
      if (!(is404 && hasNext)) {
        throw new Error(lastMessage || "Refresh failed");
      }
    }

    const refreshed = payload!.data!;
    accessTokenCache = refreshed.access_token;
    refreshTokenCache = refreshed.refresh_token;
    hydrated = true;

    await saveSession(refreshed.access_token, refreshed.refresh_token, refreshed.user);
    return refreshed.access_token;
  } catch {
    await clearSession();
    accessTokenCache = null;
    refreshTokenCache = null;
    hydrated = true;
    return null;
  }
}

async function applyRequestHeaders(
  method: "GET" | "POST",
  config: InternalRequestConfig,
  baseUrl: string
): Promise<{ skipAuth: boolean; skipRefresh: boolean }> {
  const storedConfig = await getMobileConfigForBaseUrl(baseUrl);
  const tenantId = storedConfig?.config.tenant_id;
  if (tenantId !== null && tenantId !== undefined && getHeader(config, "X-Tenant-Id") === "") {
    setHeader(config, "X-Tenant-Id", String(tenantId));
  }

  const skipAuth = getHeader(config, "X-Skip-Auth") === "1";
  const skipRefresh = getHeader(config, "X-Skip-Refresh") === "1";
  deleteHeader(config, "X-Skip-Auth");
  deleteHeader(config, "X-Skip-Refresh");

  if (!skipAuth && accessTokenCache && getHeader(config, "Authorization") === "") {
    setHeader(config, "Authorization", `Bearer ${accessTokenCache}`);
  }

  if (method === "POST" && getHeader(config, "Content-Type") === "") {
    setHeader(config, "Content-Type", "application/json");
  }

  return { skipAuth, skipRefresh };
}

async function executeFetch(
  method: "GET" | "POST",
  url: string,
  body: unknown,
  config: InternalRequestConfig
): Promise<Response> {
  const timeout = createTimeoutSignal(getRequestTimeoutMs());
  const signal = config.signal ?? timeout.signal;
  try {
    return await fetch(url, {
      method,
      headers: config.headers,
      body: method === "POST" ? JSON.stringify(body ?? {}) : undefined,
      signal,
    });
  } catch (error) {
    const message = error instanceof Error ? error.message : "Network error";
    throw new ApiClientError(message);
  } finally {
    timeout.clear();
  }
}

async function parseJsonSafely<T>(response: Response): Promise<T | null> {
  try {
    return (await response.json()) as T;
  } catch {
    return null;
  }
}

async function handle401Retry<T>(
  method: "GET" | "POST",
  path: string,
  body: unknown,
  mergedConfig: InternalRequestConfig
): Promise<ApiResponse<T>> {
  await hydrateTokensFromStorage();
  if (!refreshTokenCache) {
    throw new ApiClientError("Unauthorized", 401);
  }

  refreshPromise ??= refreshAccessToken().finally(() => {
    refreshPromise = null;
  });

  const newAccessToken = await refreshPromise;
  if (!newAccessToken) {
    throw new ApiClientError("Unauthorized", 401);
  }

  const retryConfig: InternalRequestConfig = { ...mergedConfig, _retry: true };
  setHeader(retryConfig, "Authorization", `Bearer ${newAccessToken}`);
  return request<T>(method, path, body, retryConfig);
}

function extractResponseCode<T>(payload: T | null): string | null {
  if (payload && typeof payload === "object" && "code" in payload) {
    const raw = (payload as { code?: unknown }).code;
    return typeof raw === "string" && raw ? raw : null;
  }
  return null;
}

function buildErrorMessage<T>(payload: T | null, fallback: string): string {
  if (payload && typeof payload === "object" && "message" in payload) {
    const raw = (payload as { message?: unknown }).message;
    if (typeof raw === "string" && raw) {
      return raw;
    }
  }
  return fallback;
}

function buildErrorFromResponse<T>(payload: T | null, status: number): ApiClientError {
  // On propage le code metier du body (ex: "BILLING_WALL") sur l'erreur, pas
  // seulement pour SESSION_REVOKED : la sync s'en sert pour distinguer un 402
  // mur de facturation d'un 402 generique (cf. classifySyncError).
  const code = extractResponseCode(payload) ?? undefined;
  if (payload && typeof payload === "object" && "message" in payload) {
    return new ApiClientError(buildErrorMessage(payload, "Request failed"), status, code);
  }
  return new ApiClientError(`HTTP ${status}`, status, code);
}

async function request<T>(
  method: "GET" | "POST",
  path: string,
  body?: unknown,
  config: InternalRequestConfig = {}
): Promise<ApiResponse<T>> {
  await hydrateTokensFromStorage();
  const baseUrl = await requireEnvironmentBaseUrl();

  const mergedConfig: InternalRequestConfig = {
    ...config,
    headers: normalizeHeaders(config.headers),
  };

  const { skipRefresh } = await applyRequestHeaders(method, mergedConfig, baseUrl);
  const url = await buildUrl(path, mergedConfig.params);
  const response = await executeFetch(method, url, body, mergedConfig);
  const payload = await parseJsonSafely<T>(response);

  // Lot 5 (verrouillage session unique) : un 401 portant code=SESSION_REVOKED
  // signifie que le compte s'est reconnecte ailleurs. Le refresh token a deja
  // ete revoque cote backend -> inutile (et trompeur) de tenter handle401Retry.
  // On purge la session et on delegue le re-login a App via le handler global.
  if (response.status === 401 && extractResponseCode(payload) === SESSION_REVOKED_CODE) {
    await clearSession();
    accessTokenCache = null;
    refreshTokenCache = null;
    hydrated = true;
    sessionRevokedHandler?.();
    throw new ApiClientError(
      buildErrorMessage(payload, "Session revoquee"),
      401,
      SESSION_REVOKED_CODE
    );
  }

  if (response.status === 401 && !skipRefresh && !mergedConfig._retry) {
    return handle401Retry<T>(method, path, body, mergedConfig);
  }

  if (!response.ok) {
    throw buildErrorFromResponse(payload, response.status);
  }

  return {
    data: (payload ?? {}) as T,
    status: response.status,
    headers: response.headers,
  };
}

const apiClient = {
  get<T>(path: string, config?: ApiRequestConfig): Promise<ApiResponse<T>> {
    return request<T>("GET", path, undefined, config);
  },
  post<T>(path: string, body?: unknown, config?: ApiRequestConfig): Promise<ApiResponse<T>> {
    return request<T>("POST", path, body, config);
  },
};

export function isApiClientError(error: unknown): error is ApiClientError {
  return error instanceof ApiClientError;
}

export async function initializeApiClientAuth(): Promise<void> {
  await hydrateTokensFromStorage();
}

export function setApiAuthTokens(
  accessToken: string | null,
  refreshToken: string | null
): void {
  accessTokenCache = accessToken;
  refreshTokenCache = refreshToken;
  hydrated = true;
}

export function clearApiAuthTokens(): void {
  accessTokenCache = null;
  refreshTokenCache = null;
  hydrated = true;
}

// Acces lecture-seule au token courant. Utile pour le composant Image
// (RN supporte source.headers, mais le composant n'a pas acces au request()
// du client). Hydrate depuis SecureStore au premier appel pour eviter null
// quand le tableau de bord est ouvert juste apres un cold start.
export async function getApiAccessToken(): Promise<string | null> {
  await hydrateTokensFromStorage();
  return accessTokenCache;
}

// URL absolue construite a partir de la baseUrl + path relatif (commencant
// par "/"). Utilisee pour les fichiers servis par l'API (preview photos)
// que le mobile doit charger via <Image source={{ uri, headers }} />.
export async function buildApiUrl(path: string): Promise<string> {
  return buildUrl(path);
}

export default apiClient;
