/*
 * As regras que o depósito (Cloudflare R2) cobra para juntar as partes da
 * gravação num arquivo só. Sem elas o R2 responde "InvalidPart" e o arquivo
 * inteiro se perde:
 *   - as partes vão de 1 a N, sem buraco;
 *   - toda parte, menos a última, tem o MESMO tamanho, de pelo menos 5 MiB;
 *   - a última pode ser menor (nunca maior que as outras);
 *   - a etiqueta (ETag) de cada parte é a que o depósito devolveu.
 *
 * Conferir antes de fechar diz o motivo de verdade (e as partes ficam
 * guardadas para recuperar), em vez de um "InvalidPart" sem explicação.
 */

export const MINIMO_DA_PARTE = 5 * 1024 * 1024;
export const MAXIMO_DE_PARTES = 10000;

export type ParteDaGravacao = { numero: number; etag: string; tamanho?: number };

export type ConferenciaDasPartes = { ok: true } | { ok: false; motivo: string };

export function ordenarPartes<T extends { numero: number }>(partes: T[]): T[] {
  return [...partes].sort((a, b) => a.numero - b.numero);
}

/* `exigirTamanho`: só dá para cobrar o tamanho quando ele é conhecido (a lista do depósito sempre traz). */
export function conferirPartes(partes: ParteDaGravacao[], exigirTamanho = true): ConferenciaDasPartes {
  if (!partes.length) return { ok: false, motivo: "nenhuma parte" };
  if (partes.length > MAXIMO_DE_PARTES) return { ok: false, motivo: `partes demais (${partes.length})` };
  const ordenadas = ordenarPartes(partes);
  for (let i = 0; i < ordenadas.length; i += 1) {
    const p = ordenadas[i];
    if (p.numero !== i + 1) {
      return { ok: false, motivo: p.numero === ordenadas[i - 1]?.numero ? `parte ${p.numero} repetida` : `falta a parte ${i + 1}` };
    }
    if (!p.etag) return { ok: false, motivo: `parte ${p.numero} sem etiqueta` };
  }
  if (!exigirTamanho) return { ok: true };
  const tamanhos = ordenadas.map((p) => Number(p.tamanho));
  if (tamanhos.some((t) => !Number.isFinite(t) || t <= 0)) return { ok: false, motivo: "parte sem tamanho" };
  if (ordenadas.length === 1) return { ok: true };
  const padrao = tamanhos[0];
  if (padrao < MINIMO_DA_PARTE) return { ok: false, motivo: `parte 1 menor que 5 MiB (${padrao} bytes)` };
  for (let i = 1; i < tamanhos.length - 1; i += 1) {
    if (tamanhos[i] !== padrao) return { ok: false, motivo: `parte ${i + 1} com ${tamanhos[i]} bytes (as outras têm ${padrao})` };
  }
  const ultima = tamanhos[tamanhos.length - 1];
  if (ultima > padrao) return { ok: false, motivo: `última parte maior que as outras (${ultima} > ${padrao})` };
  return { ok: true };
}

/* Um retrato curto das partes para o registro de erro (quantas, tamanhos, buracos). */
export function resumoDasPartes(partes: ParteDaGravacao[]): {
  partes: number;
  bytes: number;
  tamanhos: Record<string, number>;
  faltam: number[];
  repetidas: number[];
} {
  const ordenadas = ordenarPartes(partes);
  const tamanhos: Record<string, number> = {};
  let bytes = 0;
  const vistos = new Set<number>();
  const repetidas: number[] = [];
  for (const p of ordenadas) {
    const t = String(p.tamanho ?? "?");
    tamanhos[t] = (tamanhos[t] ?? 0) + 1;
    bytes += Number(p.tamanho ?? 0) || 0;
    if (vistos.has(p.numero)) repetidas.push(p.numero);
    vistos.add(p.numero);
  }
  const maior = ordenadas.length ? ordenadas[ordenadas.length - 1].numero : 0;
  const faltam: number[] = [];
  for (let n = 1; n <= Math.min(maior, MAXIMO_DE_PARTES) && faltam.length < 50; n += 1) if (!vistos.has(n)) faltam.push(n);
  return { partes: ordenadas.length, bytes, tamanhos, faltam, repetidas: repetidas.slice(0, 50) };
}

/*
 * Qual lista vai para o fechamento: a do depósito é a verdade (etiquetas e
 * tamanhos que ele guardou); a do banco só vale quando o depósito não listou.
 */
export function partesParaFechar(doBanco: ParteDaGravacao[], doDeposito: ParteDaGravacao[] | null): { partes: ParteDaGravacao[]; fonte: "deposito" | "banco" } {
  if (doDeposito && doDeposito.length) return { partes: ordenarPartes(doDeposito), fonte: "deposito" };
  return { partes: ordenarPartes(doBanco), fonte: "banco" };
}
