import {
  CORPO_SEM_ASSINATURA,
  HASH_DO_CORPO_VAZIO,
  cabecalhosDeAutorizacao,
  caminhoCodificado,
  enderecoAssinado,
  hashHex,
  textoDaConsulta,
  type CredenciaisS3,
} from "./assinatura-s3.ts";
import { disposicaoDoArquivo } from "./nome-do-arquivo.ts";
import { lerErro, lerIdDoEnvio, lerPartes, montarConclusao, type ParteEnviada } from "./respostas-s3.ts";

/*
 * O depósito dos produtos digitais (Cloudflare R2, API S3).
 *
 * Nenhum byte de arquivo passa pela VPS: o navegador do lojista envia as
 * partes direto para o R2 por endereços assinados, e o comprador baixa direto
 * de lá. O servidor só abre e fecha o envio, confere o tamanho e lê o começo
 * do arquivo para saber o formato real.
 *
 * Sem as variáveis, `configuracaoDoR2()` devolve null e as telas mostram que o
 * envio ainda não está disponível (nada quebra).
 */

export type ConfiguracaoR2 = { host: string; balde: string; cred: CredenciaisS3 };

export function configuracaoDoR2(env: Record<string, string | undefined> = process.env): ConfiguracaoR2 | null {
  const idDaChave = String(env.R2_ACCESS_KEY_ID || "").trim();
  const segredo = String(env.R2_SECRET_ACCESS_KEY || "").trim();
  let balde = String(env.R2_BUCKET_DIGITAIS || "").trim();
  let host = "";
  const endereco = String(env.R2_ENDPOINT || "").trim();
  if (endereco) {
    try {
      const url = new URL(endereco);
      host = url.host;
      const pasta = url.pathname.replace(/^\/+|\/+$/g, "").split("/")[0];
      if (!balde && pasta) balde = pasta;
    } catch {
      host = "";
    }
  }
  const conta = String(env.R2_ACCOUNT_ID || "").trim();
  if (!host && conta) host = `${conta}.r2.cloudflarestorage.com`;
  if (!balde) balde = "ktalogu-digitais";
  if (!idDaChave || !segredo || !host) return null;
  if (!/^[a-z0-9.-]+(:\d+)?$/i.test(host) || !/^[a-z0-9][a-z0-9.-]{1,62}$/.test(balde)) return null;
  return { host, balde, cred: { idDaChave, segredo, regiao: "auto" } };
}

export class ErroDoDeposito extends Error {
  readonly status: number;
  readonly codigo: string;
  constructor(mensagem: string, status: number, codigo: string) {
    super(mensagem);
    this.status = status;
    this.codigo = codigo;
  }
}

function caminhoDoObjeto(cfg: ConfiguracaoR2, chave: string): string {
  return `/${cfg.balde}/${chave}`;
}

async function chamar(
  cfg: ConfiguracaoR2,
  metodo: string,
  chave: string,
  opcoes: { consulta?: Record<string, string>; cabecalhos?: Record<string, string>; corpo?: string; prazoMs?: number } = {},
): Promise<Response> {
  const caminho = caminhoDoObjeto(cfg, chave);
  const corpo = opcoes.corpo;
  const cabecalhos = cabecalhosDeAutorizacao(
    {
      metodo,
      host: cfg.host,
      caminho,
      consulta: opcoes.consulta,
      cabecalhos: opcoes.cabecalhos,
      hashDoCorpo: corpo ? hashHex(corpo) : HASH_DO_CORPO_VAZIO,
    },
    cfg.cred,
  );
  const url = `https://${cfg.host}${caminhoCodificado(caminho)}${textoDaConsulta(opcoes.consulta)}`;
  return fetch(url, {
    method: metodo,
    /* Sem compressão: em arquivo de texto grande o Cloudflare comprime na hora
       e some com o Content-Length, que é como conferimos o tamanho. */
    headers: { ...cabecalhos, "accept-encoding": "identity" },
    body: corpo,
    cache: "no-store",
    signal: AbortSignal.timeout(opcoes.prazoMs ?? 20000),
  });
}

async function falhou(resposta: Response, oQue: string): Promise<never> {
  const texto = await resposta.text().catch(() => "");
  const erro = lerErro(texto);
  throw new ErroDoDeposito(`${oQue}: ${erro?.codigo ?? resposta.status}`, resposta.status, erro?.codigo ?? "erro");
}

/* Abre o envio em partes e devolve o id dele. */
export async function abrirEnvio(cfg: ConfiguracaoR2, chave: string, tipo: string): Promise<string> {
  const resposta = await chamar(cfg, "POST", chave, {
    consulta: { uploads: "" },
    cabecalhos: { "content-type": tipo || "application/octet-stream" },
  });
  if (!resposta.ok) await falhou(resposta, "abrir o envio");
  const id = lerIdDoEnvio(await resposta.text());
  if (!id) throw new ErroDoDeposito("abrir o envio: resposta sem id", 502, "sem_id");
  return id;
}

/* Endereço para o navegador enviar UMA parte (PUT), válido por `segundos`. */
export function enderecoDaParte(cfg: ConfiguracaoR2, chave: string, idDoEnvio: string, numero: number, segundos = 3600): string {
  return enderecoAssinado(
    {
      metodo: "PUT",
      host: cfg.host,
      caminho: caminhoDoObjeto(cfg, chave),
      consulta: { partNumber: String(numero), uploadId: idDoEnvio },
      expiraEmSegundos: segundos,
      hashDoCorpo: CORPO_SEM_ASSINATURA,
    },
    cfg.cred,
  );
}

/* As partes que já chegaram (para retomar um envio que caiu). */
export async function partesRecebidas(cfg: ConfiguracaoR2, chave: string, idDoEnvio: string): Promise<ParteEnviada[]> {
  const todas: ParteEnviada[] = [];
  let marcador: number | null = 0;
  for (let pagina = 0; pagina < 12 && marcador !== null; pagina += 1) {
    const consulta: Record<string, string> = { uploadId: idDoEnvio, "max-parts": "1000" };
    if (marcador) consulta["part-number-marker"] = String(marcador);
    const resposta = await chamar(cfg, "GET", chave, { consulta });
    if (resposta.status === 404) return [];
    if (!resposta.ok) await falhou(resposta, "listar as partes");
    const lido = lerPartes(await resposta.text());
    todas.push(...lido.partes);
    marcador = lido.proximo;
  }
  return todas;
}

/* Fecha o envio: o R2 junta as partes num arquivo só. */
export async function fecharEnvio(
  cfg: ConfiguracaoR2,
  chave: string,
  idDoEnvio: string,
  partes: Array<{ numero: number; etag: string }>,
): Promise<void> {
  const resposta = await chamar(cfg, "POST", chave, {
    consulta: { uploadId: idDoEnvio },
    cabecalhos: { "content-type": "application/xml" },
    corpo: montarConclusao(partes),
    prazoMs: 60000,
  });
  const texto = await resposta.text().catch(() => "");
  /* O S3 pode responder 200 com <Error> no corpo. */
  const erro = lerErro(texto);
  if (!resposta.ok || erro) {
    /* A mensagem do depósito vai junto: "InvalidPart" sozinho não diz qual parte nem por quê. */
    const detalhe = erro?.mensagem ? ` (${erro.mensagem})` : "";
    throw new ErroDoDeposito(`fechar o envio: ${erro?.codigo ?? resposta.status}${detalhe}`, resposta.ok ? 502 : resposta.status, erro?.codigo ?? "erro");
  }
}

/* Desiste do envio e libera as partes já guardadas. */
export async function cancelarEnvio(cfg: ConfiguracaoR2, chave: string, idDoEnvio: string): Promise<void> {
  const resposta = await chamar(cfg, "DELETE", chave, { consulta: { uploadId: idDoEnvio } });
  if (!resposta.ok && resposta.status !== 404) await falhou(resposta, "cancelar o envio");
}

/* Tamanho e tipo do arquivo guardado; null quando não existe. */
export async function fichaDoObjeto(
  cfg: ConfiguracaoR2,
  chave: string,
): Promise<{ tamanho: number; tipo: string | null; etag: string | null } | null> {
  const resposta = await chamar(cfg, "HEAD", chave);
  if (resposta.status === 404) return null;
  if (!resposta.ok) throw new ErroDoDeposito(`conferir o arquivo: ${resposta.status}`, resposta.status, "erro");
  return {
    tamanho: Number(resposta.headers.get("content-length") || 0),
    tipo: resposta.headers.get("content-type"),
    etag: resposta.headers.get("etag"),
  };
}

/* Um pedaço do arquivo (para ler o formato real, o índice do ZIP...). */
export async function lerTrecho(cfg: ConfiguracaoR2, chave: string, inicio: number, fim: number): Promise<Uint8Array> {
  const de = Math.max(0, Math.floor(inicio));
  const ate = Math.max(de, Math.floor(fim));
  if (ate - de > 4 * 1024 * 1024) throw new ErroDoDeposito("trecho grande demais", 400, "trecho");
  const resposta = await chamar(cfg, "GET", chave, { cabecalhos: { range: `bytes=${de}-${ate}` } });
  if (!resposta.ok) await falhou(resposta, "ler o arquivo");
  return new Uint8Array(await resposta.arrayBuffer());
}

export async function apagarObjeto(cfg: ConfiguracaoR2, chave: string): Promise<void> {
  const resposta = await chamar(cfg, "DELETE", chave);
  if (!resposta.ok && resposta.status !== 404) await falhou(resposta, "apagar o arquivo");
}

/*
 * Endereço para baixar ou assistir. Vence em minutos: o comprador nunca
 * recebe um endereço permanente.
 */
export function enderecoParaBaixar(
  cfg: ConfiguracaoR2,
  chave: string,
  opcoes: { nome: string; segundos?: number; abrirNoNavegador?: boolean; tipo?: string | null },
): string {
  const consulta: Record<string, string> = {
    "response-content-disposition": disposicaoDoArquivo(opcoes.nome, Boolean(opcoes.abrirNoNavegador)),
  };
  if (opcoes.tipo) consulta["response-content-type"] = opcoes.tipo;
  return enderecoAssinado(
    {
      metodo: "GET",
      host: cfg.host,
      caminho: caminhoDoObjeto(cfg, chave),
      consulta,
      expiraEmSegundos: opcoes.segundos ?? 900,
    },
    cfg.cred,
  );
}
