import { createHash, createHmac } from "node:crypto";

/*
 * Assinatura AWS Signature Version 4 para a API S3 do Cloudflare R2.
 *
 * Feita aqui, sem biblioteca, porque só precisamos de duas coisas: endereços
 * assinados (o navegador envia e baixa direto do R2, sem passar pela VPS) e
 * cabeçalhos assinados para as poucas chamadas que o servidor faz (abrir e
 * fechar um envio em partes, conferir o tamanho, ler o começo do arquivo).
 *
 * Os testes conferem contra os exemplos da documentação da AWS.
 */

export type CredenciaisS3 = {
  idDaChave: string;
  segredo: string;
  /* O R2 usa "auto". */
  regiao: string;
};

export type PedidoParaAssinar = {
  metodo: string;
  /* Sem protocolo: "conta.r2.cloudflarestorage.com". */
  host: string;
  /* Caminho SEM codificar ("/balde/pasta/arquivo com espaço.pdf"). */
  caminho: string;
  consulta?: Record<string, string>;
  /* Cabeçalhos que entram na assinatura, além do host. */
  cabecalhos?: Record<string, string>;
  /* Hash hexadecimal do corpo, ou "UNSIGNED-PAYLOAD". */
  hashDoCorpo?: string;
};

const SERVICO = "s3";
const ALGORITMO = "AWS4-HMAC-SHA256";
export const CORPO_SEM_ASSINATURA = "UNSIGNED-PAYLOAD";
export const HASH_DO_CORPO_VAZIO = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";

/* Só letras, números e - _ . ~ ficam como estão (RFC 3986). */
export function codificar(texto: string): string {
  return encodeURIComponent(texto).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
}

/* Cada pedaço do caminho é codificado; as barras ficam. */
export function caminhoCodificado(caminho: string): string {
  const comBarra = caminho.startsWith("/") ? caminho : `/${caminho}`;
  return comBarra.split("/").map(codificar).join("/");
}

export function hashHex(dado: string | Uint8Array): string {
  return createHash("sha256").update(dado).digest("hex");
}

function hmac(chave: Buffer | string, texto: string): Buffer {
  return createHmac("sha256", chave).update(texto, "utf8").digest();
}

export function carimbo(agora: Date): { completo: string; dia: string } {
  const iso = agora.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
  return { completo: iso, dia: iso.slice(0, 8) };
}

function chaveDeAssinatura(segredo: string, dia: string, regiao: string): Buffer {
  const doDia = hmac(`AWS4${segredo}`, dia);
  const daRegiao = hmac(doDia, regiao);
  const doServico = hmac(daRegiao, SERVICO);
  return hmac(doServico, "aws4_request");
}

function consultaCanonica(consulta: Record<string, string>): string {
  return Object.entries(consulta)
    .map(([nome, valor]) => [codificar(nome), codificar(valor)] as const)
    .sort(([a, va], [b, vb]) => (a < b ? -1 : a > b ? 1 : va < vb ? -1 : va > vb ? 1 : 0))
    .map(([nome, valor]) => `${nome}=${valor}`)
    .join("&");
}

function cabecalhosCanonicos(cabecalhos: Record<string, string>): { canonicos: string; assinados: string } {
  const lista = Object.entries(cabecalhos)
    .map(([nome, valor]) => [nome.toLowerCase().trim(), String(valor).trim().replace(/\s+/g, " ")] as const)
    .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
  return {
    canonicos: lista.map(([nome, valor]) => `${nome}:${valor}\n`).join(""),
    assinados: lista.map(([nome]) => nome).join(";"),
  };
}

function assinatura(
  pedido: PedidoParaAssinar,
  consulta: Record<string, string>,
  cabecalhos: Record<string, string>,
  hashDoCorpo: string,
  cred: CredenciaisS3,
  quando: { completo: string; dia: string },
): { assinatura: string; assinados: string } {
  const { canonicos, assinados } = cabecalhosCanonicos(cabecalhos);
  const pedidoCanonico = [
    pedido.metodo.toUpperCase(),
    caminhoCodificado(pedido.caminho),
    consultaCanonica(consulta),
    canonicos,
    assinados,
    hashDoCorpo,
  ].join("\n");
  const escopo = `${quando.dia}/${cred.regiao}/${SERVICO}/aws4_request`;
  const textoParaAssinar = [ALGORITMO, quando.completo, escopo, hashHex(pedidoCanonico)].join("\n");
  const chave = chaveDeAssinatura(cred.segredo, quando.dia, cred.regiao);
  return { assinatura: createHmac("sha256", chave).update(textoParaAssinar, "utf8").digest("hex"), assinados };
}

/*
 * Endereço assinado: quem tem o endereço faz aquele pedido até vencer.
 * O R2 aceita até 7 dias.
 */
export function enderecoAssinado(
  pedido: PedidoParaAssinar & { expiraEmSegundos: number },
  cred: CredenciaisS3,
  agora: Date = new Date(),
): string {
  const expira = Math.max(1, Math.min(604800, Math.floor(pedido.expiraEmSegundos)));
  const quando = carimbo(agora);
  const consulta: Record<string, string> = {
    ...(pedido.consulta ?? {}),
    "X-Amz-Algorithm": ALGORITMO,
    "X-Amz-Credential": `${cred.idDaChave}/${quando.dia}/${cred.regiao}/${SERVICO}/aws4_request`,
    "X-Amz-Date": quando.completo,
    "X-Amz-Expires": String(expira),
  };
  const cabecalhos = { host: pedido.host, ...(pedido.cabecalhos ?? {}) };
  consulta["X-Amz-SignedHeaders"] = cabecalhosCanonicos(cabecalhos).assinados;
  const { assinatura: valor } = assinatura(pedido, consulta, cabecalhos, pedido.hashDoCorpo ?? CORPO_SEM_ASSINATURA, cred, quando);
  return `https://${pedido.host}${caminhoCodificado(pedido.caminho)}?${consultaCanonica(consulta)}&X-Amz-Signature=${valor}`;
}

/*
 * Cabeçalhos para uma chamada do servidor. O `host` entra na assinatura, mas
 * não volta na lista: quem preenche o Host é o fetch.
 */
export function cabecalhosDeAutorizacao(
  pedido: PedidoParaAssinar,
  cred: CredenciaisS3,
  agora: Date = new Date(),
): Record<string, string> {
  const quando = carimbo(agora);
  const hashDoCorpo = pedido.hashDoCorpo ?? HASH_DO_CORPO_VAZIO;
  const extras: Record<string, string> = {};
  for (const [nome, valor] of Object.entries(pedido.cabecalhos ?? {})) extras[nome.toLowerCase()] = valor;
  const assinaveis: Record<string, string> = {
    ...extras,
    host: pedido.host,
    "x-amz-content-sha256": hashDoCorpo,
    "x-amz-date": quando.completo,
  };
  const { assinatura: valor, assinados } = assinatura(pedido, pedido.consulta ?? {}, assinaveis, hashDoCorpo, cred, quando);
  const escopo = `${quando.dia}/${cred.regiao}/${SERVICO}/aws4_request`;
  const { host: _host, ...semHost } = assinaveis;
  void _host;
  return {
    ...semHost,
    authorization: `${ALGORITMO} Credential=${cred.idDaChave}/${escopo}, SignedHeaders=${assinados}, Signature=${valor}`,
  };
}

/* "?a=1&b" da consulta, já codificada e na ordem canônica. */
export function textoDaConsulta(consulta: Record<string, string> | undefined): string {
  if (!consulta || Object.keys(consulta).length === 0) return "";
  return `?${consultaCanonica(consulta)}`;
}
