import { createHash } from "node:crypto";
import type { SupabaseClient } from "@supabase/supabase-js";
import type { StatusPedido, XbzFornecedor, XbzLogRecord, XbzPedidoRecord, XbzTransportadora } from "@/lib/xbz/xbz.types";
import { textoParaBusca, type CorDoCatalogo } from "@/lib/xbz/xbz.catalogo";

const PROVIDER = "xbz";

type IntegrationRow = {
  id: string;
  user_id: string;
  provider: string;
  active: boolean | null;
  settings: unknown;
  token: string | null;
};

type FornecedorRow = {
  id: string;
  integration_id: string;
  cnpj: string;
  token: string;
  created_at: string;
  updated_at: string;
};

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

function parseNumeric(value: unknown) {
  if (typeof value === "number" && Number.isFinite(value)) return value;
  if (typeof value === "string") {
    const raw = value.trim();
    if (!raw || /^\d{4}-\d{2}-\d{2}T/.test(raw)) return null;
    const normalized = raw
      .replace(/[^\d,.-]/g, "")
      .replace(/\.(?=\d{3}(\D|$))/g, "")
      .replace(",", ".");
    const parsed = Number(normalized);
    return Number.isFinite(parsed) ? parsed : null;
  }
  return null;
}

/* O erro do supabase-js é um objeto solto, não um `Error`: lançado assim, a
   rota não lia a mensagem e mostrava só "Falha na integração XBZ" (foi o que
   escondeu o defeito do pedido em 05/10/2026). */
function erroDoBanco(error: { message?: string; code?: string } | null, contexto: string): Error {
  const erro = new Error(`${contexto}: ${error?.message || "o banco recusou a gravação"}`);
  (erro as Error & { code?: string }).code = error?.code;
  return erro;
}

export async function getOrCreateXbzIntegration(admin: SupabaseClient, ownerId: string) {
  const { data: existing, error: existingError } = await admin
    .from("integrations")
    .select("id,user_id,provider,active,settings,token")
    .eq("user_id", ownerId)
    .eq("provider", PROVIDER)
    .maybeSingle();

  if (existingError) throw erroDoBanco(existingError, "Integração XBZ");
  if (existing) return existing as IntegrationRow;

  const { data: inserted, error: insertError } = await admin
    .from("integrations")
    .insert({
      user_id: ownerId,
      provider: PROVIDER,
      active: true,
      settings: {},
    })
    .select("id,user_id,provider,active,settings,token")
    .single();

  if (insertError) throw erroDoBanco(insertError, "Integração XBZ");
  return inserted as IntegrationRow;
}

export async function getXbzIntegrationByOwner(admin: SupabaseClient, ownerId: string) {
  const { data, error } = await admin
    .from("integrations")
    .select("id,user_id,provider,active,settings,token")
    .eq("user_id", ownerId)
    .eq("provider", PROVIDER)
    .maybeSingle();

  if (error) throw erroDoBanco(error, "Integração XBZ");
  return (data as IntegrationRow | null) || null;
}

/*
  DÍVIDA CONHECIDA: o token da XBZ fica em texto puro em `fornecedor_xbz.token`.

  Ele ficava em DOIS lugares — `integrations.token` e `fornecedor_xbz.token` —
  e ninguém lia o primeiro: todas as chamadas passam por
  `getXbzFornecedorByOwner`, que lê de `fornecedor_xbz`. Duas cópias do mesmo
  segredo é o dobro de superfície exposta sem nenhum ganho, então agora se
  grava numa só e a cópia velha é apagada quando o dono salva de novo.

  Guardar cifrado ainda falta, e não cabia aqui: exige uma coluna nova (uma
  migration no banco de produção multi-tenant) e uma chave de criptografia em
  variável de ambiente, com rotina de rotação. Enquanto isso não existe, a
  proteção real é a RLS da tabela mais o fato de só o service role ler.
*/
export async function upsertXbzFornecedorCredentials(admin: SupabaseClient, ownerId: string, cnpj: string, token: string) {
  const integration = await getOrCreateXbzIntegration(admin, ownerId);

  const { error: integrationUpdateError } = await admin
    .from("integrations")
    .update({
      token: null,
      active: true,
      updated_at: new Date().toISOString(),
    })
    .eq("id", integration.id);
  if (integrationUpdateError) throw erroDoBanco(integrationUpdateError, "Integração XBZ");

  const { error: fornecedorError } = await admin.from("fornecedor_xbz").upsert(
    {
      integration_id: integration.id,
      cnpj,
      token,
      updated_at: new Date().toISOString(),
    },
    { onConflict: "integration_id" }
  );
  if (fornecedorError) throw erroDoBanco(fornecedorError, "Credenciais da XBZ");

  return getXbzFornecedorByOwner(admin, ownerId);
}

/* Uma ida ao banco só (eram duas, uma atrás da outra): toda ação da tela
   começa por aqui, e o banco fica longe. */
export async function getXbzFornecedorByOwner(admin: SupabaseClient, ownerId: string): Promise<XbzFornecedor | null> {
  const { data, error } = await admin
    .from("fornecedor_xbz")
    .select("id,integration_id,cnpj,token,created_at,updated_at,integrations!inner(user_id,provider)")
    .eq("integrations.user_id", ownerId)
    .eq("integrations.provider", PROVIDER)
    .limit(1)
    .maybeSingle();

  if (error) throw erroDoBanco(error, "Credenciais da XBZ");
  if (!data) return null;
  const row = data as FornecedorRow;
  return {
    id: row.id,
    integrationId: row.integration_id,
    ownerId,
    cnpj: row.cnpj,
    token: row.token,
    createdAt: row.created_at,
    updatedAt: row.updated_at,
  };
}

/* `ownerId` existe para a sincronização disparada por gente: quem clica no
   botão está pedindo os pedidos DA EMPRESA DELE, não os de toda a plataforma.
   Sem o filtro, uma empresa gastava a cota de todas as outras. O relógio de
   madrugada continua chamando sem filtro, que é o trabalho dele. */
export async function listActiveXbzFornecedores(admin: SupabaseClient, ownerId?: string): Promise<XbzFornecedor[]> {
  let query = admin
    .from("fornecedor_xbz")
    .select("id,integration_id,cnpj,token,created_at,updated_at,integrations!inner(user_id,active,provider)")
    .eq("integrations.provider", PROVIDER)
    .eq("integrations.active", true);
  if (ownerId) query = query.eq("integrations.user_id", ownerId);

  const { data, error } = await query;

  if (error) throw erroDoBanco(error, "Fornecedores XBZ");

  return (data || []).flatMap((item) => {
    const record = item as Record<string, unknown>;
    const integrationRecord = record.integrations as Record<string, unknown> | Record<string, unknown>[] | null;
    const integration = Array.isArray(integrationRecord) ? integrationRecord[0] : integrationRecord;
    const ownerId = typeof integration?.user_id === "string" ? integration.user_id : "";
    if (!ownerId) return [];
    return [
      {
        id: String(record.id),
        integrationId: String(record.integration_id),
        ownerId,
        cnpj: String(record.cnpj),
        token: String(record.token),
        createdAt: String(record.created_at),
        updatedAt: String(record.updated_at),
      },
    ];
  });
}

/* ------------------------------------------------------------- ajustes */

export type AjustesDaXbz = {
  /* A margem que a pessoa usou no último cadastro, em %. */
  margemDeVenda: number | null;
};

export async function lerAjustesDaXbz(admin: SupabaseClient, ownerId: string): Promise<AjustesDaXbz> {
  const integration = await getXbzIntegrationByOwner(admin, ownerId);
  const settings = normalizeRecord(integration?.settings);
  const margem = parseNumeric(settings.margemDeVenda);
  return { margemDeVenda: margem !== null && margem >= 0 ? margem : null };
}

/* `settings` tem histórico do formato antigo da integração: só a chave nova muda. */
export async function gravarAjustesDaXbz(admin: SupabaseClient, ownerId: string, ajustes: AjustesDaXbz) {
  const integration = await getXbzIntegrationByOwner(admin, ownerId);
  if (!integration) return;
  const settings = { ...normalizeRecord(integration.settings), margemDeVenda: ajustes.margemDeVenda };
  const { error } = await admin.from("integrations").update({ settings, updated_at: new Date().toISOString() }).eq("id", integration.id);
  if (error) throw erroDoBanco(error, "Ajustes da XBZ");
}

/* ------------------------------------------------------------- catálogo */

/*
  O catálogo da XBZ mora UMA vez só (migration 229, 28/10/2026): a cor em
  `xbz_catalogo` (o item da XBZ inteiro em `payload`, sem o preço, e o texto
  de busca em `busca`) e o preço de cada empresa em `fornecedor_xbz_precos`.
  Conferido nas cópias antigas: entre as empresas, o produto é igual; só o
  preço muda (a XBZ dá preço por cliente). Antes cada empresa tinha a cópia
  inteira (192 mil linhas, 531 MB) e a sincronização esgotava o disco do
  banco.

  Só regrava o que MUDOU: a cor compara a impressão digital guardada em
  `marca`; o preço compara o valor. Depois de uma empresa sincronizar, as
  outras só gravam o preço delas (e o estoque, quando mexeu). Lotes pequenos,
  um de cada vez, com mais tentativas quando o banco corta.
*/
const LINHAS_POR_LOTE = 100;
const PRECOS_POR_LOTE = 500;
const TENTATIVAS = 4;
/* 200 códigos cabem no endereço do DELETE. */
const APAGAR_POR_VEZ = 200;
/* O PostgREST devolve no máximo 1.000 linhas por vez. */
const LER_POR_VEZ = 1000;
/* O que é de cada empresa no item da XBZ (o resto é igual para todas). */
const CAMPOS_DA_EMPRESA = ["PrecoVenda", "PrecoVendaFormatado"] as const;

const esperar = (ms: number) => new Promise((pronto) => setTimeout(pronto, ms));

/* Comando cortado pelo tempo (57014) ou queda da conexão: tenta de novo, com uma pausa que cresce. */
async function comTentativas<T extends { error: { message?: string; code?: string } | null }>(fazer: () => PromiseLike<T>): Promise<T> {
  let resposta = await fazer();
  for (let tentativa = 1; tentativa < TENTATIVAS && resposta.error; tentativa += 1) {
    const passageiro = resposta.error.code === "57014" || /timeout|fetch failed|ECONNRESET|socket|connection/i.test(resposta.error.message || "");
    if (!passageiro) break;
    await esperar(1500 * tentativa);
    resposta = await fazer();
  }
  return resposta;
}

/* JSON com as chaves em ordem (o jsonb do banco reordena as chaves). */
function jsonEstavel(valor: unknown): string {
  if (Array.isArray(valor)) return `[${valor.map(jsonEstavel).join(",")}]`;
  if (valor && typeof valor === "object") {
    return `{${Object.keys(valor as Record<string, unknown>)
      .filter((chave) => (valor as Record<string, unknown>)[chave] !== undefined)
      .sort()
      .map((chave) => `${JSON.stringify(chave)}:${jsonEstavel((valor as Record<string, unknown>)[chave])}`)
      .join(",")}}`;
  }
  return JSON.stringify(valor ?? null);
}

/* A impressão digital do que é de todos na cor (o momento não entra). */
function marcaDaCor(linha: { codigo_composto: unknown; codigo_amigavel: unknown; descricao: unknown; image: unknown; estoque_disponivel: unknown; payload: unknown }) {
  const { codigo_composto, codigo_amigavel, descricao, image, estoque_disponivel, payload } = linha;
  const estoque = estoque_disponivel === null || estoque_disponivel === undefined ? null : Number(estoque_disponivel);
  return createHash("sha1").update(jsonEstavel({ codigo_composto, codigo_amigavel, descricao: descricao ?? null, image: image ?? null, estoque, payload })).digest("base64url");
}

/* O item da XBZ separado: o que é de todos e o preço desta empresa. */
function separarDaEmpresa(bruto: Record<string, unknown>) {
  const comum: Record<string, unknown> = { ...bruto };
  for (const campo of CAMPOS_DA_EMPRESA) delete comum[campo];
  const preco = parseNumeric(bruto.PrecoVenda);
  const formatado = typeof bruto.PrecoVendaFormatado === "string" && bruto.PrecoVendaFormatado.trim() ? bruto.PrecoVendaFormatado.trim() : null;
  return { comum, preco, formatado };
}

const chaveDoPreco = (preco: number | null, formatado: string | null) => `${preco ?? ""}|${formatado ?? ""}`;

/* Lê uma tabela inteira em páginas de mil. */
async function lerEmPaginas<T>(ler: (de: number, ate: number) => PromiseLike<{ data: unknown[] | null; error: { message?: string; code?: string } | null }>, lugar: string): Promise<T[]> {
  const todas: T[] = [];
  for (let inicio = 0; ; inicio += LER_POR_VEZ) {
    const { data, error } = await comTentativas(() => ler(inicio, inicio + LER_POR_VEZ - 1));
    if (error) throw erroDoBanco(error, lugar);
    todas.push(...((data ?? []) as T[]));
    if (!data || data.length < LER_POR_VEZ) return todas;
  }
}

/**
 * Grava o catálogo novo: as cores novas ou mudadas (para todos) e o preço
 * desta empresa; com `apagarOQueSaiu`, tira desta empresa as cores que não
 * vieram (e a cor que ninguém mais tem sai do catálogo). Devolve quantas
 * linhas foram gravadas e quantas cores saíram.
 */
export async function gravarCatalogoXbz(
  admin: SupabaseClient,
  fornecedorId: string,
  itens: Array<{ bruto: Record<string, unknown>; cor: CorDoCatalogo }>,
  momento: string,
  apagarOQueSaiu = true
) {
  const [cores, precos] = await Promise.all([
    lerEmPaginas<{ codigo_xbz: string; marca: string | null }>((de, ate) => admin.from("xbz_catalogo").select("codigo_xbz,marca").order("codigo_xbz", { ascending: true }).range(de, ate), "Catálogo da XBZ"),
    lerEmPaginas<{ codigo_xbz: string; preco_venda: unknown; preco_formatado: string | null }>(
      (de, ate) => admin.from("fornecedor_xbz_precos").select("codigo_xbz,preco_venda,preco_formatado").eq("fornecedor_id", fornecedorId).order("codigo_xbz", { ascending: true }).range(de, ate),
      "Preços da XBZ"
    ),
  ]);
  const marcas = new Map(cores.map((c) => [String(c.codigo_xbz), c.marca]));
  const precosGuardados = new Map(precos.map((p) => [String(p.codigo_xbz), chaveDoPreco(parseNumeric(p.preco_venda), p.preco_formatado ?? null)]));

  const vieram = new Set<string>();
  const coresMudadas: Array<Record<string, unknown>> = [];
  const precosMudados: Array<Record<string, unknown>> = [];
  for (const { bruto, cor } of itens) {
    if (vieram.has(cor.codigoXbz)) continue;
    vieram.add(cor.codigoXbz);
    const { comum, preco, formatado } = separarDaEmpresa(bruto);
    const busca = textoParaBusca(cor);
    const conteudo = {
      codigo_composto: cor.codigoComposto,
      codigo_amigavel: cor.modelo,
      descricao: cor.descricao || null,
      image: cor.imagem,
      estoque_disponivel: cor.estoque,
      payload: { ...comum, _busca: busca },
    };
    const marca = marcaDaCor(conteudo);
    if (marcas.get(cor.codigoXbz) !== marca) coresMudadas.push({ ...conteudo, codigo_xbz: cor.codigoXbz, busca, marca, updated_at: momento });
    if (precosGuardados.get(cor.codigoXbz) !== chaveDoPreco(preco, formatado)) precosMudados.push({ fornecedor_id: fornecedorId, codigo_xbz: cor.codigoXbz, preco_venda: preco, preco_formatado: formatado, updated_at: momento });
  }

  /* A cor antes do preço (o preço aponta para ela). */
  for (let inicio = 0; inicio < coresMudadas.length; inicio += LINHAS_POR_LOTE) {
    const lote = coresMudadas.slice(inicio, inicio + LINHAS_POR_LOTE);
    const { error } = await comTentativas(() => admin.from("xbz_catalogo").upsert(lote, { onConflict: "codigo_xbz" }));
    if (error) throw erroDoBanco(error, "Catálogo da XBZ");
  }
  for (let inicio = 0; inicio < precosMudados.length; inicio += PRECOS_POR_LOTE) {
    const lote = precosMudados.slice(inicio, inicio + PRECOS_POR_LOTE);
    const { error } = await comTentativas(() => admin.from("fornecedor_xbz_precos").upsert(lote, { onConflict: "fornecedor_id,codigo_xbz" }));
    if (error) throw erroDoBanco(error, "Preços da XBZ");
  }

  const sairam = apagarOQueSaiu ? [...precosGuardados.keys()].filter((codigo) => !vieram.has(codigo)) : [];
  for (let inicio = 0; inicio < sairam.length; inicio += APAGAR_POR_VEZ) {
    const codigos = sairam.slice(inicio, inicio + APAGAR_POR_VEZ);
    const { error } = await comTentativas(() => admin.from("fornecedor_xbz_precos").delete().eq("fornecedor_id", fornecedorId).in("codigo_xbz", codigos));
    if (error) throw erroDoBanco(error, "Preços da XBZ");
  }
  /* A cor que nenhuma empresa tem mais sai do catálogo. */
  if (sairam.length) {
    const { error } = await comTentativas(() => admin.rpc("xbz_limpar_cores_sem_empresa", { p_codigos: sairam }));
    if (error) throw erroDoBanco(error, "Catálogo da XBZ");
  }
  return { gravadas: coresMudadas.length + precosMudados.length, apagadas: sairam.length };
}

/* As cores desta empresa cujo texto de busca tem todas as palavras, as de mais estoque primeiro. */
export async function buscarNoCatalogoXbz(admin: SupabaseClient, fornecedorId: string, palavras: string[], limite = 500) {
  const { data, error } = await admin.rpc("xbz_buscar_no_catalogo", { p_fornecedor: fornecedorId, p_palavras: palavras, p_limite: limite });
  if (error) throw erroDoBanco(error, "Busca no catálogo da XBZ");
  return ((data ?? []) as unknown[]).map((item) => normalizeRecord(item));
}

/* Todas as cores dos modelos pedidos, numa ida só ao banco. */
export async function coresDosModelosXbz(admin: SupabaseClient, fornecedorId: string, modelos: string[]) {
  const unicos = [...new Set(modelos.filter(Boolean))];
  if (!unicos.length) return [];
  const { data, error } = await admin.rpc("xbz_cores_dos_modelos", { p_fornecedor: fornecedorId, p_modelos: unicos });
  if (error) throw erroDoBanco(error, "Catálogo da XBZ");
  return ((data ?? []) as unknown[]).map((item) => normalizeRecord(item));
}

/* Uma cor pelo código XBZ: o pedido confere preço e estoque antes de mandar. */
export async function corDoCatalogoXbz(admin: SupabaseClient, fornecedorId: string, codigoXbz: string) {
  const { data, error } = await admin.rpc("xbz_cor_da_empresa", { p_fornecedor: fornecedorId, p_codigo: codigoXbz });
  if (error) throw erroDoBanco(error, "Catálogo da XBZ");
  const linha = ((data ?? []) as Array<{ payload?: unknown; atualizado_em?: string | null }>)[0];
  if (!linha) return null;
  return { payload: normalizeRecord(linha.payload), atualizadoEm: linha.atualizado_em ? String(linha.atualizado_em) : null };
}

export async function upsertTransportadoras(admin: SupabaseClient, fornecedorId: string, transportadoras: XbzTransportadora[]) {
  if (transportadoras.length === 0) return;
  const rows = transportadoras.map((item) => ({
    fornecedor_id: fornecedorId,
    id_xbz: item.idXbz,
    nome: item.nome,
    tipo: item.tipo,
  }));
  const { error } = await admin
    .from("fornecedor_xbz_transportadoras")
    .upsert(rows, { onConflict: "fornecedor_id,id_xbz,tipo" });
  if (error) throw erroDoBanco(error, "Transportadoras da XBZ");
}

/* Em páginas de mil: o banco devolve no máximo mil linhas por pergunta, e a
   XBZ passa de 1.300 transportadoras. As que vinham depois da milésima em
   ordem alfabética (de "S" em diante) nunca apareciam na hora de comprar. */
export async function listTransportadoras(admin: SupabaseClient, fornecedorId: string, tipo?: 30 | 40) {
  const linhas: Array<Record<string, unknown>> = [];
  for (let inicio = 0; inicio < 20_000; inicio += 1000) {
    let query = admin
      .from("fornecedor_xbz_transportadoras")
      .select("id_xbz,nome,tipo")
      .eq("fornecedor_id", fornecedorId)
      .order("nome", { ascending: true })
      .order("id_xbz", { ascending: true })
      .range(inicio, inicio + 999);
    if (tipo) query = query.eq("tipo", tipo);
    const { data, error } = await query;
    if (error) throw erroDoBanco(error, "Transportadoras da XBZ");
    linhas.push(...((data || []) as Array<Record<string, unknown>>));
    if ((data || []).length < 1000) break;
  }
  return linhas.map((row) => ({
    idXbz: Number(row.id_xbz),
    nome: String(row.nome || ""),
    tipo: Number(row.tipo) as 30 | 40,
  }));
}

/* ------------------------------------------------------------------ logs */

export async function insertXbzLog(admin: SupabaseClient, entry: XbzLogRecord) {
  const payload = {
    fornecedor_id: entry.fornecedorId,
    action: entry.action,
    payload: entry.payload,
    response: entry.response,
    created_at: entry.createdAt || new Date().toISOString(),
  };
  const { error } = await admin
    .from("fornecedor_xbz_logs")
    .upsert(payload, { onConflict: "fornecedor_id,action,created_at" });
  if (error) throw erroDoBanco(error, "Registro da XBZ");
}

export async function countXbzLogsByActionInRange(
  admin: SupabaseClient,
  fornecedorId: string,
  action: string,
  startIso: string,
  endIso: string
) {
  const { count, error } = await admin
    .from("fornecedor_xbz_logs")
    .select("id", { count: "exact", head: true })
    .eq("fornecedor_id", fornecedorId)
    .eq("action", action)
    .gte("created_at", startIso)
    .lt("created_at", endIso);
  if (error) throw erroDoBanco(error, "Contador da XBZ");
  return Number(count || 0);
}

export async function insertXbzRequestAttemptLog(
  admin: SupabaseClient,
  fornecedorId: string,
  payload: Record<string, unknown>
) {
  let lastError: Error | null = null;
  for (let index = 0; index < 4; index += 1) {
    const createdAt = new Date(Date.now() + index).toISOString();
    const { error } = await admin.from("fornecedor_xbz_logs").insert({
      fornecedor_id: fornecedorId,
      action: "xbzRequest",
      payload,
      response: {},
      created_at: createdAt,
    });
    if (!error) return createdAt;
    lastError = new Error(String(error.message || "Falha ao registrar tentativa XBZ."));
  }
  throw lastError || new Error("Falha ao registrar tentativa XBZ.");
}

/* O que a XBZ respondeu àquela chamada. Antes ficava só "tentou", e o pedido
   de 05/10/2026 não deixou rastro do que voltou. Falhar aqui não derruba nada. */
export async function registrarRespostaDaChamadaXbz(
  admin: SupabaseClient,
  fornecedorId: string,
  createdAt: string,
  resposta: Record<string, unknown>
) {
  await admin
    .from("fornecedor_xbz_logs")
    .update({ response: resposta })
    .eq("fornecedor_id", fornecedorId)
    .eq("action", "xbzRequest")
    .eq("created_at", createdAt);
}

/* Um envio de pedido com esta ordem de compra nos últimos dias, mesmo que a
   resposta não tenha chegado (é a trava contra pedido em dobro na XBZ). */
export async function envioRecenteDaOrdemDeCompra(admin: SupabaseClient, fornecedorId: string, ordemDeCompra: string, desde: string) {
  const { data, error } = await admin
    .from("fornecedor_xbz_logs")
    .select("created_at,response")
    .eq("fornecedor_id", fornecedorId)
    .eq("action", "xbzRequest")
    .eq("payload->>path", "/PedidoEnviar")
    .eq("payload->>ordemDeCompra", ordemDeCompra)
    .gte("created_at", desde)
    .order("created_at", { ascending: false })
    .limit(1)
    .maybeSingle();
  if (error) throw erroDoBanco(error, "Registro da XBZ");
  if (!data) return null;
  const linha = data as { created_at: string; response?: unknown };
  return { quando: String(linha.created_at), resposta: normalizeRecord(linha.response) };
}

export async function listXbzLogs(admin: SupabaseClient, fornecedorId: string, limit = 20) {
  const { data, error } = await admin
    .from("fornecedor_xbz_logs")
    .select("id,action,payload,response,created_at")
    .eq("fornecedor_id", fornecedorId)
    .order("created_at", { ascending: false })
    .limit(limit);
  if (error) throw erroDoBanco(error, "Registro da XBZ");
  return (data || []).map((item) => {
    const row = item as Record<string, unknown>;
    return {
      id: String(row.id),
      action: String(row.action),
      payload: normalizeRecord(row.payload),
      response: normalizeRecord(row.response),
      createdAt: String(row.created_at),
    };
  });
}

/* Quem pergunta é o relógio de madrugada, antes de gastar chamada: se já
   houve sincronização hoje, o processo reiniciou depois de rodar e não pode
   rodar de novo. Por isso a busca é da plataforma inteira, sem fornecedor. */
export async function getUltimoSyncDePedidosXbz(admin: SupabaseClient) {
  const { data, error } = await admin
    .from("fornecedor_xbz_logs")
    .select("created_at")
    .eq("action", "syncPedidosXbz")
    .order("created_at", { ascending: false })
    .limit(1)
    .maybeSingle();
  if (error) throw erroDoBanco(error, "Registro da XBZ");
  if (!data) return null;
  const row = data as Record<string, unknown>;
  return row.created_at ? String(row.created_at) : null;
}

export async function getLatestXbzLogByAction(admin: SupabaseClient, fornecedorId: string, action: string) {
  const { data, error } = await admin
    .from("fornecedor_xbz_logs")
    .select("payload,response,created_at")
    .eq("fornecedor_id", fornecedorId)
    .eq("action", action)
    .order("created_at", { ascending: false })
    .limit(1)
    .maybeSingle();
  if (error) throw erroDoBanco(error, "Registro da XBZ");
  if (!data) return null;
  const row = data as Record<string, unknown>;
  return {
    payload: normalizeRecord(row.payload),
    response: normalizeRecord(row.response),
    createdAt: String(row.created_at),
  };
}

/* --------------------------------------------------------------- pedidos */

const COLUNAS_DO_PEDIDO =
  "id,fornecedor_id,pedido_local_id,numero_xbz,status_xbz,status_mapeado,valor_total,payload_envio,payload_retorno,created_at,updated_at";

/*
  Grava o pedido da XBZ, novo ou já conhecido.

  Era um `upsert` com `onConflict: "numero_xbz"`, e o único índice desse
  campo é parcial (`WHERE numero_xbz IS NOT NULL`): o Postgres não usa índice
  parcial no ON CONFLICT sem a mesma condição, e respondia "there is no unique
  or exclusion constraint matching the ON CONFLICT specification" em TODA
  gravação com número. Nenhum pedido chegou a ser guardado desde que a tabela
  existe, e o primeiro pedido feito pela tela (05/10/2026) foi criado na XBZ
  enquanto a tela dizia "falha".

  Agora procura o pedido DESTE fornecedor pelo número e atualiza, ou insere.
  O índice parcial continua impedindo duas linhas com o mesmo número; se duas
  gravações correrem juntas, a segunda cai no 23505 e vira atualização.

  Campo vazio não apaga o que já estava: a consulta de status não sabe o
  pedido de origem nem o que foi enviado, e o cancelamento não sabe o valor.
*/
export async function upsertXbzPedido(
  admin: SupabaseClient,
  payload: {
    fornecedorId: string;
    pedidoLocalId?: string | null;
    numeroXbz: string | null;
    statusXbz?: string | null;
    statusMapeado?: StatusPedido | null;
    valorTotal?: number | null;
    payloadEnvio?: Record<string, unknown>;
    payloadRetorno?: Record<string, unknown>;
  }
): Promise<XbzPedidoRecord | null> {
  const temConteudo = (valor: Record<string, unknown> | undefined) => Boolean(valor && Object.keys(valor).length);
  const campos: Record<string, unknown> = { updated_at: new Date().toISOString() };
  if (payload.statusXbz) campos.status_xbz = payload.statusXbz;
  if (payload.statusMapeado) campos.status_mapeado = payload.statusMapeado;
  if (typeof payload.valorTotal === "number" && Number.isFinite(payload.valorTotal)) campos.valor_total = payload.valorTotal;
  if (temConteudo(payload.payloadRetorno)) campos.payload_retorno = payload.payloadRetorno;
  if (payload.pedidoLocalId) campos.pedido_local_id = payload.pedidoLocalId;
  if (temConteudo(payload.payloadEnvio)) campos.payload_envio = payload.payloadEnvio;

  const atualizar = async (id: string) => {
    const { data, error } = await admin.from("fornecedor_xbz_pedidos").update(campos).eq("id", id).select(COLUNAS_DO_PEDIDO).single();
    if (error) throw erroDoBanco(error, "Pedido da XBZ");
    return parsePedidoRow(data);
  };

  if (payload.numeroXbz) {
    const existente = await findXbzPedidoByNumero(admin, payload.fornecedorId, payload.numeroXbz);
    if (existente) return atualizar(existente.id);
  }

  const { data, error } = await admin
    .from("fornecedor_xbz_pedidos")
    .insert({ fornecedor_id: payload.fornecedorId, numero_xbz: payload.numeroXbz, ...campos })
    .select(COLUNAS_DO_PEDIDO)
    .single();
  if (!error) return parsePedidoRow(data);

  if (error.code === "23505" && payload.numeroXbz) {
    const existente = await findXbzPedidoByNumero(admin, payload.fornecedorId, payload.numeroXbz);
    if (existente) return atualizar(existente.id);
    /* O número já está guardado em outra empresa (o mesmo CNPJ ligado em
       duas contas). O pedido é dela: não mexe. */
    return null;
  }
  throw erroDoBanco(error, "Pedido da XBZ");
}

function parsePedidoRow(value: unknown): XbzPedidoRecord {
  const row = normalizeRecord(value);
  return {
    id: String(row.id),
    fornecedorId: String(row.fornecedor_id),
    pedidoLocalId: row.pedido_local_id ? String(row.pedido_local_id) : null,
    numeroXbz: row.numero_xbz ? String(row.numero_xbz) : null,
    statusXbz: row.status_xbz ? String(row.status_xbz) : null,
    statusMapeado: row.status_mapeado ? (String(row.status_mapeado) as StatusPedido) : null,
    valorTotal: typeof row.valor_total === "number" ? row.valor_total : row.valor_total ? Number(row.valor_total) : null,
    payloadEnvio: normalizeRecord(row.payload_envio),
    payloadRetorno: normalizeRecord(row.payload_retorno),
    createdAt: String(row.created_at),
    updatedAt: String(row.updated_at),
  };
}

/* Sempre do fornecedor: sem o filtro, o número de um pedido de outra empresa
   (mesmo CNPJ em duas contas) trazia o vínculo de pedido dela para esta. */
export async function findXbzPedidoByNumero(admin: SupabaseClient, fornecedorId: string, numeroXbz: string) {
  const { data, error } = await admin
    .from("fornecedor_xbz_pedidos")
    .select(COLUNAS_DO_PEDIDO)
    .eq("fornecedor_id", fornecedorId)
    .eq("numero_xbz", numeroXbz)
    .maybeSingle();
  if (error) throw erroDoBanco(error, "Pedido da XBZ");
  if (!data) return null;
  return parsePedidoRow(data);
}

/* Os pedidos guardados, os mais novos primeiro: a tela abre com eles sem
   gastar consulta da XBZ. */
export async function listarPedidosGuardadosXbz(admin: SupabaseClient, fornecedorId: string, limite = 60) {
  const { data, error } = await admin
    .from("fornecedor_xbz_pedidos")
    .select(COLUNAS_DO_PEDIDO)
    .eq("fornecedor_id", fornecedorId)
    .not("numero_xbz", "is", null)
    .order("created_at", { ascending: false })
    .limit(limite);
  if (error) throw erroDoBanco(error, "Pedidos da XBZ");
  return (data || []).map((item) => parsePedidoRow(item));
}

export async function listPedidosAbertosParaSync(admin: SupabaseClient, fornecedorId: string) {
  const { data, error } = await admin
    .from("fornecedor_xbz_pedidos")
    .select(COLUNAS_DO_PEDIDO)
    .eq("fornecedor_id", fornecedorId)
    .not("numero_xbz", "is", null)
    .order("updated_at", { ascending: true })
    .limit(500);
  if (error) throw erroDoBanco(error, "Pedidos da XBZ");
  return (data || [])
    .map((item) => parsePedidoRow(item))
    .filter((pedido) => pedido.statusMapeado !== "FINALIZADO" && pedido.statusMapeado !== "CANCELADO");
}

/* Sempre presa a um dono: a migração do formato antigo mexe em credencial e
   histórico de pedido, e varrer a plataforma inteira a partir do clique de uma
   empresa é acesso ao dado das outras. */
export async function listLegacyXbzIntegrations(admin: SupabaseClient, ownerId: string) {
  const { data, error } = await admin
    .from("integrations")
    .select("id,user_id,provider,token,settings,active")
    .eq("provider", PROVIDER)
    .eq("user_id", ownerId);
  if (error) throw erroDoBanco(error, "Integração XBZ");
  return (data || []) as IntegrationRow[];
}

export type PedidoLocalResumo = {
  id: string;
  numero: string;
  valorTotal: number | null;
  criadoEm: string | null;
  cliente: string | null;
};

/* Alimenta o seletor de "pedido meu" na hora de mandar uma compra para a XBZ.
   Antes era um campo de texto onde caberia digitar um UUID à mão — na prática
   ficava vazio e o pedido XBZ nascia sem dono do lado de cá. */
export async function listPedidosLocaisRecentes(
  admin: SupabaseClient,
  ownerId: string,
  limite = 40
): Promise<PedidoLocalResumo[]> {
  const { data, error } = await admin
    .from("orders")
    .select("id,number,total_amount,created_at,client_id")
    .eq("user_id", ownerId)
    .order("created_at", { ascending: false })
    .limit(limite);
  if (error) throw erroDoBanco(error, "Pedidos da empresa");

  const linhas = (data || []).map((item) => item as Record<string, unknown>);
  const clienteIds = Array.from(
    new Set(linhas.map((linha) => (linha.client_id ? String(linha.client_id) : "")).filter(Boolean))
  );

  /* O nome do cliente vem separado porque o join direto em `orders` já deu
     resultado vazio em silêncio noutras telas deste painel. */
  const nomePorCliente = new Map<string, string>();
  if (clienteIds.length > 0) {
    const { data: clientes, error: clientesError } = await admin
      .from("clients")
      .select("id,name")
      .in("id", clienteIds);
    if (clientesError) throw erroDoBanco(clientesError, "Clientes");
    for (const item of clientes || []) {
      const linha = item as Record<string, unknown>;
      const id = linha.id ? String(linha.id) : "";
      const nome = linha.name ? String(linha.name) : "";
      if (id && nome) nomePorCliente.set(id, nome);
    }
  }

  return linhas.map((linha) => ({
    id: String(linha.id),
    numero: linha.number === null || linha.number === undefined ? "" : String(linha.number),
    valorTotal: parseNumeric(linha.total_amount),
    criadoEm: linha.created_at ? String(linha.created_at) : null,
    cliente: linha.client_id ? nomePorCliente.get(String(linha.client_id)) || null : null,
  }));
}
