/**
 * As regras da ficha completa do produto pelo assistente, sem banco.
 *
 * A tela do produto (app/dashboard/products/[id]) carrega a ficha, a pessoa
 * mexe nos pedaços (galeria, variações, serviços, campos) e o Salvar manda
 * tudo para a rota de produtos. O assistente faz o mesmo caminho: lê a ficha
 * como a tela lê, aplica só o que a pessoa pediu, com as mesmas regras dos
 * componentes da tela, e grava pela MESMA função da rota
 * (lib/produtos/servidor/gravar-produto.ts). Os formatos no banco ficam
 * iguais aos do painel: o `selected_options` dos orçamentos e pedidos
 * depende dos ids das opções, que nunca mudam por aqui.
 *
 * Só regra, para o teste ao lado rodar sozinho.
 */

/* ================================================================ formatos */

export type OpcaoDaFicha = {
  id?: string;
  label: string;
  price: number;
  swatch?: string | null;
  price_applies_per_quantity?: boolean;
  price_unit?: string | null;
  sku?: string;
  stock?: number;
  is_required?: boolean;
  position?: number;
  image_id?: string | null;
  minimum_stock?: number;
  reorder_point?: number;
  material_id?: string | null;
  material_qty?: number;
  [outro: string]: unknown;
};

export type AtributoDaFicha = {
  id?: string;
  name: string;
  allow_multiple?: boolean;
  is_required?: boolean;
  position?: number;
  options: OpcaoDaFicha[];
  display_style?: string | null;
  show_in_store?: boolean | null;
};

export type VariacaoDaFicha = {
  id?: string;
  combination: Record<string, string>;
  sku: string;
  price: number | null;
  stock: number;
  cost_adjustment: number;
};

export type FotoDaFicha = { id: string; image_url: string; sort_order: number; is_primary: boolean };

export type EstoqueVivo = { option_id?: string | null; stock?: number | null; minimum_stock?: number | null; reorder_point?: number | null };
export type ReceitaViva = { option_id?: string | null; material_id?: string | null; qty_per_piece?: number | null };

/* ========================================================== ler como a tela */

type LinhaDeAtributo = {
  id?: string | null;
  name?: string | null;
  options?: Array<string | OpcaoDaFicha> | null;
  allow_multiple?: boolean | null;
  is_required?: boolean | null;
  position?: number | null;
  display_style?: string | null;
  show_in_store?: boolean | null;
};

/**
 * Os atributos como a tela do produto monta (fetchProduct): o saldo VIVO de
 * cada opção (product_option_stocks) e a receita (product_option_recipes)
 * por cima do que está no JSON do atributo.
 */
export function atributosDaFicha(linhas: LinhaDeAtributo[] | null | undefined, estoques: EstoqueVivo[] = [], receitas: ReceitaViva[] = []): AtributoDaFicha[] {
  const vivoPorOpcao = new Map(estoques.filter((linha) => typeof linha.option_id === "string" && linha.option_id).map((linha) => [String(linha.option_id), linha]));
  const receitaPorOpcao = new Map(receitas.filter((linha) => typeof linha.option_id === "string" && linha.option_id).map((linha) => [String(linha.option_id), linha]));
  return (linhas ?? [])
    .map((a, index) => ({
      id: a.id ?? undefined,
      name: a.name ?? "",
      options: Array.isArray(a.options)
        ? a.options.map((opt): OpcaoDaFicha => {
            if (typeof opt === "string") {
              return { label: opt, price: 0, is_required: false, price_applies_per_quantity: true };
            }
            const vivo = opt.id ? vivoPorOpcao.get(String(opt.id)) : undefined;
            const receita = opt.id ? receitaPorOpcao.get(String(opt.id)) : undefined;
            return {
              ...opt,
              ...(vivo
                ? { stock: Number(vivo.stock) || 0, minimum_stock: Number(vivo.minimum_stock) || 0, reorder_point: Number(vivo.reorder_point) || 0 }
                : {}),
              material_id: receita?.material_id ?? null,
              material_qty: receita ? Number(receita.qty_per_piece) || 0 : 0,
              price_applies_per_quantity: opt.price_applies_per_quantity !== false,
            };
          })
        : [],
      allow_multiple: a.allow_multiple ?? false,
      is_required: a.is_required ?? false,
      position: a.position ?? index,
      display_style: a.display_style ?? null,
      show_in_store: a.show_in_store !== false,
    }))
    .sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
}

/** A galeria como a tela monta: em ordem, sempre com uma principal; sem galeria, a foto antiga do produto. */
export function fotosDaFicha(linhas: Array<Record<string, unknown>> | null | undefined, imagemDoProduto: string | null | undefined, novoId: () => string): FotoDaFicha[] {
  const mapeadas = (linhas ?? [])
    .map((foto, index) => ({
      id: typeof foto.id === "string" ? foto.id : novoId(),
      image_url: typeof foto.image_url === "string" ? foto.image_url : "",
      sort_order: typeof foto.sort_order === "number" ? foto.sort_order : index,
      is_primary: Boolean(foto.is_primary),
    }))
    .filter((foto) => Boolean(foto.image_url))
    .sort((a, b) => a.sort_order - b.sort_order)
    .map((foto, index) => ({ ...foto, sort_order: index }));
  if (mapeadas.length) {
    const temPrincipal = mapeadas.some((foto) => foto.is_primary);
    return mapeadas.map((foto, index) => ({ ...foto, is_primary: temPrincipal ? foto.is_primary : index === 0 }));
  }
  if (imagemDoProduto) return [{ id: novoId(), image_url: imagemDoProduto, sort_order: 0, is_primary: true }];
  return [];
}

/* ===================================================== galeria (como a tela) */

/** ensurePrimary do editor da galeria. */
export function garantirPrincipal(fotos: FotoDaFicha[]): FotoDaFicha[] {
  if (!fotos.length) return [];
  if (fotos.some((foto) => foto.is_primary)) return fotos;
  const [primeira, ...resto] = fotos;
  return [{ ...primeira, is_primary: true }, ...resto.map((foto) => ({ ...foto, is_primary: false }))];
}

/** Fotos novas entram no fim, como no envio da galeria; `principal` troca a principal. */
export function adicionarFotos(fotos: FotoDaFicha[], novas: Array<{ id: string; image_url: string; principal?: boolean }>): FotoDaFicha[] {
  const tamanho = fotos.length;
  const criadas = novas.map((nova, indice) => ({
    id: nova.id,
    image_url: nova.image_url,
    sort_order: tamanho + indice,
    is_primary: tamanho === 0 && indice === 0,
  }));
  let juntas = garantirPrincipal([...fotos, ...criadas]);
  const escolhida = novas.find((nova) => nova.principal);
  if (escolhida) juntas = juntas.map((foto) => ({ ...foto, is_primary: foto.id === escolhida.id }));
  return garantirPrincipal(juntas);
}

export function definirPrincipal(fotos: FotoDaFicha[], id: string): FotoDaFicha[] {
  return garantirPrincipal(fotos.map((foto) => ({ ...foto, is_primary: foto.id === id })));
}

export function removerFotos(fotos: FotoDaFicha[], ids: string[]): FotoDaFicha[] {
  const sair = new Set(ids);
  return garantirPrincipal(fotos.filter((foto) => !sair.has(foto.id)).map((foto, indice) => ({ ...foto, sort_order: indice })));
}

/** A nova ordem pelos ids; as fotos que não vieram na lista ficam depois, na ordem em que estavam. */
export function reordenarFotos(fotos: FotoDaFicha[], ordem: string[]): FotoDaFicha[] {
  const porId = new Map(fotos.map((foto) => [foto.id, foto]));
  const primeiro = ordem.map((id) => porId.get(id)).filter((foto): foto is FotoDaFicha => Boolean(foto));
  const vistos = new Set(primeiro.map((foto) => foto.id));
  const resto = fotos.filter((foto) => !vistos.has(foto.id));
  return garantirPrincipal([...primeiro, ...resto].map((foto, indice) => ({ ...foto, sort_order: indice })));
}

/** Opção apontando para foto que saiu da galeria perde o vínculo (a tela faz o mesmo). */
export function limparFotosDasOpcoes(atributos: AtributoDaFicha[], fotos: FotoDaFicha[]): { atributos: AtributoDaFicha[]; mudou: boolean } {
  const validas = new Set(fotos.map((foto) => foto.id));
  let mudou = false;
  const proximos = atributos.map((atributo) => ({
    ...atributo,
    options: atributo.options.map((opcao) => {
      if (!opcao.image_id || validas.has(opcao.image_id)) return opcao;
      mudou = true;
      return { ...opcao, image_id: null };
    }),
  }));
  return { atributos: proximos, mudou };
}

/* ============================================== encolher (como no navegador) */

export const LADO_MAIOR = 1600;
export const PEQUENA_O_BASTANTE = 300 * 1024;

/**
 * O que lib/fotos/encolher.ts decide no navegador, aqui para o servidor:
 * GIF sobe como veio; foto até 1600 px e até 300 KB também; o resto vira
 * WebP de até 1600 px no lado maior, qualidade 0,85.
 */
export function comoGuardarAFoto(foto: { formato: string; largura: number; altura: number; bytes: number }): "original" | "encolher" {
  if (foto.formato === "gif") return "original";
  const maior = Math.max(foto.largura, foto.altura);
  if (maior <= LADO_MAIOR && foto.bytes <= PEQUENA_O_BASTANTE) return "original";
  return "encolher";
}

export const EXTENSAO_DO_FORMATO: Record<string, { extensao: string; tipo: string }> = {
  jpeg: { extensao: "jpg", tipo: "image/jpeg" },
  png: { extensao: "png", tipo: "image/png" },
  webp: { extensao: "webp", tipo: "image/webp" },
  gif: { extensao: "gif", tipo: "image/gif" },
  avif: { extensao: "avif", tipo: "image/avif" },
  heif: { extensao: "heic", tipo: "image/heic" },
  tiff: { extensao: "tiff", tipo: "image/tiff" },
};

/* ========================================================= variações (atributos) */

export const ESTILOS: Record<string, string | null> = {
  "automático": null,
  cores: "cores",
  miniaturas: "miniaturas",
  "botões": "botoes",
  lista: "lista",
  menu: "dropdown",
};
export const OPCOES_DE_ESTILO = Object.keys(ESTILOS);
export const NOME_DO_ESTILO: Record<string, string> = Object.fromEntries(Object.entries(ESTILOS).map(([nome, valor]) => [valor ?? "", nome]));

export const COBRAR_POR = ["peça", "pedido", "medida"] as const;
export type CobrarPor = (typeof COBRAR_POR)[number];

function vendidoPorMedida(unitType: string | null | undefined) {
  return unitType === "square_meter" || unitType === "meter" || unitType === "linear_meter";
}

/** A unidade do adicional como lib/adicionais.ts lê (unidadeDoAdicional). */
export function cobrancaDaOpcao(opcao: Pick<OpcaoDaFicha, "price_unit" | "price_applies_per_quantity">, unitType: string | null | undefined): CobrarPor {
  const declarada = String(opcao.price_unit ?? "").trim().toLowerCase();
  if (declarada === "order") return "pedido";
  if (declarada === "piece") return "peça";
  if (declarada === "measure") return vendidoPorMedida(unitType) ? "medida" : "peça";
  if (opcao.price_applies_per_quantity === false) return "pedido";
  return vendidoPorMedida(unitType) ? "medida" : "peça";
}

export type PedidoDeOpcao = {
  rotulo: string;
  novo_rotulo?: string;
  remover?: boolean;
  preco_adicional?: number;
  cobrar_por?: CobrarPor;
  cor?: string;
  obrigatoria?: boolean;
  estoque_inicial?: number;
  estoque_minimo?: number;
  ponto_de_reposicao?: number;
  foto_id?: string;
  foto_numero?: number;
  insumo_id?: string;
  insumo_quantidade?: number;
  posicao?: number;
};

export type PedidoDeAtributo = {
  nome: string;
  novo_nome?: string;
  remover?: boolean;
  multipla_escolha?: boolean;
  obrigatorio?: boolean;
  estilo?: string;
  mostrar_na_loja?: boolean;
  posicao?: number;
  opcoes?: PedidoDeOpcao[];
};

export type OpcaoQueSai = { atributo: string; opcao: string; id: string | null };

export type ResultadoDosAtributos =
  | { ok: true; atributos: AtributoDaFicha[]; mudancas: string[]; saem: OpcaoQueSai[]; opcoesNovas: number }
  | { ok: false; erro: string };

const igual = (a: string, b: string) => a.trim().toLocaleLowerCase("pt-BR") === b.trim().toLocaleLowerCase("pt-BR");
const HEX = /^#[0-9a-f]{6}$/i;

function mover<T>(lista: T[], de: number, para: number): T[] {
  const copia = [...lista];
  const [item] = copia.splice(de, 1);
  copia.splice(Math.max(0, Math.min(para, copia.length)), 0, item);
  return copia;
}

/**
 * Aplica os pedidos do assistente nos atributos, como os botões da aba
 * Variações fariam: o que não foi citado fica como está; opção nova nasce com
 * os mesmos campos do "Adicionar opção" da tela; os ids que já existem nunca
 * mudam (é por eles que orçamentos, pedidos e estoque acham a opção).
 */
export function aplicarPedidosNosAtributos(
  atuais: AtributoDaFicha[],
  pedidos: PedidoDeAtributo[],
  contexto: { unitType: string | null | undefined; fotos: FotoDaFicha[]; novoId: () => string; insumos?: Set<string> }
): ResultadoDosAtributos {
  let atributos: AtributoDaFicha[] = atuais.map((atributo) => ({ ...atributo, options: atributo.options.map((opcao) => ({ ...opcao })) }));
  const mudancas: string[] = [];
  const saem: OpcaoQueSai[] = [];
  let opcoesNovas = 0;
  const porMedida = vendidoPorMedida(contexto.unitType);

  for (const pedido of pedidos) {
    const nome = pedido.nome.trim();
    if (!nome) return { ok: false, erro: "Cada atributo precisa do nome (ex.: Cor, Tamanho, Papel)." };
    let indice = atributos.findIndex((atributo) => igual(atributo.name, nome));

    if (pedido.remover) {
      if (indice < 0) return { ok: false, erro: `O atributo "${nome}" não existe neste produto.` };
      const atributo = atributos[indice];
      for (const opcao of atributo.options) saem.push({ atributo: atributo.name, opcao: opcao.label, id: opcao.id ?? null });
      atributos.splice(indice, 1);
      atributos = atributos.map((item, posicao) => ({ ...item, position: posicao }));
      mudancas.push(`atributo ${atributo.name} sai (com ${atributo.options.length} opção(ões))`);
      continue;
    }

    if (indice < 0) {
      atributos.push({ name: nome, options: [], allow_multiple: false, is_required: false, position: atributos.length, display_style: null, show_in_store: true });
      indice = atributos.length - 1;
      mudancas.push(`atributo novo: ${nome}`);
    }
    const atributo = atributos[indice];

    if (pedido.novo_nome !== undefined) {
      const novo = pedido.novo_nome.trim();
      if (!novo) return { ok: false, erro: `O novo nome do atributo "${nome}" está vazio.` };
      if (atributos.some((outro, i) => i !== indice && igual(outro.name, novo))) return { ok: false, erro: `Já existe um atributo "${novo}" neste produto.` };
      if (novo !== atributo.name) mudancas.push(`atributo ${atributo.name} passa a se chamar ${novo}`);
      atributo.name = novo;
    }
    if (pedido.multipla_escolha !== undefined && pedido.multipla_escolha !== Boolean(atributo.allow_multiple)) {
      atributo.allow_multiple = pedido.multipla_escolha;
      mudancas.push(`${atributo.name}: ${pedido.multipla_escolha ? "aceita várias escolhas" : "uma escolha só"}`);
    }
    if (pedido.obrigatorio !== undefined && pedido.obrigatorio !== Boolean(atributo.is_required)) {
      atributo.is_required = pedido.obrigatorio;
      mudancas.push(`${atributo.name}: ${pedido.obrigatorio ? "escolha obrigatória" : "escolha opcional"}`);
    }
    if (pedido.estilo !== undefined) {
      if (!(pedido.estilo in ESTILOS)) return { ok: false, erro: `Estilo "${pedido.estilo}" não existe. Use: ${OPCOES_DE_ESTILO.join(", ")}.` };
      const valor = ESTILOS[pedido.estilo];
      if ((atributo.display_style ?? null) !== valor) mudancas.push(`${atributo.name}: aparece na loja como ${pedido.estilo}`);
      atributo.display_style = valor;
    }
    if (pedido.mostrar_na_loja !== undefined && pedido.mostrar_na_loja !== (atributo.show_in_store !== false)) {
      atributo.show_in_store = pedido.mostrar_na_loja;
      mudancas.push(`${atributo.name}: ${pedido.mostrar_na_loja ? "aparece na loja" : "some da loja (fica só no painel)"}`);
    }

    for (const pedidoDaOpcao of pedido.opcoes ?? []) {
      const rotulo = pedidoDaOpcao.rotulo.trim();
      if (!rotulo) return { ok: false, erro: `Uma opção de "${atributo.name}" está sem nome.` };
      let posicaoDaOpcao = atributo.options.findIndex((opcao) => igual(String(opcao.label ?? ""), rotulo));

      if (pedidoDaOpcao.remover) {
        if (posicaoDaOpcao < 0) return { ok: false, erro: `A opção "${rotulo}" não existe em "${atributo.name}".` };
        const [saindo] = atributo.options.splice(posicaoDaOpcao, 1);
        atributo.options = atributo.options.map((opcao, i) => ({ ...opcao, position: i }));
        saem.push({ atributo: atributo.name, opcao: saindo.label, id: saindo.id ?? null });
        mudancas.push(`${atributo.name}: opção ${saindo.label} sai`);
        continue;
      }

      let nova = false;
      if (posicaoDaOpcao < 0) {
        /* Os mesmos campos do "Adicionar opção" da aba Variações. */
        atributo.options.push({
          id: contexto.novoId(),
          label: rotulo,
          price: 0,
          price_applies_per_quantity: true,
          stock: 0,
          is_required: false,
          position: atributo.options.length,
          image_id: null,
          minimum_stock: 0,
          reorder_point: 0,
        });
        posicaoDaOpcao = atributo.options.length - 1;
        nova = true;
        opcoesNovas += 1;
      }
      const opcao = atributo.options[posicaoDaOpcao];
      const partes: string[] = [];

      if (pedidoDaOpcao.novo_rotulo !== undefined) {
        const novo = pedidoDaOpcao.novo_rotulo.trim();
        if (!novo) return { ok: false, erro: `O novo nome da opção "${rotulo}" está vazio.` };
        if (atributo.options.some((outra, i) => i !== posicaoDaOpcao && igual(String(outra.label ?? ""), novo))) {
          return { ok: false, erro: `Já existe a opção "${novo}" em "${atributo.name}".` };
        }
        if (novo !== opcao.label) partes.push(`passa a se chamar ${novo}`);
        opcao.label = novo;
      }
      if (pedidoDaOpcao.preco_adicional !== undefined) {
        if (Number(opcao.price ?? 0) !== pedidoDaOpcao.preco_adicional) partes.push(`adicional ${pedidoDaOpcao.preco_adicional.toFixed(2).replace(".", ",")}`);
        opcao.price = pedidoDaOpcao.preco_adicional;
      }
      if (pedidoDaOpcao.cobrar_por !== undefined) {
        const cobrar = pedidoDaOpcao.cobrar_por;
        if (porMedida) {
          /* updateOptionPriceUnit: a unidade e o campo antigo andam juntos. */
          const unidade = cobrar === "medida" ? "measure" : cobrar === "peça" ? "piece" : "order";
          opcao.price_unit = unidade;
          opcao.price_applies_per_quantity = unidade !== "order";
        } else {
          if (cobrar === "medida") return { ok: false, erro: `"medida" só vale em produto vendido por m² ou metro. Use "peça" ou "pedido" na opção "${rotulo}".` };
          /* Fora do m², a tela só tem a chave "multiplica pela quantidade". */
          opcao.price_applies_per_quantity = cobrar === "peça";
        }
        partes.push(`cobrado por ${cobrar}`);
      }
      if (pedidoDaOpcao.cor !== undefined) {
        const cor = pedidoDaOpcao.cor.trim();
        if (cor && cor.toLowerCase() !== "automática" && !HEX.test(cor)) return { ok: false, erro: `A cor da opção "${rotulo}" precisa ser um código como #1F5FD0 (ou "automática").` };
        opcao.swatch = cor && HEX.test(cor) ? cor.toLowerCase() : null;
        partes.push(opcao.swatch ? `cor ${opcao.swatch}` : "cor automática");
      }
      if (pedidoDaOpcao.obrigatoria !== undefined) {
        opcao.is_required = pedidoDaOpcao.obrigatoria;
        partes.push(pedidoDaOpcao.obrigatoria ? "obrigatória" : "não obrigatória");
      }
      if (pedidoDaOpcao.estoque_inicial !== undefined) {
        if (!nova) {
          return {
            ok: false,
            erro: `O saldo da opção "${rotulo}", que já existe, muda por ajuste de estoque (ajustar_estoque_do_produto), como no painel: o estoque_inicial vale só para opção nova.`,
          };
        }
        opcao.stock = Math.max(0, pedidoDaOpcao.estoque_inicial);
        partes.push(`estoque inicial ${opcao.stock}`);
      }
      if (pedidoDaOpcao.estoque_minimo !== undefined) {
        opcao.minimum_stock = Math.max(0, pedidoDaOpcao.estoque_minimo);
        partes.push(`estoque mínimo ${opcao.minimum_stock}`);
      }
      if (pedidoDaOpcao.ponto_de_reposicao !== undefined) {
        opcao.reorder_point = Math.max(0, pedidoDaOpcao.ponto_de_reposicao);
        partes.push(`ponto de reposição ${opcao.reorder_point}`);
      }
      if (pedidoDaOpcao.foto_id !== undefined || pedidoDaOpcao.foto_numero !== undefined) {
        let fotoId: string | null = null;
        if (pedidoDaOpcao.foto_numero !== undefined && pedidoDaOpcao.foto_numero > 0) {
          const foto = contexto.fotos[pedidoDaOpcao.foto_numero - 1];
          if (!foto) return { ok: false, erro: `O produto não tem a foto número ${pedidoDaOpcao.foto_numero} (tem ${contexto.fotos.length}).` };
          fotoId = foto.id;
        } else if (pedidoDaOpcao.foto_id) {
          if (!contexto.fotos.some((foto) => foto.id === pedidoDaOpcao.foto_id)) return { ok: false, erro: `A foto ${pedidoDaOpcao.foto_id} não está na galeria deste produto.` };
          fotoId = pedidoDaOpcao.foto_id;
        }
        opcao.image_id = fotoId;
        partes.push(fotoId ? `foto ${contexto.fotos.findIndex((foto) => foto.id === fotoId) + 1} da galeria` : "sem foto");
      }
      if (pedidoDaOpcao.insumo_id !== undefined) {
        const pedido = pedidoDaOpcao.insumo_id.trim();
        const insumo = pedido.toLowerCase() === "nenhum" ? "" : pedido;
        if (insumo && contexto.insumos && !contexto.insumos.has(insumo)) return { ok: false, erro: `O insumo ${insumo} não está no cadastro da empresa.` };
        /* updateOptionMaterial: sem insumo a quantidade zera; com insumo e sem quantidade, 1. */
        opcao.material_id = insumo || null;
        if (!insumo) opcao.material_qty = 0;
        else if (!opcao.material_qty || opcao.material_qty <= 0) opcao.material_qty = 1;
        partes.push(insumo ? "consome insumo" : "não consome insumo");
      }
      if (pedidoDaOpcao.insumo_quantidade !== undefined) {
        if (!opcao.material_id) return { ok: false, erro: `Escolha o insumo (insumo_id) da opção "${rotulo}" antes da quantidade.` };
        opcao.material_qty = Math.max(0, pedidoDaOpcao.insumo_quantidade);
        partes.push(`${opcao.material_qty} do insumo por peça`);
      }
      if (pedidoDaOpcao.posicao !== undefined) {
        atributo.options = mover(atributo.options, posicaoDaOpcao, pedidoDaOpcao.posicao - 1).map((item, i) => ({ ...item, position: i }));
        partes.push(`vai para a posição ${pedidoDaOpcao.posicao}`);
      }
      if (nova) mudancas.push(`${atributo.name}: opção nova ${opcao.label}${partes.length ? ` (${partes.join(", ")})` : ""}`);
      else if (partes.length) mudancas.push(`${atributo.name}, ${rotulo}: ${partes.join(", ")}`);
    }

    if (pedido.posicao !== undefined) {
      const agora = atributos.indexOf(atributo);
      atributos = mover(atributos, agora, pedido.posicao - 1).map((item, i) => ({ ...item, position: i }));
      mudancas.push(`atributo ${atributo.name} vai para a posição ${pedido.posicao}`);
    }
  }

  return { ok: true, atributos, mudancas, saem, opcoesNovas };
}

function assinatura(combinacao: Record<string, string>) {
  return Object.entries(combinacao)
    .map(([chave, valor]) => [chave.trim().toLowerCase(), (valor || "").trim().toLowerCase()] as const)
    .sort((a, b) => a[0].localeCompare(b[0]))
    .map(([chave, valor]) => `${chave}:${valor}`)
    .join("|");
}

/**
 * As combinações que a tela manda junto com os atributos. No estoque por
 * combinação, a aba Variações refaz a lista a cada mudança (todas as
 * combinações das opções, guardando código e saldo das que já existiam);
 * nos outros casos, vai o que estava gravado.
 */
export function variacoesParaSalvar(atributos: AtributoDaFicha[], atuais: VariacaoDaFicha[], escopoEfetivo: string): VariacaoDaFicha[] {
  if (escopoEfetivo !== "variant") return atuais;
  const validos = atributos
    .map((atributo) => ({ name: (atributo.name || "").trim(), options: (atributo.options || []).map((opcao) => (opcao.label || "").trim()).filter(Boolean) }))
    .filter((atributo) => atributo.name && atributo.options.length > 0);
  if (!validos.length) return [];
  const combinacoes: Record<string, string>[] = [];
  const andar = (indice: number, atual: Record<string, string>) => {
    if (indice >= validos.length) {
      combinacoes.push({ ...atual });
      return;
    }
    for (const rotulo of validos[indice].options) andar(indice + 1, { ...atual, [validos[indice].name]: rotulo });
  };
  andar(0, {});
  const existentes = new Map(atuais.map((variacao) => [assinatura(variacao.combination || {}), variacao]));
  return combinacoes.map((combinacao) => {
    const existente = existentes.get(assinatura(combinacao));
    return { id: existente?.id, combination: combinacao, sku: existente?.sku || "", price: null, stock: Number(existente?.stock) || 0, cost_adjustment: 0 };
  });
}

/* ============================================================== serviços */

export type ServicoDaBiblioteca = { id: string; titulo: string; valeParaTodos: boolean; ativo: boolean };
export type Vinculo = { serviceId: string; ligado: boolean };

/** Ligado neste produto? O vínculo manda; sem vínculo, o "oferecer em todos" da biblioteca. */
export function servicoLigado(servico: ServicoDaBiblioteca, vinculos: Vinculo[]) {
  const decisao = vinculos.find((vinculo) => vinculo.serviceId === servico.id);
  return decisao === undefined ? servico.valeParaTodos : decisao.ligado;
}

/**
 * O "alternar" da tela: quando a escolha volta a ser igual ao padrão da
 * biblioteca, o vínculo some (o produto volta a acompanhar o padrão).
 * `padrao` = volta a seguir a biblioteca de propósito.
 */
export function vinculosDepoisDe(vinculos: Vinculo[], servico: ServicoDaBiblioteca, escolha: "oferecer" | "não oferecer" | "padrão"): Vinculo[] {
  const semEste = vinculos.filter((vinculo) => vinculo.serviceId !== servico.id);
  if (escolha === "padrão") return semEste;
  const agora = escolha === "oferecer";
  if (agora === servico.valeParaTodos) return semEste;
  return [...semEste, { serviceId: servico.id, ligado: agora }];
}

/* ===================================================== campos personalizados */

export type CampoPersonalizado = {
  id: string;
  title: string;
  field_type: "text" | "textarea" | "number";
  placeholder: string | null;
  required: boolean;
  variation_signature: string | null;
  position: number;
  active: boolean;
};

export const TIPOS_DE_CAMPO: Record<string, CampoPersonalizado["field_type"]> = { "texto curto": "text", "texto longo": "textarea", "número": "number" };
export const NOME_DO_TIPO_DE_CAMPO: Record<string, string> = Object.fromEntries(Object.entries(TIPOS_DE_CAMPO).map(([nome, valor]) => [valor, nome]));

export type PedidoDeCampo = {
  titulo: string;
  novo_titulo?: string;
  remover?: boolean;
  tipo?: string;
  exemplo?: string;
  obrigatorio?: boolean;
  ativo?: boolean;
  so_na_combinacao?: string;
};

/** A assinatura de uma combinação ("Cor: Azul, Tamanho: P"), como a tela guarda (cor:azul|tamanho:p). */
export function assinaturaDaCombinacao(texto: string): string {
  const pares = texto
    .split(/[,;|]/)
    .map((parte) => parte.split(":"))
    .filter((par) => par.length === 2 && par[0].trim() && par[1].trim())
    .map(([chave, valor]) => [chave.trim().toLowerCase(), valor.trim().toLowerCase()] as const)
    .sort((a, b) => a[0].localeCompare(b[0]));
  return pares.map(([chave, valor]) => `${chave}:${valor}`).join("|");
}

export function camposDepoisDe(
  atuais: CampoPersonalizado[],
  pedidos: PedidoDeCampo[],
  novoId: () => string
): { ok: true; campos: CampoPersonalizado[]; mudancas: string[] } | { ok: false; erro: string } {
  let campos = atuais.map((campo) => ({ ...campo }));
  const mudancas: string[] = [];
  for (const pedido of pedidos) {
    const titulo = pedido.titulo.trim();
    const indice = campos.findIndex((campo) => igual(campo.title, titulo));
    if (pedido.remover) {
      if (indice < 0) return { ok: false, erro: `O campo "${titulo}" não existe neste produto.` };
      campos.splice(indice, 1);
      mudancas.push(`campo ${titulo} sai`);
      continue;
    }
    let tipo: CampoPersonalizado["field_type"] | undefined;
    if (pedido.tipo !== undefined) {
      tipo = TIPOS_DE_CAMPO[pedido.tipo];
      if (!tipo) return { ok: false, erro: `Tipo "${pedido.tipo}" não existe. Use: ${Object.keys(TIPOS_DE_CAMPO).join(", ")}.` };
    }
    const campo: CampoPersonalizado =
      indice >= 0
        ? campos[indice]
        : { id: novoId(), title: titulo, field_type: "text", placeholder: null, required: false, variation_signature: null, position: campos.length, active: true };
    if (indice < 0) {
      campos.push(campo);
      mudancas.push(`campo novo: ${titulo}`);
    }
    if (pedido.novo_titulo !== undefined) {
      const novo = pedido.novo_titulo.trim();
      if (!novo) return { ok: false, erro: `O novo título do campo "${titulo}" está vazio.` };
      campo.title = novo;
      mudancas.push(`campo ${titulo} passa a se chamar ${novo}`);
    }
    if (tipo) campo.field_type = tipo;
    if (pedido.exemplo !== undefined) campo.placeholder = pedido.exemplo.trim() || null;
    if (pedido.obrigatorio !== undefined) campo.required = pedido.obrigatorio;
    if (pedido.ativo !== undefined) campo.active = pedido.ativo;
    if (pedido.so_na_combinacao !== undefined) {
      const texto = pedido.so_na_combinacao.trim();
      const assinaturaNova = texto ? assinaturaDaCombinacao(texto) : "";
      if (texto && !assinaturaNova) return { ok: false, erro: 'Escreva a combinação como "Cor: Azul, Tamanho: P".' };
      campo.variation_signature = assinaturaNova || null;
    }
    if (indice >= 0 && (tipo || pedido.exemplo !== undefined || pedido.obrigatorio !== undefined || pedido.ativo !== undefined || pedido.so_na_combinacao !== undefined)) {
      mudancas.push(`campo ${campo.title} ajustado`);
    }
  }
  campos = campos.map((campo, posicao) => ({ ...campo, position: posicao }));
  return { ok: true, campos, mudancas };
}

/* ========================================================= ficha técnica */

/** A conta da aba Composição ao adicionar insumo: quantidade × custo × (1 + perda). Rendimento vira 1 ÷ peças. */
export function linhaDeInsumo(insumo: { custoPorUnidade: number; perdaPct: number }, pedido: { quantidade?: number; pecas_por_unidade?: number }) {
  const rendimento = Number(pedido.pecas_por_unidade);
  const temRendimento = Number.isFinite(rendimento) && rendimento > 0;
  const quantidade = temRendimento ? Math.round((1 / rendimento) * 10000) / 10000 : Number(pedido.quantidade);
  if (!Number.isFinite(quantidade) || quantidade <= 0) return null;
  return {
    consumption_qty: quantidade,
    waste_pct: insumo.perdaPct,
    cost_total: quantidade * insumo.custoPorUnidade * (1 + insumo.perdaPct / 100),
    pieces_per_unit: temRendimento ? rendimento : null,
  };
}

/** Máquina: (tempo + preparo) × custo por hora ÷ 60. Mão de obra: tempo × valor da hora ÷ 60. */
export function custoDoProcesso(tipo: "MACHINE" | "ROLE", minutos: number, preparo: number, valorDaHora: number) {
  return tipo === "MACHINE" ? (minutos + preparo) * (valorDaHora / 60) : minutos * (valorDaHora / 60);
}
