import { randomUUID } from "node:crypto";
import { createAdminClient } from "@/lib/supabase-server";
import { avisarLoja } from "@/lib/loja/avisar-loja";
import { custoDoProduto } from "@/lib/precificacao/custo-do-produto";
import { sanitizeProductDescription } from "@/lib/product-description-rich-text";
import {
  coresDosModelosXbz,
  countXbzLogsByActionInRange,
  envioRecenteDaOrdemDeCompra,
  getLatestXbzLogByAction,
  getXbzFornecedorByOwner,
  gravarAjustesDaXbz,
  gravarCatalogoXbz,
  buscarNoCatalogoXbz,
  insertXbzLog,
  insertXbzRequestAttemptLog,
  lerAjustesDaXbz,
  listLegacyXbzIntegrations,
  listPedidosLocaisRecentes,
  listTransportadoras,
  listarPedidosGuardadosXbz,
  registrarRespostaDaChamadaXbz,
  upsertTransportadoras,
  upsertXbzFornecedorCredentials,
  upsertXbzPedido,
} from "@/lib/xbz/xbz.repository";
import {
  agruparPorModelo,
  lerItemDoCatalogo,
  montarProdutoDaXbz,
  ordenarAchados,
  palavrasDaBusca,
  MARGEM_MAXIMA,
  type CorDoCatalogo,
  type ModeloDoCatalogo,
} from "@/lib/xbz/xbz.catalogo";
import { numeroDoPedidoXbz, resumoDoPedidoXbz, statusDoPedidoXbz, valorDoPedidoXbz, type PedidoXbzResumo } from "@/lib/xbz/xbz.pedido";
import { mapearStatusXbz } from "@/lib/xbz/xbz.status";
import { StatusPedido, type XbzCredentials, type XbzFornecedor, type XbzTransportadora } from "@/lib/xbz/xbz.types";

export { mapearStatusXbz };

const BASE_URL = "https://api.minhaxbz.com.br:5001/api/clientes";
const XBZ_DAILY_LIMIT = 24;
const MENSAGEM_DE_COTA =
  "A XBZ libera no máximo 24 consultas por dia para cada CNPJ, e as de hoje acabaram. O contador zera quando o dia virar.";

const xbzErrorMap: Record<number, string> = {
  400: "Requisição inválida para a XBZ.",
  401: "Token ou CNPJ inválidos.",
  403: "SEU CADASTRO ESTÁ TEMPORARIAMENTE BLOQUEADO POR EXCESSO DE REQUISIÇÕES (LIMITE XBZ ATINGIDO). TENTE NOVAMENTE DEPOIS OU CONTATE A XBZ.",
  404: "Pedido não encontrado na XBZ.",
  406: "Produto não identificado.",
  407: "Quantidade inválida.",
  408: "Tipo de entrega não informado.",
  409: "Transportadora ou gravação inválida.",
  410: "Forma de pagamento ausente.",
  411: "Sem estoque disponível.",
  412: "Erro interno na geração do pedido na XBZ.",
  413: "Erro ao vincular produto ao pedido na XBZ.",
  422: "Pedido já em produção e não pode ser cancelado.",
  499: "Erro desconhecido na finalização do pedido na XBZ.",
  500: "Erro interno da API XBZ.",
};

function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function cleanCnpj(value: string) {
  return value.replace(/\D/g, "").slice(0, 14);
}

function maskToken(token: string) {
  if (!token) return "";
  if (token.length <= 8) return "********";
  return `${token.slice(0, 4)}••••${token.slice(-4)}`;
}

function normalizeRecord(value: unknown) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
  return value as Record<string, unknown>;
}

function normalizeRecordArray(value: unknown) {
  if (!Array.isArray(value)) return [];
  return value.filter((item) => item && typeof item === "object") as Array<Record<string, unknown>>;
}

function parseTextJson(raw: string) {
  if (!raw) return null;
  try {
    return JSON.parse(raw) as unknown;
  } catch {
    return null;
  }
}

function readString(record: Record<string, unknown>, keys: string[]) {
  for (const key of keys) {
    if (typeof record[key] === "string" && String(record[key]).trim()) return String(record[key]).trim();
  }
  return "";
}

function readNumber(record: Record<string, unknown>, keys: string[]) {
  for (const key of keys) {
    const value = record[key];
    if (typeof value === "number" && Number.isFinite(value)) return value;
    if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
  }
  return null;
}

function mensagemDe(erro: unknown, padrao: string) {
  return erro instanceof Error && erro.message ? erro.message : padrao;
}

function parseXbzError(status: number, parsed: unknown) {
  const body = normalizeRecord(parsed);
  const message =
    readString(body, ["message", "error", "mensagensDiversasSistema", "detalhes"]) ||
    xbzErrorMap[status] ||
    `Falha na API XBZ (${status}).`;
  return message;
}

function getSaoPauloDayRange(reference = new Date()) {
  const offsetMs = -3 * 60 * 60 * 1000;
  const local = new Date(reference.getTime() + offsetMs);
  const year = local.getUTCFullYear();
  const month = local.getUTCMonth();
  const day = local.getUTCDate();
  const startLocalUtc = Date.UTC(year, month, day, 0, 0, 0, 0);
  const endLocalUtc = Date.UTC(year, month, day + 1, 0, 0, 0, 0);
  const startIso = new Date(startLocalUtc - offsetMs).toISOString();
  const endIso = new Date(endLocalUtc - offsetMs).toISOString();
  return { startIso, endIso };
}

/* Erro que já é resposta final: insistir não muda o resultado e cada nova
   tentativa consome mais uma das 24 chamadas diárias do CNPJ. */
class ErroXbzSemRetentativa extends Error {
  constructor(
    message: string,
    readonly status?: number
  ) {
    super(message);
  }
}

/* A resposta de erro da XBZ, com o código HTTP: o pedido precisa saber se o
   erro foi recusa (nada criado) ou falha no meio da geração. */
export class ErroDaXbz extends Error {
  constructor(
    message: string,
    readonly status?: number
  ) {
    super(message);
  }
}

/* A XBZ não chegou a responder (tempo esgotado ou conexão caída). Num pedido
   isso é perigoso: ela pode ter criado o pedido e a resposta se perdido. */
export class ErroXbzSemResposta extends Error {}

async function ensureXbzDailyQuota(admin: ReturnType<typeof createAdminClient>, fornecedorId: string) {
  const { startIso, endIso } = getSaoPauloDayRange();
  const used = await countXbzLogsByActionInRange(admin, fornecedorId, "xbzRequest", startIso, endIso);
  if (used >= XBZ_DAILY_LIMIT) {
    throw new ErroXbzSemRetentativa(MENSAGEM_DE_COTA);
  }
  return { used, startIso, endIso };
}

type OpcoesDaChamada = {
  /* Pedido novo nunca é repetido sozinho: se a XBZ criou o primeiro e a
     resposta se perdeu, a segunda tentativa cria outro. Em abril de 2026 uma
     empresa teve três envios repetidos três vezes cada por esta função. */
  repetir?: boolean;
  tempoMs?: number;
  /* O GetListaDeProdutos recebe CNPJ e token no endereço. O registro guarda
     só o caminho, sem eles. */
  credenciaisNoEndereco?: boolean;
  /* Vai junto no registro da tentativa (a ordem de compra do pedido: é ela
     que trava o envio em dobro). */
  registro?: Record<string, unknown>;
};

export async function requestXbz(
  fornecedorId: string,
  path: string,
  method: "GET" | "POST" | "PUT",
  credentials: XbzCredentials,
  body?: Record<string, unknown>,
  opcoes: OpcoesDaChamada = {}
) {
  const maxAttempts = opcoes.repetir === false ? 1 : 3;
  const tempoMs = opcoes.tempoMs ?? 30_000;
  let lastMessage = "Falha ao chamar API XBZ.";
  const admin = createAdminClient();

  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
    try {
      await ensureXbzDailyQuota(admin, fornecedorId);
      const registradaEm = await insertXbzRequestAttemptLog(admin, fornecedorId, {
        path,
        method,
        attempt,
        ...(opcoes.registro ?? {}),
      });
      const endereco = new URL(`${BASE_URL}${path}`);
      if (opcoes.credenciaisNoEndereco) {
        endereco.searchParams.set("cnpj", credentials.cnpj);
        endereco.searchParams.set("token", credentials.token);
      }

      /* O prazo vale até o fim da leitura: o catálogo tem 15 MB e pode parar
         no meio do caminho. */
      let response: Response;
      let text: string;
      try {
        response = await fetch(endereco, {
          method,
          headers: {
            "Content-Type": "application/json",
            cnpj: credentials.cnpj,
            token: credentials.token,
          },
          body: body ? JSON.stringify(body) : undefined,
          cache: "no-store",
          signal: AbortSignal.timeout(tempoMs),
        });
        text = await response.text();
      } catch (erro) {
        const esgotou = erro instanceof Error && (erro.name === "TimeoutError" || erro.name === "AbortError");
        await registrarRespostaDaChamadaXbz(admin, fornecedorId, registradaEm, { erro: esgotou ? "tempo" : "conexao" }).catch(() => null);
        throw new ErroXbzSemResposta(esgotou ? "A XBZ não respondeu a tempo." : "Não foi possível falar com a XBZ agora.");
      }

      const parsed = parseTextJson(text);
      const detail = response.ok ? null : parseXbzError(response.status, parsed);
      await registrarRespostaDaChamadaXbz(admin, fornecedorId, registradaEm, {
        status: response.status,
        ...(detail ? { erro: detail.slice(0, 300) } : {}),
      }).catch(() => null);

      if (response.ok) {
        return parsed;
      }
      lastMessage = detail || lastMessage;

      /* Só erro de servidor (5xx) merece nova tentativa. O 403 da XBZ é
         exatamente "cadastro bloqueado por excesso de requisições": repetir
         queimava mais duas das 24 chamadas do dia e piorava o bloqueio que
         causou o erro. Token inválido e regra de negócio também não mudam
         de resposta ao repetir. */
      if (response.status >= 500 && attempt < maxAttempts) {
        await sleep(500 * attempt);
        continue;
      }

      throw new ErroXbzSemRetentativa(lastMessage, response.status);
    } catch (error) {
      if (error instanceof ErroXbzSemRetentativa) throw new ErroDaXbz(error.message, error.status);
      if (error instanceof ErroXbzSemResposta && attempt >= maxAttempts) throw error;
      lastMessage = mensagemDe(error, "Erro de rede na integração XBZ.");
      if (attempt < maxAttempts) {
        await sleep(500 * attempt);
        continue;
      }
      throw new Error(lastMessage);
    }
  }

  throw new Error(lastMessage);
}

async function getFornecedorOrThrow(ownerId: string) {
  const admin = createAdminClient();
  const fornecedor = await getXbzFornecedorByOwner(admin, ownerId);
  if (!fornecedor) {
    throw new Error("Configure CNPJ e token da XBZ antes de executar esta ação.");
  }
  return fornecedor;
}

const credenciaisDe = (fornecedor: XbzFornecedor): XbzCredentials => ({ cnpj: fornecedor.cnpj, token: fornecedor.token });

function mapTransportadoras(raw: unknown, tipo: 30 | 40): XbzTransportadora[] {
  return normalizeRecordArray(raw).flatMap((item) => {
    const idXbz = readNumber(item, ["id"]);
    if (!idXbz) return [];
    const nome = readString(item, ["razaoSocialNome", "nomeFantasia", "nome"]);
    if (!nome) return [];
    return [{ idXbz, nome, tipo }];
  });
}

export async function salvarCredenciaisXbz(ownerId: string, cnpj: string, token: string) {
  const clean = cleanCnpj(cnpj);
  const normalizedToken = String(token || "").trim();
  if (clean.length !== 14) throw new Error("CNPJ inválido. Use 14 dígitos.");
  if (!normalizedToken) throw new Error("Token é obrigatório.");
  const admin = createAdminClient();
  const fornecedor = await upsertXbzFornecedorCredentials(admin, ownerId, clean, normalizedToken);
  if (!fornecedor) throw new Error("Falha ao salvar credenciais XBZ.");
  await insertXbzLog(admin, {
    fornecedorId: fornecedor.id,
    action: "saveCredentials",
    payload: { cnpj: clean },
    response: { ok: true },
  });
  return {
    cnpj: fornecedor.cnpj,
    tokenMasked: maskToken(fornecedor.token),
    updatedAt: fornecedor.updatedAt,
  };
}

export async function testConnectionXbz(ownerId: string) {
  const admin = createAdminClient();
  const fornecedor = await getFornecedorOrThrow(ownerId);
  const response = await requestXbz(fornecedor.id, "/EntregaTipoListar", "GET", credenciaisDe(fornecedor));
  await insertXbzLog(admin, {
    fornecedorId: fornecedor.id,
    action: "testConnection",
    payload: {},
    response: {
      ok: true,
      sampleCount: normalizeRecordArray(response).length,
    },
  });
  return { ok: true };
}

export async function syncAuxiliaryXbz(ownerId: string) {
  const admin = createAdminClient();
  const fornecedor = await getFornecedorOrThrow(ownerId);
  const credentials = credenciaisDe(fornecedor);

  const [deliveryTypes, paymentTypes, transportadorasRaw, gravacoesRaw] = await Promise.all([
    requestXbz(fornecedor.id, "/EntregaTipoListar", "GET", credentials),
    requestXbz(fornecedor.id, "/PagamentoTipoListar", "GET", credentials),
    requestXbz(fornecedor.id, "/EmpresasGravacaoTransportadoraListar?tipo=30", "GET", credentials),
    requestXbz(fornecedor.id, "/EmpresasGravacaoTransportadoraListar?tipo=40", "GET", credentials),
  ]);

  const transportadoras = mapTransportadoras(transportadorasRaw, 30);
  const gravacoes = mapTransportadoras(gravacoesRaw, 40);
  await upsertTransportadoras(admin, fornecedor.id, [...transportadoras, ...gravacoes]);

  const responsePayload = {
    deliveryTypes: normalizeRecordArray(deliveryTypes),
    paymentTypes: normalizeRecordArray(paymentTypes),
    transportadoras: transportadoras.map((item) => ({ id: item.idXbz, nome: item.nome, tipo: item.tipo })),
    gravacoes: gravacoes.map((item) => ({ id: item.idXbz, nome: item.nome, tipo: item.tipo })),
  };
  await insertXbzLog(admin, {
    fornecedorId: fornecedor.id,
    action: "syncAuxiliary",
    payload: {},
    response: responsePayload,
  });

  return responsePayload;
}

/*
  Salvar, conferir e trazer as listas viraram um passo só.

  Eram três botões — "Salvar", "Testar conexão" e "Sincronizar auxiliares" — e
  os dois últimos não são decisão de ninguém: quem acabou de digitar CNPJ e
  token quer saber se funcionou e já sair usando. Como a sincronização dos
  auxiliares começa pelo mesmo endereço que o teste usava, ela sozinha
  responde as duas perguntas e economiza uma das 24 chamadas do dia.
*/
export async function salvarCredenciaisEConectarXbz(ownerId: string, cnpj: string, token: string) {
  const credenciais = await salvarCredenciaisXbz(ownerId, cnpj, token);

  try {
    const cache = await syncAuxiliaryXbz(ownerId);
    return {
      credenciais,
      conectado: true,
      mensagem: "Conectado à XBZ. Já trouxemos as formas de entrega, de pagamento e as transportadoras.",
      resumo: {
        entregas: cache.deliveryTypes.length,
        pagamentos: cache.paymentTypes.length,
        transportadoras: cache.transportadoras.length,
        gravacoes: cache.gravacoes.length,
      },
    };
  } catch (error) {
    /* As credenciais ficam salvas de propósito: se o problema for o token, o
       dono corrige uma letra e salva de novo sem redigitar tudo. */
    const motivo = mensagemDe(error, "Não foi possível falar com a XBZ agora.");
    return {
      credenciais,
      conectado: false,
      mensagem: `Os dados foram guardados, mas a XBZ não respondeu: ${motivo}`,
      resumo: { entregas: 0, pagamentos: 0, transportadoras: 0, gravacoes: 0 },
    };
  }
}

/* ================================================================ catálogo */

/*
  A recomendação do próprio desenvolvedor da API da XBZ (05/10/2026): baixar
  a lista inteira e trabalhar em cima dela, e não perguntar a cada busca. Cada
  download traz preço e estoque atualizados e custa uma das 24 consultas do
  dia; a busca, o detalhe e o cadastro leem a cópia guardada, de graça.

  A cópia vale por 6 horas. Quem busca com ela vencida recebe na hora o que
  já está guardado e a atualização corre por trás (leva uns 30 segundos:
  são 15 MB e quase 12 mil itens); a tela pergunta de tempos em tempos se
  terminou. "Atualizar agora" força, no máximo uma vez a cada 30 minutos.
*/
const CATALOGO_VALE_POR_MS = 6 * 60 * 60 * 1000;
const ATUALIZACAO_MANUAL_A_CADA_MS = 30 * 60 * 1000;
/* Depois de uma falha, a busca espera isto antes de tentar baixar sozinha de
   novo: falha repetida a cada busca queimaria as consultas do dia. */
const ESPERA_DEPOIS_DE_FALHA_MS = 15 * 60 * 1000;
const MODELOS_NA_BUSCA = 40;

type AtualizacaoDoCatalogo = { rodando: boolean; quando: number; erro: string | null };

declare global {
  var __catalogoDaXbz: Map<string, AtualizacaoDoCatalogo> | undefined;
}

const atualizacoes = () => (globalThis.__catalogoDaXbz ??= new Map<string, AtualizacaoDoCatalogo>());

export type SituacaoDoCatalogoXbz = {
  atualizadoEm: string | null;
  itens: number;
  modelos: number;
  atualizando: boolean;
  erro: string | null;
  /* Quando o "Atualizar agora" volta a valer (antes disso, a cópia é nova demais). */
  podeAtualizarEm: string | null;
};

async function situacaoDoCatalogo(admin: ReturnType<typeof createAdminClient>, fornecedorId: string): Promise<SituacaoDoCatalogoXbz> {
  const ultimo = await getLatestXbzLogByAction(admin, fornecedorId, "syncCatalogo");
  const estado = atualizacoes().get(fornecedorId);
  const atualizadoEm = ultimo?.createdAt ?? null;
  const liberaEm = atualizadoEm ? new Date(new Date(atualizadoEm).getTime() + ATUALIZACAO_MANUAL_A_CADA_MS) : null;
  const falhaRecente = estado && !estado.rodando && estado.erro && Date.now() - estado.quando < ESPERA_DEPOIS_DE_FALHA_MS;
  return {
    atualizadoEm,
    itens: Number(ultimo?.response.itens ?? 0) || 0,
    modelos: Number(ultimo?.response.modelos ?? 0) || 0,
    atualizando: Boolean(estado?.rodando),
    erro: falhaRecente ? estado.erro : null,
    podeAtualizarEm: liberaEm && liberaEm.getTime() > Date.now() ? liberaEm.toISOString() : null,
  };
}

const catalogoVencido = (situacao: SituacaoDoCatalogoXbz) =>
  !situacao.atualizadoEm || Date.now() - new Date(situacao.atualizadoEm).getTime() > CATALOGO_VALE_POR_MS;

async function baixarCatalogo(fornecedor: XbzFornecedor) {
  const admin = createAdminClient();
  const inicio = Date.now();
  const momento = new Date(inicio).toISOString();
  /* Sem repetir: cada tentativa é uma das 24 consultas, e a próxima busca
     tenta de novo se esta falhar. */
  const bruto = await requestXbz(fornecedor.id, "/GetListaDeProdutos", "GET", credenciaisDe(fornecedor), undefined, {
    tempoMs: 120_000,
    credenciaisNoEndereco: true,
    repetir: false,
  });
  if (!Array.isArray(bruto)) throw new Error("A XBZ devolveu o catálogo num formato que não conhecemos.");

  const itens: Array<{ bruto: Record<string, unknown>; cor: CorDoCatalogo }> = [];
  for (const item of bruto) {
    const cor = lerItemDoCatalogo(item);
    if (cor) itens.push({ bruto: item as Record<string, unknown>, cor });
  }
  if (!itens.length) throw new Error("A XBZ devolveu o catálogo vazio.");

  const anterior = await getLatestXbzLogByAction(admin, fornecedor.id, "syncCatalogo");
  /* Só apaga o que saiu do catálogo se a lista veio inteira: uma resposta
     pela metade não pode esvaziar a cópia guardada. */
  const itensAntes = Number(anterior?.response.itens ?? 0) || 0;
  const { gravadas, apagadas } = await gravarCatalogoXbz(admin, fornecedor.id, itens, momento, itens.length >= itensAntes * 0.5);

  await insertXbzLog(admin, {
    fornecedorId: fornecedor.id,
    action: "syncCatalogo",
    payload: {},
    response: {
      itens: itens.length,
      modelos: new Set(itens.map(({ cor }) => cor.modelo)).size,
      comPreco: itens.filter(({ cor }) => cor.preco !== null).length,
      comEstoque: itens.filter(({ cor }) => cor.estoque > 0).length,
      gravadas,
      apagadas,
      duracaoMs: Date.now() - inicio,
    },
  });
}

/* Começa a atualização por trás, se ela já não estiver correndo. Volta na hora. */
function iniciarAtualizacaoDoCatalogo(fornecedor: XbzFornecedor) {
  const mapa = atualizacoes();
  if (mapa.get(fornecedor.id)?.rodando) return;
  mapa.set(fornecedor.id, { rodando: true, quando: Date.now(), erro: null });
  void baixarCatalogo(fornecedor)
    .then(() => mapa.set(fornecedor.id, { rodando: false, quando: Date.now(), erro: null }))
    .catch((erro) => {
      const mensagem = mensagemDe(erro, "Não foi possível baixar o catálogo da XBZ agora.");
      console.error("[xbz] catálogo não atualizado:", fornecedor.id, mensagem);
      mapa.set(fornecedor.id, { rodando: false, quando: Date.now(), erro: mensagem });
    });
}

export async function situacaoDoCatalogoXbz(ownerId: string) {
  const fornecedor = await getFornecedorOrThrow(ownerId);
  return situacaoDoCatalogo(createAdminClient(), fornecedor.id);
}

/** "Atualizar agora": baixa de novo, salvo se a cópia tem menos de 30 minutos. */
export async function atualizarCatalogoXbz(ownerId: string) {
  const admin = createAdminClient();
  const fornecedor = await getFornecedorOrThrow(ownerId);
  const situacao = await situacaoDoCatalogo(admin, fornecedor.id);
  if (situacao.atualizando) return situacao;
  if (situacao.podeAtualizarEm) {
    const hora = new Intl.DateTimeFormat("pt-BR", { hour: "2-digit", minute: "2-digit", timeZone: "America/Sao_Paulo" }).format(
      new Date(situacao.podeAtualizarEm)
    );
    throw new Error(`O catálogo acabou de ser atualizado. Para poupar as consultas do dia, dá para atualizar de novo a partir das ${hora}.`);
  }
  iniciarAtualizacaoDoCatalogo(fornecedor);
  return { ...situacao, atualizando: true, erro: null };
}

/**
 * A busca no catálogo guardado. Não gasta consulta da XBZ; com a cópia
 * vencida (ou sem cópia), dispara a atualização por trás e responde com o que
 * tiver.
 */
export async function buscarProdutosXbz(ownerId: string, busca: string) {
  const admin = createAdminClient();
  const fornecedor = await getFornecedorOrThrow(ownerId);
  const palavras = palavrasDaBusca(busca);
  /* A situação e a busca ao mesmo tempo: sem cópia guardada, a busca só
     volta vazia. */
  const [situacao, brutas] = await Promise.all([
    situacaoDoCatalogo(admin, fornecedor.id),
    palavras.length ? buscarNoCatalogoXbz(admin, fornecedor.id, palavras) : Promise.resolve([]),
  ]);
  if (!situacao.atualizando && catalogoVencido(situacao) && !situacao.erro) {
    iniciarAtualizacaoDoCatalogo(fornecedor);
    situacao.atualizando = true;
  }

  let modelos: ModeloDoCatalogo[] = [];
  if (brutas.length) {
    const achadas = brutas.map(lerItemDoCatalogo).filter((cor): cor is CorDoCatalogo => Boolean(cor));
    const primeiros = ordenarAchados(agruparPorModelo(achadas), palavras).slice(0, MODELOS_NA_BUSCA);
    /* A busca acha a cor ("caneca azul" acha só a azul); a tela mostra o
       modelo inteiro, com todas as cores. */
    const inteiros = new Map(
      agruparPorModelo(
        (await coresDosModelosXbz(admin, fornecedor.id, primeiros.map((modelo) => modelo.codigo)))
          .map(lerItemDoCatalogo)
          .filter((cor): cor is CorDoCatalogo => Boolean(cor))
      ).map((modelo) => [modelo.codigo, modelo])
    );
    modelos = primeiros.map((modelo) => inteiros.get(modelo.codigo) ?? modelo);
  }

  /* Sem registro por busca: a busca anda a cada letra digitada, não gasta
     consulta da XBZ, e cada uma virava uma linha no registro. */
  return { modelos, catalogo: situacao };
}

/* ================================================================ cadastro */

export type EntradaDoCadastroXbz = {
  modelo: string;
  cores?: string[] | null;
  margem: number;
  nome?: string | null;
};

/* Cada cor da XBZ tem a sua foto; um modelo chega a 24 cores. */
const FOTOS_POR_PRODUTO = 30;

/**
 * Cadastra um modelo da XBZ no catálogo da empresa, a partir da cópia
 * guardada (o navegador manda só o código e as escolhas: preço, peso e o
 * resto vêm daqui, e não do que a tela disse).
 *
 * As fotos vão para a mesma fila da importação de lojas: são baixadas,
 * reduzidas e guardadas na Ktalogu. Antes o produto apontava para a foto no
 * site da XBZ, que a loja do cliente mostrava com o endereço do fornecedor.
 */
export async function cadastrarModeloXbz(
  dono: { ownerId: string; companyId: string; userId: string },
  entrada: EntradaDoCadastroXbz,
  request?: Request
) {
  const admin = createAdminClient();
  const fornecedor = await getFornecedorOrThrow(dono.ownerId);
  const codigo = String(entrada.modelo || "").trim();
  if (!codigo) throw new Error("Escolha o produto da XBZ que vai para o catálogo.");

  const [modelo] = agruparPorModelo(
    (await coresDosModelosXbz(admin, fornecedor.id, [codigo])).map(lerItemDoCatalogo).filter((cor): cor is CorDoCatalogo => Boolean(cor))
  );
  if (!modelo) throw new Error("Esse produto não está no catálogo guardado da XBZ. Atualize o catálogo e tente de novo.");
  const margem = Math.min(MARGEM_MAXIMA, Math.max(0, Number(entrada.margem)));
  const produto = montarProdutoDaXbz(modelo, { cores: entrada.cores, margem, nome: entrada.nome });

  const origemChave = `xbz:${modelo.codigo}`;
  const [{ data: jaVeio }, { data: antigos }] = await Promise.all([
    admin.from("importacao_itens").select("product_id,products(id,name)").eq("company_id", dono.companyId).eq("origem_chave", origemChave).maybeSingle(),
    /* Cadastro do jeito antigo (uma cor por produto, com o código XBZ no SKU). */
    admin
      .from("products")
      .select("id,name,sku")
      .eq("user_id", dono.ownerId)
      .in(
        "sku",
        modelo.cores.map((cor) => cor.codigoXbz)
      )
      .limit(5),
  ]);
  const existente = jaVeio ? (Array.isArray(jaVeio.products) ? jaVeio.products[0] : jaVeio.products) : null;
  if (existente?.id) return { jaExistia: true as const, id: String(existente.id), nome: String(existente.name ?? produto.nome) };

  const { data: lote, error: erroDoLote } = await admin
    .from("importacoes")
    .insert({ company_id: dono.companyId, dono_id: dono.ownerId, usuario_id: dono.userId, origem: "xbzbrindes.com.br", plataforma: "XBZ" })
    .select("id")
    .single();
  if (erroDoLote || !lote) throw new Error(`Não foi possível começar o cadastro: ${erroDoLote?.message ?? "sem resposta do banco"}`);
  const loteId = String((lote as { id: string }).id);

  /* As fotos: a principal do modelo primeiro, depois a de cada cor, sem repetir. */
  const fotos: Array<{ id: string; endereco: string; posicao: number }> = [];
  const fotoDoEndereco = new Map<string, string>();
  const principal = produto.cores.find((cor) => cor.imagem === modelo.imagem)?.imagem ?? null;
  for (const endereco of [principal, ...produto.cores.map((cor) => cor.imagem)]) {
    if (!endereco || fotoDoEndereco.has(endereco) || fotos.length >= FOTOS_POR_PRODUTO) continue;
    const id = randomUUID();
    fotoDoEndereco.set(endereco, id);
    fotos.push({ id, endereco, posicao: fotos.length });
  }

  const productId = randomUUID();
  const custo = produto.custo ?? 0;
  const { error: erroDoProduto } = await admin.from("products").insert({
    id: productId,
    user_id: dono.ownerId,
    name: produto.nome,
    description: sanitizeProductDescription(produto.descricao) || null,
    price: produto.preco,
    price_on_request: produto.sobConsulta,
    use_variation_price_only: false,
    unit_type: "unit",
    category_id: null,
    estimated_base_cost: custo,
    cost_total: custoDoProduto({ estimatedBaseCost: custo, somaDaFicha: 0, unitType: "unit" }),
    sku: produto.sku,
    barcode: null,
    image_url: null,
    /* Sem preço na XBZ não entra na loja: vender por zero é pior que não mostrar. */
    show_in_catalog: !produto.sobConsulta,
    active: true,
    is_catalog_exclusive: false,
    /* O estoque da XBZ é dela, e muda todo dia: virar estoque da gráfica
       travava a venda (o cadastro antigo punha zero e ligava o controle, e o
       produto nascia esgotado). Ele fica à vista na tela da XBZ. */
    stock: 0,
    track_inventory: false,
    inventory_scope: "product",
    inventory_attribute_name: null,
    allow_negative_stock: false,
    min_sale_qty_enabled: produto.quantidadeMinima !== null,
    min_sale_qty: produto.quantidadeMinima,
    max_sale_qty_enabled: false,
    max_sale_qty: null,
    allowed_sale_qty_enabled: false,
    allowed_sale_quantities: [],
    shipping_weight_kg: produto.pesoKg,
    shipping_height_cm: produto.alturaCm,
    shipping_width_cm: produto.larguraCm,
    shipping_length_cm: produto.comprimentoCm,
    ncm: produto.ncm,
  });
  if (erroDoProduto) {
    await admin.from("importacoes").delete().eq("id", loteId);
    throw new Error(`O produto não foi cadastrado: ${erroDoProduto.message}`);
  }

  /* As cores, o registro de origem e a fila das fotos dependem só do
     produto: vão juntos. */
  const avisos: string[] = [];
  const opcoes = produto.cores.map((cor, posicao) => ({
    id: randomUUID(),
    label: cor.rotulo,
    price: cor.acrescimo,
    price_applies_per_quantity: true,
    stock: 0,
    is_required: false,
    position: posicao,
    image_id: cor.imagem ? (fotoDoEndereco.get(cor.imagem) ?? null) : null,
    minimum_stock: 0,
    reorder_point: 0,
  }));
  const [cores, item, filaDasFotos] = await Promise.all([
    produto.cores.length >= 2
      ? admin.from("product_attributes").insert({
          product_id: productId,
          name: "Cor",
          options: opcoes,
          position: 0,
          allow_multiple: false,
          is_required: true,
          show_in_store: true,
        })
      : Promise.resolve({ error: null }),
    admin
      .from("importacao_itens")
      .insert({ importacao_id: loteId, company_id: dono.companyId, product_id: productId, origem_chave: origemChave, nome: produto.nome }),
    fotos.length
      ? admin.from("importacao_fotos").insert(
          fotos.map((foto) => ({
            id: foto.id,
            importacao_id: loteId,
            company_id: dono.companyId,
            dono_id: dono.ownerId,
            product_id: productId,
            endereco: foto.endereco,
            posicao: foto.posicao,
          }))
        )
      : Promise.resolve({ error: null }),
  ]);
  if (cores.error) avisos.push(`As cores não foram gravadas (${cores.error.message.slice(0, 120)}). O produto ficou sem elas.`);
  if (item.error && item.error.code !== "23505") console.error("[xbz] cadastro sem registro de origem:", productId, item.error.message);
  if (filaDasFotos.error) avisos.push(`As fotos não entraram na fila (${filaDasFotos.error.message.slice(0, 120)}).`);
  const fotosNaFila = fotos.length && !filaDasFotos.error ? fotos.length : 0;
  if (fotosNaFila) {
    const { acordarFilaDeFotos } = await import("@/lib/assistentes/importacao/fila-de-fotos");
    acordarFilaDeFotos();
  }
  avisarLoja(dono.ownerId, request);

  for (const antigo of (antigos ?? []) as Array<{ id: string; name: string | null; sku: string | null }>) {
    avisos.push(`Você já tinha cadastrado uma cor deste produto antes ("${antigo.name ?? antigo.sku}"). Confira se quer manter os dois.`);
  }

  await Promise.all([
    admin.from("importacoes").update({ produtos: 1, fotos: fotosNaFila }).eq("id", loteId),
    gravarAjustesDaXbz(admin, dono.ownerId, { margemDeVenda: margem }).catch(() => null),
    insertXbzLog(admin, {
      fornecedorId: fornecedor.id,
      action: "importProduct",
      payload: { modelo: modelo.codigo, cores: produto.cores.map((cor) => cor.codigoXbz), margem },
      response: { productId, created: true, sku: produto.sku, price: produto.preco, custo: produto.custo, fotos: fotosNaFila },
    }).catch(() => null),
  ]);

  return {
    jaExistia: false as const,
    id: productId,
    nome: produto.nome,
    preco: produto.preco,
    custo: produto.custo,
    cores: produto.cores.length,
    fotos: fotosNaFila,
    naLoja: !produto.sobConsulta,
    avisos,
  };
}

/* ================================================================= pedidos */

export async function criarPedidoXbz(
  ownerId: string,
  payload: {
    pedidoLocalId?: string;
    ordemDeCompra: string;
    pagamentoTipoId: number;
    entregaTipoId: number;
    entregaTransportadoraGravacaoId?: number | null;
    produtoCodigoXbz: string;
    quantidade: number;
    detalhes?: string;
  }
) {
  const admin = createAdminClient();
  const fornecedor = await getFornecedorOrThrow(ownerId);

  const requestPayload = {
    ordemDeCompra: String(payload.ordemDeCompra || "").trim(),
    pagamentoTipoId: Number(payload.pagamentoTipoId),
    entregaTipoId: Number(payload.entregaTipoId),
    entregaTransportadoraGravacaoId:
      typeof payload.entregaTransportadoraGravacaoId === "number" ? payload.entregaTransportadoraGravacaoId : undefined,
    produtoCodigoXbz: String(payload.produtoCodigoXbz || "").trim(),
    quantidade: Number(payload.quantidade),
    detalhes: String(payload.detalhes || "").trim(),
  };

  if (!requestPayload.ordemDeCompra || !requestPayload.produtoCodigoXbz || !(requestPayload.quantidade > 0)) {
    throw new Error("Preencha ordem de compra, produto e quantidade válida.");
  }
  if (!Number.isInteger(requestPayload.quantidade)) throw new Error("A quantidade precisa ser um número inteiro de peças.");
  if (!Number.isFinite(requestPayload.pagamentoTipoId) || !Number.isFinite(requestPayload.entregaTipoId)) {
    throw new Error("Pagamento e entrega são obrigatórios.");
  }
  if (
    (requestPayload.entregaTipoId === 30 || requestPayload.entregaTipoId === 40) &&
    !requestPayload.entregaTransportadoraGravacaoId
  ) {
    throw new Error("Transportadora/Gravação é obrigatória para os tipos de entrega 30 e 40.");
  }

  /* A mesma ordem de compra mandada há pouco é, quase sempre, o mesmo pedido
     tentado de novo depois de um erro na tela. A XBZ não barra: cria outro. */
  const desde = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString();
  const anterior = await envioRecenteDaOrdemDeCompra(admin, fornecedor.id, requestPayload.ordemDeCompra, desde);
  /* Recusa clara da XBZ (sem estoque, quantidade inválida...) não criou
     nada: dá para tentar de novo com a mesma ordem de compra. */
  const statusAnterior = Number(anterior?.resposta.status);
  const recusaClara = Number.isFinite(statusAnterior) && statusAnterior >= 400 && statusAnterior <= 411;
  if (anterior && !recusaClara) {
    const quando = new Intl.DateTimeFormat("pt-BR", { dateStyle: "short", timeStyle: "short", timeZone: "America/Sao_Paulo" }).format(
      new Date(anterior.quando)
    );
    throw new Error(
      `Um pedido com a ordem de compra ${requestPayload.ordemDeCompra} já foi mandado para a XBZ em ${quando}. Confira a lista de pedidos antes de mandar de novo; se for outro pedido, use outra ordem de compra.`
    );
  }

  let resultRaw: unknown;
  try {
    resultRaw = await requestXbz(fornecedor.id, "/PedidoEnviar", "POST", credenciaisDe(fornecedor), requestPayload, {
      repetir: false,
      tempoMs: 60_000,
      registro: { ordemDeCompra: requestPayload.ordemDeCompra },
    });
  } catch (erro) {
    /* Sem resposta, ou "erro interno na geração/finalização do pedido"
       (412, 413, 499 e 5xx): a XBZ pode ter criado o pedido pela metade. */
    const talvezCriado =
      erro instanceof ErroXbzSemResposta || (erro instanceof ErroDaXbz && typeof erro.status === "number" && (erro.status >= 500 || [412, 413, 499].includes(erro.status)));
    if (talvezCriado) {
      throw new Error(
        `${erro instanceof Error ? erro.message : "A XBZ não confirmou o pedido."} O pedido pode ter sido criado mesmo assim: atualize a lista de pedidos antes de tentar de novo, para não comprar duas vezes.`
      );
    }
    throw erro;
  }
  const result = normalizeRecord(resultRaw);
  const numeroXbz = numeroDoPedidoXbz(result);
  const statusXbz = statusDoPedidoXbz(result);

  /* O registro primeiro: daqui em diante o número do pedido não se perde
     mesmo que guardar o pedido falhe. */
  await insertXbzLog(admin, {
    fornecedorId: fornecedor.id,
    action: "createOrder",
    payload: requestPayload,
    response: result,
  }).catch((erro) => console.error("[xbz] pedido sem registro:", numeroXbz, mensagemDe(erro, "")));

  /* O pedido já existe na XBZ. Falhar ao guardar aqui não pode virar "falha"
     na tela: a pessoa mandaria de novo e compraria duas vezes. */
  let guardado = true;
  try {
    await upsertXbzPedido(admin, {
      fornecedorId: fornecedor.id,
      pedidoLocalId: payload.pedidoLocalId ? String(payload.pedidoLocalId) : null,
      numeroXbz,
      statusXbz,
      statusMapeado: mapearStatusXbz(statusXbz || "PEDIDO EMITIDO"),
      valorTotal: valorDoPedidoXbz(result),
      payloadEnvio: requestPayload,
      payloadRetorno: result,
    });
  } catch (erro) {
    guardado = false;
    console.error("[xbz] pedido criado na XBZ e não guardado:", numeroXbz, mensagemDe(erro, ""));
  }

  return {
    numero: numeroXbz,
    guardado,
    pedido: resumoDoPedidoXbz({ retorno: result, envio: requestPayload, numero: numeroXbz, pedidoLocalId: payload.pedidoLocalId ?? null }),
  };
}

export async function listarPedidosXbz(
  ownerId: string,
  params: { qtd_dias: number; exibir_finalizados: boolean; exibir_cancelados: boolean; pedido_numero?: string }
) {
  const admin = createAdminClient();
  const fornecedor = await getFornecedorOrThrow(ownerId);
  const qtdDias = Math.min(365, Math.max(1, Math.round(Number(params.qtd_dias || 10))));
  const exibirFinalizados = params.exibir_finalizados === true;
  const exibirCancelados = params.exibir_cancelados === true;
  const pedidoNumero = String(params.pedido_numero || "").trim();
  const query = new URLSearchParams({
    qtd_dias: String(qtdDias),
    exibir_finalizados: String(exibirFinalizados),
    exibir_cancelados: String(exibirCancelados),
  });
  if (pedidoNumero) query.set("pedido_numero", pedidoNumero);

  const raw = await requestXbz(fornecedor.id, `/PedidosListar?${query.toString()}`, "GET", credenciaisDe(fornecedor));
  const orders = normalizeRecordArray(raw);

  const pedidos: PedidoXbzResumo[] = [];
  for (const order of orders) {
    const numeroXbz = numeroDoPedidoXbz(order);
    if (!numeroXbz) continue;
    const statusXbz = statusDoPedidoXbz(order);
    try {
      const guardado = await upsertXbzPedido(admin, {
        fornecedorId: fornecedor.id,
        numeroXbz,
        statusXbz,
        statusMapeado: mapearStatusXbz(statusXbz),
        valorTotal: valorDoPedidoXbz(order),
        payloadRetorno: order,
      });
      const resumo = resumoDoPedidoXbz({ retorno: order, envio: guardado?.payloadEnvio, numero: numeroXbz, pedidoLocalId: guardado?.pedidoLocalId });
      if (resumo) pedidos.push(resumo);
    } catch (erro) {
      console.error("[xbz] pedido da lista não guardado:", numeroXbz, mensagemDe(erro, ""));
      const resumo = resumoDoPedidoXbz({ retorno: order, numero: numeroXbz });
      if (resumo) pedidos.push(resumo);
    }
  }

  await insertXbzLog(admin, {
    fornecedorId: fornecedor.id,
    action: "listOrders",
    payload: Object.fromEntries(query.entries()),
    response: { count: orders.length },
  }).catch(() => null);

  return { orders, pedidos };
}

export async function cancelarPedidoXbz(ownerId: string, numero: string) {
  const admin = createAdminClient();
  const fornecedor = await getFornecedorOrThrow(ownerId);
  const normalizedNumero = String(numero || "").trim();
  if (!normalizedNumero) throw new Error("Informe o número do pedido XBZ para cancelar.");
  const raw = await requestXbz(
    fornecedor.id,
    "/PedidoCancelar",
    "PUT",
    credenciaisDe(fornecedor),
    { numero: normalizedNumero },
    { repetir: false }
  );
  const response = normalizeRecord(raw);
  await insertXbzLog(admin, {
    fornecedorId: fornecedor.id,
    action: "cancelOrder",
    payload: { numero: normalizedNumero },
    response,
  }).catch(() => null);
  /* A resposta do cancelamento fica no registro; o retorno guardado continua
     sendo o do pedido (produto, quantidade, valor). */
  try {
    await upsertXbzPedido(admin, {
      fornecedorId: fornecedor.id,
      numeroXbz: normalizedNumero,
      statusXbz: "PEDIDO CANCELADO",
      statusMapeado: StatusPedido.CANCELADO,
    });
  } catch (erro) {
    console.error("[xbz] cancelamento feito na XBZ e não guardado:", normalizedNumero, mensagemDe(erro, ""));
  }
  return response;
}

/* ================================================================== painel */

export async function buildXbzDashboardState(ownerId: string) {
  const admin = createAdminClient();
  const fornecedor = await getXbzFornecedorByOwner(admin, ownerId);
  const [pedidosRecentes, ajustes] = await Promise.all([listPedidosLocaisRecentes(admin, ownerId), lerAjustesDaXbz(admin, ownerId)]);

  if (!fornecedor) {
    return {
      pedidosRecentes,
      ultimaSincronizacao: null as string | null,
      pedidos: [] as PedidoXbzResumo[],
      catalogo: null as SituacaoDoCatalogoXbz | null,
      margemDeVenda: ajustes.margemDeVenda,
      credentials: {
        cnpj: "",
        tokenMasked: "",
        hasToken: false,
        active: false,
        updatedAt: null as string | null,
      },
      cache: {
        deliveryTypes: [] as Array<Record<string, unknown>>,
        paymentTypes: [] as Array<Record<string, unknown>>,
        transportadoras: [] as Array<Record<string, unknown>>,
        gravacoes: [] as Array<Record<string, unknown>>,
        lastProductSearch: null as { busca: string; items: Array<Record<string, unknown>>; syncedAt: string } | null,
        lastOrdersSync: null as { params: Record<string, unknown>; items: Array<Record<string, unknown>>; syncedAt: string } | null,
        orderHistory: [] as Array<Record<string, unknown>>,
      },
      quota: {
        dailyLimit: XBZ_DAILY_LIMIT,
        used: 0,
      },
    };
  }

  const { startIso, endIso } = getSaoPauloDayRange();
  const [auxiliaryLog, productLog, ordersLog, syncLog, transportadoras, gravacoes, guardados, catalogo, used] = await Promise.all([
    getLatestXbzLogByAction(admin, fornecedor.id, "syncAuxiliary"),
    getLatestXbzLogByAction(admin, fornecedor.id, "searchProducts"),
    getLatestXbzLogByAction(admin, fornecedor.id, "listOrders"),
    getLatestXbzLogByAction(admin, fornecedor.id, "syncPedidosXbz"),
    listTransportadoras(admin, fornecedor.id, 30),
    listTransportadoras(admin, fornecedor.id, 40),
    listarPedidosGuardadosXbz(admin, fornecedor.id),
    situacaoDoCatalogo(admin, fornecedor.id),
    countXbzLogsByActionInRange(admin, fornecedor.id, "xbzRequest", startIso, endIso),
  ]);

  const auxiliaryResponse = auxiliaryLog?.response || {};
  const deliveryTypes = normalizeRecordArray(auxiliaryResponse.deliveryTypes);
  const paymentTypes = normalizeRecordArray(auxiliaryResponse.paymentTypes);
  const transportadorasResponse = transportadoras.map((item) => ({
    id: item.idXbz,
    nome: item.nome,
    tipo: item.tipo,
    transportadora: true,
    gravacao: false,
  }));
  const gravacoesResponse = gravacoes.map((item) => ({
    id: item.idXbz,
    nome: item.nome,
    tipo: item.tipo,
    transportadora: false,
    gravacao: true,
  }));

  const pedidos = guardados
    .map((pedido) =>
      resumoDoPedidoXbz({
        retorno: pedido.payloadRetorno,
        envio: pedido.payloadEnvio,
        numero: pedido.numeroXbz,
        statusXbz: pedido.statusXbz,
        valorTotal: pedido.valorTotal,
        pedidoLocalId: pedido.pedidoLocalId,
        criadoEm: pedido.createdAt,
      })
    )
    .filter((pedido): pedido is PedidoXbzResumo => Boolean(pedido));

  return {
    pedidosRecentes,
    ultimaSincronizacao: syncLog?.createdAt || null,
    pedidos,
    catalogo,
    margemDeVenda: ajustes.margemDeVenda,
    credentials: {
      cnpj: fornecedor.cnpj,
      tokenMasked: maskToken(fornecedor.token),
      hasToken: Boolean(fornecedor.token),
      active: true,
      updatedAt: fornecedor.updatedAt,
    },
    cache: {
      deliveryTypes,
      paymentTypes,
      transportadoras: transportadorasResponse,
      gravacoes: gravacoesResponse,
      lastProductSearch: productLog
        ? {
            busca: typeof productLog.payload.busca === "string" ? String(productLog.payload.busca) : "",
            items: [],
            syncedAt: productLog.createdAt,
          }
        : null,
      lastOrdersSync: ordersLog
        ? {
            params: ordersLog.payload,
            items: [],
            syncedAt: ordersLog.createdAt,
          }
        : null,
      /* A tela de logs saiu do painel: o dono da gráfica não tem o que fazer
         com JSON de requisição. O campo continua na resposta só porque é
         contrato antigo desta rota; quando foi a última sincronização agora
         vai em `ultimaSincronizacao`. */
      orderHistory: [] as Array<Record<string, unknown>>,
    },
    quota: {
      dailyLimit: XBZ_DAILY_LIMIT,
      used,
    },
  };
}

/* Recebe o dono porque a migração roda em cima de credencial e histórico: a
   versão anterior varria TODAS as empresas da plataforma a partir do clique de
   uma só, e ainda devolvia o total agregado para quem clicou. */
export async function migrateLegacyXbzSettingsToTables(ownerId: string) {
  const admin = createAdminClient();
  const integrations = await listLegacyXbzIntegrations(admin, ownerId);
  let migratedLogs = 0;
  let migratedPedidos = 0;
  let migratedFornecedores = 0;

  for (const integration of integrations) {
    const settings = normalizeRecord(integration.settings);
    const cnpjRaw = typeof settings.cnpj === "string" ? settings.cnpj : "";
    const cnpj = cleanCnpj(cnpjRaw);
    const token = String(integration.token || "").trim();
    if (cnpj.length !== 14 || !token) continue;

    const fornecedor = await upsertXbzFornecedorCredentials(admin, integration.user_id, cnpj, token);
    if (!fornecedor) continue;
    migratedFornecedores += 1;

    const orderHistory = Array.isArray(settings.orderHistory) ? settings.orderHistory : [];
    for (const item of orderHistory) {
      const record = normalizeRecord(item);
      const action = readString(record, ["action"]) || "legacyMigration";
      const payload = normalizeRecord(record.payload);
      const response = normalizeRecord(record.response);
      const createdAt = readString(record, ["syncedAt", "created_at"]) || new Date().toISOString();

      await insertXbzLog(admin, {
        fornecedorId: fornecedor.id,
        action,
        payload,
        response,
        createdAt,
      });
      migratedLogs += 1;

      const numeroXbz = numeroDoPedidoXbz(response);
      if (!numeroXbz) continue;
      const statusXbz = statusDoPedidoXbz(response);
      const pedidoLocalId =
        readString(payload, ["pedidoLocalId", "pedido_local_id"]) ||
        readString(record, ["pedidoLocalId", "pedido_local_id"]) ||
        null;

      await upsertXbzPedido(admin, {
        fornecedorId: fornecedor.id,
        pedidoLocalId: pedidoLocalId || null,
        numeroXbz,
        statusXbz,
        statusMapeado: statusXbz ? mapearStatusXbz(statusXbz) : null,
        valorTotal: valorDoPedidoXbz(response),
        payloadEnvio: payload,
        payloadRetorno: response,
      });
      migratedPedidos += 1;
    }
  }

  return {
    fornecedores: migratedFornecedores,
    logs: migratedLogs,
    pedidos: migratedPedidos,
  };
}
