import * as kvAtomic from "./kv_atomic.tsx";

type DocumentoExistente = Record<string, any>;

export interface ReservaNumeroSequencial {
  numeroDocumento: string;
  sequencia: number;
  reservaKey: string;
}

interface ReservarNumeroSequencialParams<TDocumento extends DocumentoExistente = DocumentoExistente> {
  empresaId: string;
  modulo: string;
  tipoDocumento: string;
  ano: number | string;
  serieId?: string | null;
  numeroPretendido?: string | null;
  documentosExistentes: TDocumento[];
  carregarDocumentosExistentes?: () => Promise<TDocumento[]>;
  extrairSequencia: (documento: TDocumento, ano: number) => number | null | undefined;
  extrairSequenciaNumeroPretendido?: (numeroDocumento: string, ano: number) => number | null | undefined;
  formatarNumero: (sequencia: number, ano: number) => string;
}

export interface EscopoNumeroDocumento {
  empresaId: string;
  modulo: string;
  tipoDocumento: string;
  ano: number | string;
  serieId?: string | null;
}

const LOCK_TTL_MS = 120_000;
const LOCK_RETRY_MS = 80;
const LOCK_MAX_TENTATIVAS = 150;
const RESERVA_EM_CURSO_TTL_MS = 15 * 60_000;

function slug(valor: unknown): string {
  return String(valor || "padrao")
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "") || "padrao";
}

function chaveBaseReserva(escopo: EscopoNumeroDocumento): string {
  return [
    "document_number_reservation",
    slug(escopo.empresaId),
    slug(escopo.modulo),
    slug(escopo.tipoDocumento),
    slug(escopo.serieId || "padrao"),
    String(Number(escopo.ano)),
  ].join(":");
}

function chaveBloqueio(escopo: EscopoNumeroDocumento): string {
  return chaveBaseReserva(escopo).replace(
    "document_number_reservation:",
    "document_number_lock:",
  );
}

export function construirChaveReservaNumero(
  escopo: EscopoNumeroDocumento,
  sequencia: number,
): string {
  return `${chaveBaseReserva(escopo)}:${String(sequencia).padStart(12, "0")}`;
}

async function adquirirBloqueioNumeracao(
  escopo: EscopoNumeroDocumento,
): Promise<{ key: string; token: string }> {
  const key = chaveBloqueio(escopo);

  for (let tentativa = 0; tentativa < LOCK_MAX_TENTATIVAS; tentativa += 1) {
    const token = crypto.randomUUID();
    const agora = Date.now();
    const resultado = await kvAtomic.createIfAbsent(key, {
      token,
      createdAt: new Date(agora).toISOString(),
      expiresAt: new Date(agora + LOCK_TTL_MS).toISOString(),
    });

    if (resultado.created) {
      return { key, token };
    }

    const actual = await kvAtomic.getValue<{ token?: string; expiresAt?: string }>(key);
    const expirou = !actual?.expiresAt || new Date(actual.expiresAt).getTime() <= Date.now();
    if (actual?.token && expirou) {
      await kvAtomic.deleteKeyIfTokenMatches(key, actual.token).catch(() => false);
      continue;
    }

    await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
  }

  throw new Error("A numeracao deste documento esta a ser actualizada por outro utilizador. Tente novamente.");
}

export async function executarComBloqueioNumeracao<T>(
  escopo: EscopoNumeroDocumento,
  operacao: () => Promise<T>,
): Promise<T> {
  const bloqueio = await adquirirBloqueioNumeracao(escopo);
  try {
    return await operacao();
  } finally {
    await kvAtomic.deleteKeyIfTokenMatches(bloqueio.key, bloqueio.token).catch(() => false);
  }
}

export function removerMarcadorPreview(numeroDocumento?: string | null): string {
  return String(numeroDocumento || "").replace(/\s+\(preview\)$/i, "").trim();
}

export function extrairSequenciaFinalComAno(numeroDocumento: string, ano: number): number | null {
  const texto = removerMarcadorPreview(numeroDocumento);
  const match = texto.match(new RegExp(`(\\d+)\\s*/\\s*${ano}\\s*$`, "i"));
  if (!match) return null;
  const sequencia = Number.parseInt(match[1], 10);
  return Number.isFinite(sequencia) && sequencia > 0 ? sequencia : null;
}

export async function reservarNumeroSequencial<TDocumento extends DocumentoExistente = DocumentoExistente>(
  params: ReservarNumeroSequencialParams<TDocumento>,
): Promise<ReservaNumeroSequencial> {
  const ano = Number(params.ano);
  if (!params.empresaId || !params.tipoDocumento || !Number.isFinite(ano)) {
    throw new Error("Dados insuficientes para reservar numero do documento");
  }

  const escopo: EscopoNumeroDocumento = {
    empresaId: params.empresaId,
    modulo: params.modulo,
    tipoDocumento: params.tipoDocumento,
    serieId: params.serieId,
    ano,
  };

  return executarComBloqueioNumeracao(escopo, async () => {
    const documentosExistentes = params.carregarDocumentosExistentes
      ? await params.carregarDocumentosExistentes()
      : params.documentosExistentes;
    const maiorExistente = (documentosExistentes || []).reduce((maior, documento) => {
      const sequencia = params.extrairSequencia(documento, ano);
      return Number.isFinite(Number(sequencia)) && Number(sequencia) > maior
        ? Number(sequencia)
        : maior;
    }, 0);

    const numeroPretendido = removerMarcadorPreview(params.numeroPretendido);
    const sequenciaPretendida = numeroPretendido
      ? (
        params.extrairSequenciaNumeroPretendido
          ? params.extrairSequenciaNumeroPretendido(numeroPretendido, ano)
          : extrairSequenciaFinalComAno(numeroPretendido, ano)
      )
      : null;

    let sequencia = Number.isFinite(Number(sequenciaPretendida)) && Number(sequenciaPretendida) > maiorExistente
      ? Number(sequenciaPretendida)
      : maiorExistente + 1;

    for (let tentativa = 0; tentativa < 500; tentativa += 1) {
      const numeroDocumento = params.formatarNumero(sequencia, ano);
      const reservaKey = construirChaveReservaNumero(escopo, sequencia);
      const resultado = await kvAtomic.createIfAbsent(reservaKey, {
        empresaId: params.empresaId,
        modulo: params.modulo,
        tipoDocumento: params.tipoDocumento,
        serieId: params.serieId || "padrao",
        ano,
        sequencia,
        numeroDocumento,
        status: "RESERVED",
        reservedAt: new Date().toISOString(),
      });

      if (resultado.created) {
        return { numeroDocumento, sequencia, reservaKey };
      }

      sequencia += 1;
    }

    throw new Error("Nao foi possivel reservar um numero unico para o documento");
  });
}

export async function libertarReservaNumero(reservaKey?: string | null): Promise<void> {
  if (!reservaKey) return;
  await kvAtomic.deleteKey(reservaKey);
}

export async function libertarReservaNumeroDoDocumento(
  escopo: EscopoNumeroDocumento,
  sequencia: number,
): Promise<void> {
  await kvAtomic.deleteKey(construirChaveReservaNumero(escopo, sequencia));
}

export async function restaurarReservaNumeroDoDocumento(
  escopo: EscopoNumeroDocumento,
  sequencia: number,
  numeroDocumento: string,
): Promise<void> {
  const key = construirChaveReservaNumero(escopo, sequencia);
  const resultado = await kvAtomic.createIfAbsent(key, {
    ...escopo,
    serieId: escopo.serieId || "padrao",
    ano: Number(escopo.ano),
    sequencia,
    numeroDocumento,
    status: "RESERVED",
    reservedAt: new Date().toISOString(),
    restored: true,
  });

  if (!resultado.created) {
    const actual = await kvAtomic.getValue<{ numeroDocumento?: string }>(key);
    if (actual?.numeroDocumento !== numeroDocumento) {
      throw new Error("A reserva numerica nao pode ser restaurada porque o numero ja foi reutilizado.");
    }
  }
}

export async function assegurarSemReservaPosterior(
  escopo: EscopoNumeroDocumento,
  sequenciaActual: number,
  sequenciasPersistidas: Iterable<number>,
): Promise<void> {
  const persistidas = new Set(Array.from(sequenciasPersistidas).filter((item) => item > 0));
  const reservas = await kvAtomic.listByPrefix<any>(`${chaveBaseReserva(escopo)}:`);

  for (const reserva of reservas) {
    const sequencia = Number(reserva.value?.sequencia) || Number(reserva.key.split(":").pop());
    if (!Number.isFinite(sequencia) || sequencia <= sequenciaActual || persistidas.has(sequencia)) {
      continue;
    }

    const reservadaEm = new Date(reserva.value?.reservedAt || 0).getTime();
    const stale = !Number.isFinite(reservadaEm) || Date.now() - reservadaEm > RESERVA_EM_CURSO_TTL_MS;
    if (stale) {
      await kvAtomic.deleteKey(reserva.key);
      continue;
    }

    throw new Error("Existe uma emissao posterior em processamento. Aguarde a conclusao e tente novamente.");
  }
}
