/*
  Baixar arquivo de fora para o catálogo em PDF: foto de produto e logo.

  O endereço da foto é o lojista quem grava, então ele pode ser qualquer
  coisa. Este servidor não busca endereço interno (localhost, rede privada,
  IP cru) nem segue redirecionamento para um deles, e não lê arquivo sem
  fim: passou do limite, desiste. Só http e https.
*/

import { isIP } from "node:net";

export const LIMITE_DO_ARQUIVO = 25 * 1024 * 1024;

function hostProibido(host: string) {
  const nome = host.toLowerCase().replace(/^\[|\]$/g, "");
  if (!nome || nome === "localhost" || nome.endsWith(".localhost") || nome.endsWith(".local") || nome.endsWith(".internal")) return true;
  /* IP escrito direto no endereço: foto de loja mora em domínio, não em IP. */
  return isIP(nome) !== 0;
}

export function enderecoPermitido(bruto: string) {
  try {
    const url = new URL(bruto);
    if (url.protocol !== "https:" && url.protocol !== "http:") return false;
    if (url.username || url.password) return false;
    return !hostProibido(url.hostname);
  } catch {
    return false;
  }
}

async function lerAteOLimite(resposta: Response, limite: number) {
  const tamanho = Number(resposta.headers.get("content-length") || 0);
  if (tamanho > limite) return null;
  if (!resposta.body) return Buffer.from(await resposta.arrayBuffer());
  const leitor = resposta.body.getReader();
  const pedacos: Uint8Array[] = [];
  let total = 0;
  for (;;) {
    const { done, value } = await leitor.read();
    if (done) break;
    total += value.byteLength;
    if (total > limite) {
      await leitor.cancel().catch(() => undefined);
      return null;
    }
    pedacos.push(value);
  }
  return Buffer.concat(pedacos);
}

async function umaVez(inicio: string, tempo: number, limite: number) {
  const controle = new AbortController();
  const relogio = setTimeout(() => controle.abort(), tempo);
  try {
    let endereco = inicio;
    for (let salto = 0; salto < 4; salto += 1) {
      if (!enderecoPermitido(endereco)) return null;
      const resposta = await fetch(endereco, { signal: controle.signal, redirect: "manual", headers: { "user-agent": "Ktalogu-Catalogo/1.0" } });
      if (resposta.status >= 300 && resposta.status < 400) {
        const proximo = resposta.headers.get("location");
        await resposta.body?.cancel().catch(() => undefined);
        if (!proximo) return null;
        endereco = new URL(proximo, endereco).toString();
        continue;
      }
      if (!resposta.ok) {
        await resposta.body?.cancel().catch(() => undefined);
        return { falhou: true as const };
      }
      const corpo = await lerAteOLimite(resposta, limite);
      return corpo ? { corpo } : null;
    }
    return null;
  } finally {
    clearTimeout(relogio);
  }
}

/**
 * Baixa com paciência: o serviço de imagem recusa quando chegam muitas de
 * uma vez, e uma foto que falha não pode virar um buraco no catálogo. Três
 * tentativas, com espera crescente. `null` = não veio.
 */
export async function baixar(url: string, { tentativas = 3, tempo = 30000, limite = LIMITE_DO_ARQUIVO } = {}) {
  if (!enderecoPermitido(url)) return null;
  for (let i = 0; i < tentativas; i += 1) {
    try {
      const resultado = await umaVez(url, tempo, limite);
      if (resultado && "corpo" in resultado) return resultado.corpo;
      /* Endereço recusado ou arquivo grande demais: tentar de novo não muda. */
      if (!resultado) return null;
    } catch {
      /* tempo esgotado ou rede: tenta de novo */
    }
    await new Promise((pronto) => setTimeout(pronto, 600 * (i + 1)));
  }
  return null;
}
