/**
 * Helpers de validation purs (aucune dépendance à Fastify / pg).
 *
 * Extraits de server.js / adminRoutes.js pour être testables unitairement sans
 * démarrer de serveur ni de base de données.
 *
 * Convention : chaque helper renvoie soit la valeur normalisée, soit lève une
 * ValidationError dont le `message` est directement affichable à l'utilisateur.
 */

export class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

/**
 * Nettoie une chaîne de recherche destinée à un ILIKE.
 * Rejette les caractères de contrôle et les patterns pathologiques qui font
 * exploser le planner Postgres (backtracking sur `%%%`, `____________`, …).
 */
export function sanitizeSearchString(str, fieldName = "champ") {
  const trimmed = String(str).trim();

  if (trimmed.length < 2 || trimmed.length > 100) {
    throw new ValidationError(`${fieldName} doit faire entre 2 et 100 caractères`);
  }
  // eslint-disable-next-line no-control-regex
  if (/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/.test(trimmed)) {
    throw new ValidationError(`${fieldName} contient des caractères invalides`);
  }
  if (/%%%|_{10,}|%{10,}/.test(trimmed)) {
    throw new ValidationError(`${fieldName} contient des patterns invalides`);
  }

  return trimmed;
}

/**
 * Parse et valide un `limit`/`offset` de liste publique.
 *
 * Corrige un bug de l'implémentation initiale : `Number("abc")` donnait NaN,
 * que `Math.min` propageait jusqu'au `LIMIT $n` de la requête, provoquant une
 * erreur pg et un 500 au lieu d'un 400. On retombe désormais sur la valeur par
 * défaut pour une entrée non numérique.
 */
export function parseLimitOffset(rawLimit, rawOffset, { defaultLimit = 1000, maxLimit = 10000 } = {}) {
  const limitNum = rawLimit === undefined || rawLimit === null || rawLimit === "" ? defaultLimit : Number(rawLimit);
  if (!Number.isFinite(limitNum) || limitNum < 1) {
    throw new ValidationError("limit invalide");
  }
  const offsetNum = rawOffset === undefined || rawOffset === null || rawOffset === "" ? 0 : Number(rawOffset);
  if (!Number.isFinite(offsetNum) || offsetNum < 0) {
    throw new ValidationError("offset invalide");
  }
  if (offsetNum > 1_000_000) {
    throw new ValidationError("Offset trop grand (max 1 million)");
  }

  return {
    limit: Math.min(Math.floor(limitNum), maxLimit),
    offset: Math.floor(offsetNum),
  };
}

/** Pagination admin : page/pageSize bornés, offset dérivé. */
export function pagination(query = {}) {
  const page = Math.max(1, Number(query.page) || 1);
  const pageSize = Math.min(200, Math.max(1, Number(query.pageSize) || 50));
  return { page, pageSize, offset: (page - 1) * pageSize };
}

/**
 * Parse "minLon,minLat,maxLon,maxLat" et valide la cohérence géographique.
 */
export function parseBbox(raw) {
  if (!raw) throw new ValidationError("bbox and zoom are required");

  const parts = String(raw).split(",").map(Number);
  if (parts.length !== 4) throw new ValidationError("bbox must have 4 values");

  const [minLon, minLat, maxLon, maxLat] = parts;
  if (parts.some((v) => !Number.isFinite(v))) {
    throw new ValidationError("Invalid bbox or zoom values");
  }
  if (minLat < -90 || maxLat > 90 || minLon < -180 || maxLon > 180) {
    throw new ValidationError("Coordinates out of range");
  }
  if (minLon >= maxLon || minLat >= maxLat) {
    throw new ValidationError("Invalid bbox bounds");
  }

  return { minLon, minLat, maxLon, maxLat };
}

export function parseZoom(raw) {
  const zoom = Number(raw);
  if (!Number.isFinite(zoom)) throw new ValidationError("Invalid bbox or zoom values");
  if (zoom < 0 || zoom > 22) throw new ValidationError("Zoom level must be between 0 and 22");
  return zoom;
}

/** Taille de cellule de la grille de clustering, en degrés. */
export function cellSizeForZoom(zoom) {
  return Math.max(0.01, 180 / Math.pow(2, zoom));
}

/**
 * Découpe une liste CSV (pays, statuts…) en tableau borné.
 * `transform` permet de normaliser chaque élément (ex: toUpperCase pour les pays).
 *
 * @param {unknown} raw
 * @param {{ max?: number, label?: string, transform?: (s: string) => string }} [opts]
 * @returns {string[]}
 */
export function parseCsvList(raw, { max, label, transform = (s) => s } = {}) {
  const items = String(raw)
    .split(",")
    .map((s) => transform(s.trim()))
    .filter(Boolean);
  if (max && items.length > max) {
    throw new ValidationError(`Max ${max} ${label} simultanés`);
  }
  return items;
}

/** Valide une année de formation. Renvoie null si l'entrée est vide. */
export function parseYear(raw, fieldName = "year") {
  if (raw === undefined || raw === null || raw === "") return null;
  const year = Number(raw);
  if (!Number.isFinite(year) || year < 1800 || year > 2100) {
    throw new ValidationError(`${fieldName} invalide`);
  }
  return year;
}

/** Valide une latitude. `null` autorisé (efface la valeur). */
export function parseLat(raw) {
  if (raw === null || raw === undefined || raw === "") return null;
  const v = Number(raw);
  if (!Number.isFinite(v) || v < -90 || v > 90) throw new ValidationError("lat invalide");
  return v;
}

/** Valide une longitude. `null` autorisé (efface la valeur). */
export function parseLon(raw) {
  if (raw === null || raw === undefined || raw === "") return null;
  const v = Number(raw);
  if (!Number.isFinite(v) || v < -180 || v > 180) throw new ValidationError("lon invalide");
  return v;
}

/** Valide un identifiant Metal Archives (BIGINT positif). */
export function parseMaId(raw) {
  const id = Number(raw);
  if (!Number.isInteger(id) || id < 0 || id > Number.MAX_SAFE_INTEGER) {
    throw new ValidationError("bad ma_id");
  }
  return id;
}
