import crypto from "crypto";
import { NextResponse } from "next/server";
import type { SupabaseClient } from "@supabase/supabase-js";
import { createAdminClient } from "@/lib/supabase-server";
import { canCompany, type CompanyContext } from "@/lib/company-context";
import { avisarLoja } from "@/lib/loja/avisar-loja";
import { parseMoneyInput } from "@/utils/formatters";
import { numeroDoTexto } from "@/lib/numero-do-campo";
import {
  type ProductPriceTier,
  validateAndNormalizePriceTiers,
  validatePriceTiersAgainstSaleLimits,
} from "@/lib/price-tiers";
import { sanitizeProductDescription } from "@/lib/product-description-rich-text";
import { normalizeProductImagesInput } from "@/lib/product-images";
import { lerVinculosDeServico, type VinculoDeServico } from "@/lib/vinculos-de-servico";
import { normalizeProductCustomFieldsConfig } from "@/lib/product-custom-fields";
import { custoDoProduto } from "@/lib/precificacao/custo-do-produto";
import { lerLinkDeVideo } from "@/lib/digitais/curso";
import { lerOpcoesDoVideo } from "@/lib/opcoes-do-video";
import { duracaoPassou, VIDEO_DO_PRODUTO } from "@/lib/limites-do-video-do-produto";
import { linhaDaComposicaoParaGravar } from "@/lib/estoque/gravar-composicao";

/*
 * A gravação do produto físico, igual para o painel e para o assistente.
 *
 * Era o miolo da rota /api/products (POST, PATCH e DELETE). Mudou para cá sem
 * mudar uma linha da regra, para o assistente (MCP) gravar pelo MESMO
 * caminho da tela do produto: as mesmas conferências, os mesmos formatos no
 * banco (selected_options, estoque por opção, receitas, serviços) e os mesmos
 * avisos à loja. A rota só confere quem chama e repassa.
 *
 * Quem chama entrega a sessão da pessoa (as travas do banco valem), quem é
 * ela e como ler a empresa dela. A resposta é a mesma da rota (NextResponse),
 * para a rota devolver como está e o assistente ler status e corpo.
 */

export type EntradaDaGravacao = {
  supabase: SupabaseClient;
  user: { id: string };
  obterContexto: () => Promise<CompanyContext>;
  body: unknown;
  /* O pedido de origem: avisarLoja usa para achar o endereço da loja. */
  req: Request;
};


const writableFields = new Set([
  "video_file_id",
  "video_link",
  "video_flutuante",
  "video_opcoes",
  "name",
  "description",
  "price",
  "price_on_request",
  "use_variation_price_only",
  "stock",
  "unit_type",
  "category_id",
  "image_url",
  "show_in_catalog",
  "catalog_color",
  "active",
  "is_catalog_exclusive",
  "estimated_base_cost",
  "cost_total",
  "pricing_reference_width_m",
  "pricing_reference_height_m",
  "min_sale_qty_enabled",
  "min_sale_qty",
  "max_sale_qty_enabled",
  "max_sale_qty",
  "allowed_sale_qty_enabled",
  "allowed_sale_quantities",
  "inventory_scope",
  "inventory_attribute_name",
  "allow_negative_stock",
  "track_inventory",
  "sku",
  "barcode",
  "custom_fields_config",
  "shipping_weight_kg",
  "shipping_width_cm",
  "shipping_height_cm",
  "shipping_length_cm",
  "production_time_value",
  "production_time_unit",
  "production_template_url",
  "production_template_name",
  "production_template_format",
  "production_templates",
  /* Nota fiscal (migration 135). Conferidos em normalizarCamposFiscais. */
  "fiscal_tipo",
  "ncm",
  "cest",
  "fiscal_origem",
  "fiscal_perfil_id",
  /* Valor mínimo do item (migration 194). Conferidos em normalizarValorMinimo. */
  "valor_minimo_item",
  "valor_minimo_orcamentos",
  "valor_minimo_loja",
  "valor_minimo_pdv",
]);

/*
  O valor mínimo do item: um valor em reais (vazio ou zero desliga) e onde
  ele vale. Só faz sentido em produto por m² ou por metro; nos outros o valor
  fica gravado, mas a conta (lib/adicionais.ts) não o usa.
*/
function normalizarValorMinimo(patch: Record<string, unknown>): string | null {
  if ("valor_minimo_item" in patch) {
    const bruto = patch.valor_minimo_item;
    const valor =
      bruto === null || bruto === undefined || bruto === ""
        ? 0
        : typeof bruto === "number"
          ? bruto
          : numeroDoTexto(String(bruto));
    if (!Number.isFinite(valor) || valor < 0) return "Valor mínimo do item inválido.";
    if (valor > 10_000_000) return "Valor mínimo do item alto demais.";
    patch.valor_minimo_item = valor > 0 ? Math.round(valor * 100) / 100 : null;
  }
  for (const chave of ["valor_minimo_orcamentos", "valor_minimo_loja", "valor_minimo_pdv"]) {
    if (chave in patch) patch[chave] = patch[chave] !== false;
  }
  return null;
}

/* Os campos fiscais chegam do formulário; aqui viram o que o banco aceita, e a
   tributação escolhida precisa ser da própria empresa. */
/*
  O vídeo do produto (migration 183): ou um vídeo dos arquivos da própria
  empresa (Produtos digitais), ou um link do YouTube/Vimeo. Nunca os dois.
*/
async function normalizarVideoDoProduto(
  patch: Record<string, unknown>,
  admin: ReturnType<typeof createAdminClient>,
  empresaId: string,
  /* O produto que está sendo salvo (vazio na criação). */
  produtoId: string | null = null
): Promise<string | null> {
  if ("video_flutuante" in patch) patch.video_flutuante = patch.video_flutuante === true;
  if ("video_opcoes" in patch) patch.video_opcoes = lerOpcoesDoVideo(patch.video_opcoes);
  if ("video_link" in patch) {
    const bruto = patch.video_link;
    const texto = typeof bruto === "string" ? bruto : bruto && typeof bruto === "object" ? (bruto as { url?: unknown }).url : null;
    if (texto === null || texto === undefined || String(texto).trim() === "") {
      patch.video_link = null;
    } else {
      const link = lerLinkDeVideo(texto);
      if (!link || link.provedor === "panda") return "Cole o link de um vídeo do YouTube ou do Vimeo.";
      patch.video_link = link;
      patch.video_file_id = null;
    }
  }
  if ("video_file_id" in patch && patch.video_file_id) {
    const id = String(patch.video_file_id);
    if (!/^[0-9a-f-]{36}$/i.test(id)) return "Vídeo inválido.";
    const { data } = await admin.from("digital_files").select("id,user_id,categoria,status,tamanho_bytes,meta").eq("id", id).maybeSingle();
    const arquivo = data as { user_id: string; categoria: string; status: string; tamanho_bytes: number | null; meta: Record<string, unknown> | null } | null;
    if (!arquivo || arquivo.user_id !== empresaId) return "Vídeo não encontrado nos arquivos da empresa.";
    if (arquivo.categoria !== "video") return "O arquivo escolhido não é um vídeo.";
    if (arquivo.status === "recusado" || arquivo.status === "apagado") return "Esse vídeo não está mais disponível.";

    /* As regras do vídeo enviado (lib/limites-do-video-do-produto.ts) valem
       quando o vídeo do produto muda: salvar de novo não trava. */
    const { data: atual } = produtoId
      ? await admin.from("products").select("video_file_id").eq("id", produtoId).maybeSingle()
      : { data: null };
    if ((atual as { video_file_id?: string | null } | null)?.video_file_id !== id) {
      if (Number(arquivo.tamanho_bytes) > VIDEO_DO_PRODUTO.bytes) return "O vídeo do produto pode ter até 50 MB.";
      const duracao = typeof arquivo.meta?.duracao === "number" ? arquivo.meta.duracao : null;
      if (duracaoPassou(duracao)) return "O vídeo do produto pode ter até 1 minuto.";
      let outros = admin.from("products").select("id", { count: "exact", head: true }).eq("user_id", empresaId).not("video_file_id", "is", null);
      if (produtoId) outros = outros.neq("id", produtoId);
      const { count } = await outros;
      if ((count ?? 0) >= VIDEO_DO_PRODUTO.produtos) {
        return `Sua empresa já tem vídeo enviado em ${VIDEO_DO_PRODUTO.produtos} produtos, o máximo. Tire de algum ou use um link do YouTube neste.`;
      }
    }
    patch.video_file_id = id;
    patch.video_link = null;
  } else if ("video_file_id" in patch) {
    patch.video_file_id = null;
  }
  return null;
}

async function normalizarCamposFiscais(
  patch: Record<string, unknown>,
  admin: ReturnType<typeof createAdminClient>,
  empresaId: string
): Promise<string | null> {
  if ("fiscal_tipo" in patch) {
    patch.fiscal_tipo = patch.fiscal_tipo === "servico" ? "servico" : patch.fiscal_tipo === "mercadoria" ? "mercadoria" : null;
  }
  if ("ncm" in patch) {
    const ncm = String(patch.ncm ?? "").replace(/\D/g, "");
    if (ncm && ncm.length !== 8) return "O NCM precisa ter 8 números.";
    patch.ncm = ncm || null;
  }
  if ("cest" in patch) {
    const cest = String(patch.cest ?? "").replace(/\D/g, "");
    if (cest && cest.length !== 7) return "O CEST precisa ter 7 números.";
    patch.cest = cest || null;
  }
  if ("fiscal_origem" in patch) {
    const origem = patch.fiscal_origem === null || patch.fiscal_origem === "" || patch.fiscal_origem === undefined ? null : Number(patch.fiscal_origem);
    if (origem !== null && (!Number.isInteger(origem) || origem < 0 || origem > 8)) return "Origem da mercadoria inválida.";
    patch.fiscal_origem = origem;
  }
  if ("fiscal_perfil_id" in patch) {
    const perfilId = typeof patch.fiscal_perfil_id === "string" ? patch.fiscal_perfil_id.trim() : "";
    if (!perfilId) {
      patch.fiscal_perfil_id = null;
    } else {
      const { data } = await admin.from("fiscal_perfis").select("id").eq("id", perfilId).eq("empresa_id", empresaId).maybeSingle();
      if (!data) return "A tributação escolhida não existe.";
      patch.fiscal_perfil_id = perfilId;
    }
  }
  return null;
}

const saleQuantityFields = [
  "min_sale_qty_enabled",
  "min_sale_qty",
  "max_sale_qty_enabled",
  "max_sale_qty",
  "allowed_sale_qty_enabled",
  "allowed_sale_quantities",
] as const;

type CompositionCostRow = { cost_total?: number | null };
type CompositionMaterialInput = {
  material_id?: string | null;
  consumption_qty?: number | null;
  waste_pct?: number | null;
  cost_total?: number | null;
  consumption_base?: string | null;
  pieces_per_unit?: number | null;
};
type CompositionProcessInput = {
  kind?: "MACHINE" | "ROLE" | string | null;
  minutes?: number | null;
  setup_minutes?: number | null;
  cost_total?: number | null;
  machine_id?: string | null;
  role_id?: string | null;
  process_name?: string | null;
  consumption_base?: string | null;
};

/* As cinco bases do levantamento do ramo. Valor desconhecido vira o default do
   modelo antigo (por peça) em vez de estourar o check do banco. */
const BASES_DE_CONSUMO = new Set(["por_peca", "por_m2", "por_m_largura", "por_m_altura", "por_perimetro"]);
const baseDeConsumo = (v: unknown): string =>
  typeof v === "string" && BASES_DE_CONSUMO.has(v) ? v : "por_peca";
type ProductAttributeInput = {
  name?: string | null;
  options?: unknown;
  allow_multiple?: boolean | null;
  is_required?: boolean | null;
  position?: number | null;
  /* Como a loja desenha (null = automatico) e se ela mostra. */
  display_style?: string | null;
  show_in_store?: boolean | null;
};

const ESTILOS_DE_VARIACAO = new Set(["cores", "miniaturas", "botoes", "lista", "dropdown"]);

/* O estilo so vale se for um dos que a loja sabe desenhar; qualquer outra
   coisa vira automatico. */
function normalizeDisplayStyle(value: unknown) {
  const style = typeof value === "string" ? value.trim() : "";
  return ESTILOS_DE_VARIACAO.has(style) ? style : null;
}

type ProductAttributeOptionInput = {
  id?: string | null;
  label?: string | null;
  /* A cor que o lojista escolheu para a bolinha desta opcao. */
  swatch?: string | null;
  price?: number | null;
  price_applies_per_quantity?: boolean | null;
  price_unit?: string | null;
  stock?: number | null;
  is_required?: boolean | null;
  position?: number | null;
  image_id?: string | null;
  minimum_stock?: number | null;
  reorder_point?: number | null;
  /* Receita da opção: o insumo que esta opção consome (por peça). */
  material_id?: string | null;
  material_qty?: number | null;
};
type ProductVariationInput = {
  id?: string | null;
  combination?: Record<string, string> | null;
  sku?: string | null;
  price?: number | null;
  stock?: number | null;
  cost_adjustment?: number | null;
};

function pickWritablePatch(value: unknown) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
  const record = value as Record<string, unknown>;
  const patch: Record<string, unknown> = {};
  for (const [key, v] of Object.entries(record)) {
    if (writableFields.has(key)) patch[key] = v;
  }
  return patch;
}

/* Os campos que so dizem respeito ao saldo. Se o patch nao sai desta lista,
   quem manda e a permissao do Estoque; basta um campo de fora para voltar a
   ser edicao de produto. */
const CAMPOS_DE_ESTOQUE = new Set([
  "id",
  "stock",
  "track_inventory",
  "inventory_scope",
  "inventory_attribute_name",
  "allow_negative_stock",
  "minimum_stock",
  "reorder_point",
]);

function somenteEstoque(
  patch: Record<string, unknown>,
  extras: { attributes: unknown; variations: unknown; hasPriceTiers: boolean; hasImages: boolean }
) {
  if (extras.attributes || extras.variations || extras.hasPriceTiers || extras.hasImages) return false;
  const campos = Object.keys(patch);
  return campos.length > 0 && campos.every((campo) => CAMPOS_DE_ESTOQUE.has(campo));
}

function requiresCatalogWrite(patch: Record<string, unknown>) {
  return Object.prototype.hasOwnProperty.call(patch, "show_in_catalog") || Object.prototype.hasOwnProperty.call(patch, "catalog_color");
}

function sanitizeAttributeOptionsWithImages(options: unknown, validImageIds: Set<string>) {
  if (!Array.isArray(options)) return options;
  return options.map((option, index) => {
    if (typeof option === "string") {
      return {
        id: crypto.randomUUID(),
        label: option,
        price: 0,
        price_applies_per_quantity: true,
        stock: 0,
        is_required: false,
        position: index,
        image_id: null,
        minimum_stock: 0,
        reorder_point: 0,
      };
    }
    if (!option || typeof option !== "object") return option;
    const row = option as ProductAttributeOptionInput;
    const rawImageId = typeof row.image_id === "string" ? row.image_id : "";
    const id = typeof row.id === "string" && isUuid(row.id) ? row.id : crypto.randomUUID();
    const label = typeof row.label === "string" ? row.label.trim() : "";
    const stock = Number.isFinite(Number(row.stock)) ? Math.max(0, Number(row.stock)) : 0;
    const minimumStock = Number.isFinite(Number(row.minimum_stock)) ? Math.max(0, Number(row.minimum_stock)) : 0;
    const reorderPoint = Number.isFinite(Number(row.reorder_point)) ? Math.max(0, Number(row.reorder_point)) : 0;
    const price = Number.isFinite(Number(row.price)) ? Number(row.price) : 0;
    const position = typeof row.position === "number" && Number.isFinite(row.position) ? row.position : index;
    return {
      ...row,
      id,
      label,
      price,
      price_applies_per_quantity: row.price_applies_per_quantity !== false,
      /* Em que o adicional se multiplica: measure, piece ou order; fora disso,
         nulo, e vale a regra antiga. */
      price_unit:
        row.price_unit === "measure" || row.price_unit === "piece" || row.price_unit === "order" ? row.price_unit : null,
      stock,
      minimum_stock: minimumStock,
      reorder_point: reorderPoint,
      is_required: Boolean(row.is_required),
      position,
      image_id: rawImageId && validImageIds.has(rawImageId) ? rawImageId : null,
      /* Cor da bolinha: so hexadecimal entra, para a vitrine nunca receber
         um `background` inventado por quem chamou a API. */
      swatch: typeof row.swatch === "string" && /^#[0-9a-f]{6}$/i.test(row.swatch.trim())
        ? row.swatch.trim().toLowerCase()
        : null,
    };
  });
}

function sanitizeAttributesWithImages(
  attributes: ProductAttributeInput[],
  validImageIds: Set<string>
): ProductAttributeInput[] {
  return attributes.map((attribute) => ({
    ...attribute,
    name: typeof attribute.name === "string" ? attribute.name.trim() : attribute.name,
    options: sanitizeAttributeOptionsWithImages(attribute.options, validImageIds),
  }));
}

function isUuid(value: string) {
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
}

function normalizeInventoryScope(value: unknown) {
  if (value === "option" || value === "variant") return value;
  return "product";
}

type ProductOptionStockRow = {
  option_id: string;
  product_id: string;
  user_id: string;
  attribute_name: string;
  option_label: string;
  stock: number;
  minimum_stock: number;
  reorder_point: number;
  active: boolean;
};

function sumAllocatedOptionStock(rows: ProductOptionStockRow[]) {
  return rows.reduce((acc, row) => acc + (Number.isFinite(Number(row.stock)) ? Number(row.stock) : 0), 0);
}

function extractOptionStockRows(
  attributes: ProductAttributeInput[],
  params: {
    productId: string;
    userId: string;
    inventoryScope: string;
    inventoryAttributeName: string | null;
  }
) {
  const { productId, userId, inventoryScope, inventoryAttributeName } = params;
  if (inventoryScope !== "option") return [] as ProductOptionStockRow[];
  const normalizedInventoryAttr = (inventoryAttributeName || "").trim().toLowerCase();
  if (!normalizedInventoryAttr) return [] as ProductOptionStockRow[];

  const rows: ProductOptionStockRow[] = [];
  for (const attribute of attributes) {
    const attributeName = typeof attribute.name === "string" ? attribute.name.trim() : "";
    if (!attributeName || attributeName.toLowerCase() !== normalizedInventoryAttr) continue;
    const options = Array.isArray(attribute.options) ? attribute.options : [];
    for (const option of options) {
      if (!option || typeof option !== "object") continue;
      const row = option as ProductAttributeOptionInput;
      const optionId = typeof row.id === "string" && isUuid(row.id) ? row.id : "";
      const optionLabel = typeof row.label === "string" ? row.label.trim() : "";
      if (!optionId || !optionLabel) continue;
      rows.push({
        option_id: optionId,
        product_id: productId,
        user_id: userId,
        attribute_name: attributeName,
        option_label: optionLabel,
        stock: Number.isFinite(Number(row.stock)) ? Math.max(0, Number(row.stock)) : 0,
        minimum_stock: Number.isFinite(Number(row.minimum_stock)) ? Math.max(0, Number(row.minimum_stock)) : 0,
        reorder_point: Number.isFinite(Number(row.reorder_point)) ? Math.max(0, Number(row.reorder_point)) : 0,
        active: true,
      });
    }
  }
  return rows;
}

async function syncProductOptionStocks(
  supabase: SupabaseClient,
  params: {
    productId: string;
    userId: string;
    inventoryScope: string;
    inventoryAttributeName: string | null;
    attributes: ProductAttributeInput[];
  }
) {
  const rows = extractOptionStockRows(params.attributes, {
    productId: params.productId,
    userId: params.userId,
    inventoryScope: params.inventoryScope,
    inventoryAttributeName: params.inventoryAttributeName,
  });
  const { data: existingRowsData, error: existingRowsError } = await supabase
    .from("product_option_stocks")
    .select("option_id,stock,active")
    .eq("product_id", params.productId);
  if (existingRowsError) {
    const msg = String(existingRowsError.message || "").toLowerCase();
    const missingRelation = msg.includes("product_option_stocks") && msg.includes("does not exist");
    if (!missingRelation) throw existingRowsError;
  }

  const existingRows =
    (existingRowsData as Array<{ option_id?: string; stock?: number | null; active?: boolean | null }> | null) || [];
  const existingByOptionId = new Map(
    existingRows
      .filter((row) => typeof row.option_id === "string" && row.option_id)
      .map((row) => [String(row.option_id), Number(row.stock) || 0])
  );
  const incomingOptionIds = new Set(rows.map((row) => row.option_id));

  /* O saldo VIVO de quem já existe vence o número que veio da tela: entre
     abrir o editor e salvar pode ter havido venda, e o extrato é a verdade.
     O valor digitado só vale como saldo inicial de opção nova. */
  const rowsToUpsert = rows.map((row) => {
    const liveStock = existingByOptionId.get(row.option_id);
    return liveStock === undefined ? row : { ...row, stock: liveStock };
  });

  if (rowsToUpsert.length > 0) {
    const { error: upsertError } = await supabase.from("product_option_stocks").upsert(rowsToUpsert, { onConflict: "option_id" });
    if (upsertError) throw upsertError;
  }

  const toDeactivate = existingRows
    .filter((row) => row.active !== false && typeof row.option_id === "string" && !incomingOptionIds.has(String(row.option_id)))
    .map((row) => String(row.option_id));

  if (toDeactivate.length > 0) {
    const { error: deactivateError } = await supabase
      .from("product_option_stocks")
      .update({ active: false, updated_at: new Date().toISOString() })
      .eq("product_id", params.productId)
      .in("option_id", toDeactivate);
    if (deactivateError) throw deactivateError;
  }

  const movementRows = rowsToUpsert
    .map((row) => {
      const previousStock = existingByOptionId.get(row.option_id);
      if (previousStock === undefined) {
        /* Opção nova com saldo: o número digitado entra no extrato como saldo
           inicial — antes o saldo aparecia sem movimento nenhum que o explicasse. */
        const inicial = Number(row.stock) || 0;
        if (Math.abs(inicial) <= 0.000001) return null;
        return {
          user_id: params.userId,
          product_id: params.productId,
          option_id: row.option_id,
          quantity: Math.abs(inicial),
          type: inicial > 0 ? "adjustment_in" : "adjustment_out",
          reference_type: "product_edit",
          notes: "Saldo inicial do cadastro",
          balance_after: inicial,
        };
      }
      const delta = Number(row.stock) - Number(previousStock);
      if (Math.abs(delta) <= 0.000001) return null;
      return {
        user_id: params.userId,
        product_id: params.productId,
        option_id: row.option_id,
        quantity: Math.abs(delta),
        type: delta > 0 ? "adjustment_in" : "adjustment_out",
        reference_type: "product_edit",
        notes: "Ajuste via edição de variações",
        balance_after: Number(row.stock) || 0,
      };
    })
    .filter(
      (
        row
      ): row is {
        user_id: string;
        product_id: string;
        option_id: string;
        quantity: number;
        type: string;
        reference_type: string;
        notes: string;
        balance_after: number;
      } => row !== null
    );

  if (movementRows.length > 0) {
    const { error: movementError } = await supabase.from("product_option_stock_movements").insert(movementRows);
    if (movementError) {
      const msg = String(movementError.message || "").toLowerCase();
      const missingRelation = msg.includes("product_option_stock_movements") && msg.includes("does not exist");
      if (!missingRelation) throw movementError;
    }
  }
}

/* Receita da opção: mantém product_option_recipes espelhando o que cada opção
   declara consumir. Opção sem vínculo não gera linha; vínculo removido é
   desativado (nunca apagado — o extrato de vendas passadas referencia). */
async function syncProductOptionRecipes(
  supabase: SupabaseClient,
  params: {
    productId: string;
    userId: string;
    attributes: ProductAttributeInput[];
  }
) {
  const rows: Array<{
    user_id: string;
    product_id: string;
    option_id: string;
    material_id: string;
    qty_per_piece: number;
    active: boolean;
    updated_at: string;
  }> = [];
  for (const attribute of params.attributes) {
    const options = Array.isArray(attribute?.options) ? (attribute.options as ProductAttributeOptionInput[]) : [];
    for (const option of options) {
      const optionId = typeof option?.id === "string" && isUuid(option.id) ? option.id : null;
      const materialId = typeof option?.material_id === "string" && isUuid(option.material_id) ? option.material_id : null;
      const qty = Number(option?.material_qty);
      if (!optionId || !materialId || !Number.isFinite(qty) || qty <= 0) continue;
      rows.push({
        user_id: params.userId,
        product_id: params.productId,
        option_id: optionId,
        material_id: materialId,
        qty_per_piece: qty,
        active: true,
        updated_at: new Date().toISOString(),
      });
    }
  }
  const { data: existingData, error: existingError } = await supabase
    .from("product_option_recipes")
    .select("option_id,material_id,active")
    .eq("product_id", params.productId);
  if (existingError) {
    const msg = String(existingError.message || "").toLowerCase();
    const missingRelation = msg.includes("product_option_recipes") && msg.includes("does not exist");
    if (missingRelation) return;
    throw existingError;
  }

  if (rows.length > 0) {
    const { error: upsertError } = await supabase
      .from("product_option_recipes")
      .upsert(rows, { onConflict: "option_id,material_id" });
    if (upsertError) throw upsertError;
  }

  const incoming = new Set(rows.map((row) => `${row.option_id}:${row.material_id}`));
  const toDeactivate = ((existingData as Array<{ option_id?: string; material_id?: string; active?: boolean }> | null) || [])
    .filter((row) => row.active !== false && row.option_id && row.material_id && !incoming.has(`${row.option_id}:${row.material_id}`));
  for (const row of toDeactivate) {
    const { error: deactivateError } = await supabase
      .from("product_option_recipes")
      .update({ active: false, updated_at: new Date().toISOString() })
      .eq("product_id", params.productId)
      .eq("option_id", row.option_id as string)
      .eq("material_id", row.material_id as string);
    if (deactivateError) throw deactivateError;
  }
}

/* O que este produto oferece da biblioteca de serviços.

   Só liga e desliga: o preço, o texto e o custo do serviço moram na
   biblioteca da empresa, num lugar só. A linha existe para o produto poder
   discordar do padrão — inclusive para DESLIGAR um serviço que vale para
   todos. Por isso gravamos também o `false`, em vez de apagar a linha. */
async function syncProductServiceLinks(
  supabase: SupabaseClient,
  params: { productId: string; userId: string; links: VinculoDeServico[] }
) {
  const agora = new Date().toISOString();
  const linhas = params.links.map((vinculo, indice) => ({
    user_id: params.userId,
    product_id: params.productId,
    service_id: vinculo.serviceId,
    enabled: vinculo.ligado,
    position: indice,
    updated_at: agora,
  }));

  if (linhas.length > 0) {
    const { error } = await supabase
      .from("product_optional_services")
      .upsert(linhas, { onConflict: "product_id,service_id" });
    if (error) {
      const msg = String(error.message || "").toLowerCase();
      /* Banco sem a migração 113: o produto salva igual, só não guarda quais
         serviços oferece. */
      if (msg.includes("product_optional_services") && msg.includes("does not exist")) return;
      throw error;
    }
  }

  const enviados = new Set(linhas.map((linha) => linha.service_id));
  const { data: existentes, error: leituraError } = await supabase
    .from("product_optional_services")
    .select("service_id")
    .eq("product_id", params.productId);
  if (leituraError) {
    const msg = String(leituraError.message || "").toLowerCase();
    if (msg.includes("product_optional_services") && msg.includes("does not exist")) return;
    throw leituraError;
  }

  /* Serviço que sumiu da lista volta a obedecer ao padrão da biblioteca:
     a linha do produto some, e o "vale para todos" volta a mandar. */
  const sobrando = ((existentes as Array<{ service_id?: string }> | null) || [])
    .map((linha) => linha.service_id)
    .filter((id): id is string => typeof id === "string" && id.length > 0 && !enviados.has(id));
  for (const serviceId of sobrando) {
    await supabase
      .from("product_optional_services")
      .delete()
      .eq("product_id", params.productId)
      .eq("service_id", serviceId);
  }
}

type ProductVariationStockRow = {
  id: string;
  product_id: string;
  combination: Record<string, string>;
  sku: string | null;
  price: number | null;
  stock: number;
  cost_adjustment: number;
  active: boolean;
};

function normalizeVariationCombination(combination: Record<string, string> | null | undefined) {
  const normalized: Record<string, string> = {};
  if (!combination || typeof combination !== "object") return normalized;
  for (const [key, value] of Object.entries(combination)) {
    const normalizedKey = (key || "").trim();
    const normalizedValue = (value || "").trim();
    if (!normalizedKey || !normalizedValue) continue;
    normalized[normalizedKey] = normalizedValue;
  }
  return normalized;
}

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

function extractVariationRows(variations: ProductVariationInput[], productId: string): ProductVariationStockRow[] {
  return variations
    .map((variation) => {
      const id = typeof variation.id === "string" && isUuid(variation.id) ? variation.id : crypto.randomUUID();
      const combination = normalizeVariationCombination(variation.combination || {});
      if (!Object.keys(combination).length) return null;
      return {
        id,
        product_id: productId,
        combination,
        sku: typeof variation.sku === "string" ? variation.sku : null,
        price: Number.isFinite(Number(variation.price)) ? Number(variation.price) : null,
        stock: Number.isFinite(Number(variation.stock)) ? Math.max(0, Number(variation.stock)) : 0,
        cost_adjustment: Number.isFinite(Number(variation.cost_adjustment)) ? Number(variation.cost_adjustment) : 0,
        active: true,
      };
    })
    .filter((row): row is ProductVariationStockRow => row !== null);
}

async function syncProductVariations(
  supabase: SupabaseClient,
  params: {
    productId: string;
    userId: string;
    variations: ProductVariationInput[];
  }
) {
  const { data: existingRowsData, error: existingRowsError } = await supabase
    .from("product_variations")
    .select("id,combination,stock,active")
    .eq("product_id", params.productId);
  if (existingRowsError) throw existingRowsError;

  const existingRows =
    (existingRowsData as Array<{
      id?: string | null;
      combination?: Record<string, string> | null;
      stock?: number | null;
      active?: boolean | null;
    }> | null) || [];
  const existingById = new Map(
    existingRows
      .filter((row) => typeof row.id === "string" && row.id)
      .map((row) => [String(row.id), row])
  );
  const existingBySignature = new Map<string, { id: string; stock: number }>();
  for (const row of existingRows) {
    if (typeof row.id !== "string" || !row.id) continue;
    const combination = normalizeVariationCombination((row.combination || {}) as Record<string, string>);
    if (!Object.keys(combination).length) continue;
    existingBySignature.set(combinationSignature(combination), { id: row.id, stock: Number(row.stock) || 0 });
  }

  const extracted = extractVariationRows(params.variations, params.productId);
  const rows: ProductVariationStockRow[] = extracted.map((row) => {
    if (existingById.has(row.id)) return row;
    const match = existingBySignature.get(combinationSignature(row.combination));
    if (!match) return row;
    return { ...row, id: match.id };
  });

  const incomingIds = new Set(rows.map((row) => row.id));

  /* O saldo VIVO de quem já existe vence o número que veio da tela (como nas
     opções): entre abrir o editor e salvar pode ter havido venda, e o extrato
     é a verdade. Regravar o número da tela desfazia vendas — e o Math.max(0)
     da leitura zerava saldo negativo. O valor digitado só vale como saldo
     inicial de combinação nova. */
  const rowsToUpsert = rows.map((row) => {
    const previous = existingById.get(row.id);
    if (!previous) return row;
    return { ...row, stock: Number(previous.stock) || 0 };
  });

  if (rowsToUpsert.length > 0) {
    const { error: upsertError } = await supabase.from("product_variations").upsert(rowsToUpsert, { onConflict: "id" });
    if (upsertError) throw upsertError;
  }

  const toDeactivate = existingRows
    .filter((row) => row.active !== false && typeof row.id === "string" && !incomingIds.has(String(row.id)))
    .map((row) => String(row.id));

  if (toDeactivate.length > 0) {
    const { error: deactivateError } = await supabase
      .from("product_variations")
      .update({ active: false, updated_at: new Date().toISOString() })
      .eq("product_id", params.productId)
      .in("id", toDeactivate);
    if (deactivateError) throw deactivateError;
  }

  const movementRows = rowsToUpsert
    .map((row) => {
      if (existingById.has(row.id)) return null;
      const inicial = Number(row.stock) || 0;
      if (Math.abs(inicial) <= 0.000001) return null;
      return {
        user_id: params.userId,
        product_id: params.productId,
        variation_id: row.id,
        combination: row.combination,
        quantity: Math.abs(inicial),
        type: inicial > 0 ? "adjustment_in" : "adjustment_out",
        reference_type: "product_edit",
        notes: "Saldo inicial do cadastro",
        balance_after: inicial,
      };
    })
    .filter(
      (
        row
      ): row is {
        user_id: string;
        product_id: string;
        variation_id: string;
        combination: Record<string, string>;
        quantity: number;
        type: string;
        reference_type: string;
        notes: string;
        balance_after: number;
      } => row !== null
    );

  if (movementRows.length > 0) {
    const { error: movementError } = await supabase.from("product_variation_stock_movements").insert(movementRows);
    if (movementError) {
      const msg = String(movementError.message || "").toLowerCase();
      const missingRelation = msg.includes("product_variation_stock_movements") && msg.includes("does not exist");
      if (!missingRelation) throw movementError;
    }
  }
}

function parsePositiveQuantityValue(value: unknown) {
  if (value === null || value === undefined || value === "") {
    return { value: null as number | null, error: null as string | null };
  }

  let parsed: number;
  if (typeof value === "number") {
    parsed = value;
  } else if (typeof value === "string") {
    parsed = Number.parseFloat(value.replace(",", "."));
  } else {
    return { value: null, error: "valor invalido" };
  }

  if (!Number.isFinite(parsed)) {
    return { value: null, error: "valor invalido" };
  }

  if (parsed <= 0) {
    return { value: null, error: "deve ser maior que zero" };
  }

  return { value: Number(parsed.toFixed(3)), error: null as string | null };
}

function parseAllowedSaleQuantitiesValue(value: unknown) {
  if (value === null || value === undefined || value === "") {
    return { values: [] as number[], error: null as string | null };
  }

  let rawItems: unknown[] = [];
  if (Array.isArray(value)) {
    rawItems = value;
  } else if (typeof value === "string") {
    rawItems = value
      .split(/[\s,;|/]+/)
      .map((entry) => entry.trim())
      .filter(Boolean);
  } else {
    return { values: [], error: "formato invalido" };
  }

  const normalized = new Set<number>();
  for (const entry of rawItems) {
    const parsed = parsePositiveQuantityValue(entry);
    if (parsed.error || parsed.value === null) {
      return { values: [], error: "valor invalido na lista" };
    }
    normalized.add(Number(parsed.value.toFixed(3)));
  }

  return {
    values: Array.from(normalized).sort((a, b) => a - b),
    error: null as string | null,
  };
}

type ProductionTimeUnit = "hours" | "days" | "business_days" | "weeks";
type ProductionTemplateLegacyFormat = "pdf" | "ai" | "cdr";
type ProductionTemplateFormat = ProductionTemplateLegacyFormat | "canva";
type ProductionTemplateItem = {
  format: ProductionTemplateFormat;
  url: string;
  name: string;
};

function normalizeProductionTimeUnit(value: unknown): ProductionTimeUnit | null {
  if (typeof value !== "string") return null;
  const normalized = value.trim().toLowerCase();
  if (
    normalized === "hours" ||
    normalized === "days" ||
    normalized === "business_days" ||
    normalized === "weeks"
  ) {
    return normalized;
  }
  return null;
}

function normalizeProductionTemplateLegacyFormat(value: unknown): ProductionTemplateLegacyFormat | null {
  if (typeof value !== "string") return null;
  const normalized = value.trim().toLowerCase();
  if (normalized === "pdf" || normalized === "ai" || normalized === "cdr") {
    return normalized;
  }
  return null;
}

function normalizeProductionTemplateFormat(value: unknown): ProductionTemplateFormat | null {
  if (typeof value !== "string") return null;
  const normalized = value.trim().toLowerCase();
  if (normalized === "pdf" || normalized === "ai" || normalized === "cdr" || normalized === "canva") {
    return normalized;
  }
  return null;
}

function normalizeProductionTemplatesInput(value: unknown, fallback?: unknown): ProductionTemplateItem[] {
  const source = Array.isArray(value) ? value : Array.isArray(fallback) ? fallback : [];
  const templates: ProductionTemplateItem[] = [];
  const seenFormats = new Set<ProductionTemplateFormat>();

  for (const item of source) {
    if (!item || typeof item !== "object") continue;
    const row = item as Record<string, unknown>;
    const format = normalizeProductionTemplateFormat(row.format);
    const url = typeof row.url === "string" ? row.url.trim() : "";
    const name = typeof row.name === "string" ? row.name.trim() : "";
    if (!format || !url) continue;
    if (seenFormats.has(format)) continue;
    seenFormats.add(format);
    templates.push({
      format,
      url,
      name: name || "Gabarito",
    });
  }

  const order: Record<ProductionTemplateFormat, number> = { ai: 0, canva: 1, cdr: 2, pdf: 3 };
  return templates.sort((a, b) => order[a.format] - order[b.format]);
}

function buildProductionConfig(source: Record<string, unknown>, fallback?: Record<string, unknown> | null) {
  const hasTimeValue = Object.prototype.hasOwnProperty.call(source, "production_time_value");
  const hasTimeUnit = Object.prototype.hasOwnProperty.call(source, "production_time_unit");
  const hasTemplateUrl = Object.prototype.hasOwnProperty.call(source, "production_template_url");
  const hasTemplateName = Object.prototype.hasOwnProperty.call(source, "production_template_name");

  const rawTimeValue = hasTimeValue ? source.production_time_value : fallback?.production_time_value;
  const rawTimeUnit = hasTimeUnit ? source.production_time_unit : fallback?.production_time_unit;
  const rawTemplateUrl = hasTemplateUrl ? source.production_template_url : fallback?.production_template_url;
  const rawTemplateName = hasTemplateName ? source.production_template_name : fallback?.production_template_name;
  const rawTemplateFormat = Object.prototype.hasOwnProperty.call(source, "production_template_format")
    ? source.production_template_format
    : fallback?.production_template_format;
  const rawTemplates = Object.prototype.hasOwnProperty.call(source, "production_templates")
    ? source.production_templates
    : fallback?.production_templates;

  const timeValueParsed = parsePositiveQuantityValue(rawTimeValue);
  if (timeValueParsed.error) {
    return { config: null, error: `Prazo de producao: ${timeValueParsed.error}.` };
  }

  const timeValue = timeValueParsed.value;
  const timeUnit = normalizeProductionTimeUnit(rawTimeUnit);
  if (timeValue !== null && !timeUnit) {
    return { config: null, error: "Selecione a unidade do prazo de producao." };
  }
  if (timeValue === null && rawTimeUnit !== null && rawTimeUnit !== undefined && String(rawTimeUnit).trim() !== "") {
    return { config: null, error: "Informe o prazo de producao para usar a unidade selecionada." };
  }

  const templateUrl = typeof rawTemplateUrl === "string" ? rawTemplateUrl.trim() : "";
  const templateName = typeof rawTemplateName === "string" ? rawTemplateName.trim() : "";
  const templateFormat = normalizeProductionTemplateLegacyFormat(rawTemplateFormat);
  const templates = normalizeProductionTemplatesInput(rawTemplates, fallback?.production_templates);

  if ((templateUrl && !templateName) || (!templateUrl && templateName)) {
    return { config: null, error: "O gabarito precisa ter arquivo e nome juntos." };
  }
  if (templateUrl && !templateFormat) {
    return { config: null, error: "Selecione o formato do gabarito." };
  }
  if (!templateUrl && rawTemplateFormat !== null && rawTemplateFormat !== undefined && String(rawTemplateFormat).trim() !== "") {
    return { config: null, error: "Envie o gabarito para usar um formato." };
  }

  const effectiveTemplates =
    templates.length > 0
      ? templates
      : templateUrl && templateFormat
        ? [
            {
              format: templateFormat,
              url: templateUrl,
              name: templateName || "Gabarito",
            },
          ]
        : [];

  const preferredLegacyTemplate =
    effectiveTemplates.find((item) => item.format === "pdf") ||
    effectiveTemplates.find((item) => item.format === "ai") ||
    effectiveTemplates.find((item) => item.format === "cdr") ||
    null;

  return {
    config: {
      production_time_value: timeValue,
      production_time_unit: timeValue !== null ? timeUnit : null,
      production_template_url: preferredLegacyTemplate?.url || null,
      production_template_name: preferredLegacyTemplate?.name || null,
      production_template_format: preferredLegacyTemplate?.format || null,
      production_templates: effectiveTemplates,
    },
    error: null as string | null,
  };
}

function buildSaleQuantityConfig(
  source: Record<string, unknown>,
  fallback?: Record<string, unknown> | null
) {
  const hasMinEnabled = Object.prototype.hasOwnProperty.call(source, "min_sale_qty_enabled");
  const hasMaxEnabled = Object.prototype.hasOwnProperty.call(source, "max_sale_qty_enabled");
  const hasAllowedEnabled = Object.prototype.hasOwnProperty.call(source, "allowed_sale_qty_enabled");
  const hasMinValue = Object.prototype.hasOwnProperty.call(source, "min_sale_qty");
  const hasMaxValue = Object.prototype.hasOwnProperty.call(source, "max_sale_qty");
  const hasAllowedValues = Object.prototype.hasOwnProperty.call(source, "allowed_sale_quantities");

  const minEnabled = hasMinEnabled
    ? Boolean(source.min_sale_qty_enabled)
    : Boolean(fallback?.min_sale_qty_enabled);
  const maxEnabled = hasMaxEnabled
    ? Boolean(source.max_sale_qty_enabled)
    : Boolean(fallback?.max_sale_qty_enabled);
  const allowedEnabled = hasAllowedEnabled
    ? Boolean(source.allowed_sale_qty_enabled)
    : Boolean(fallback?.allowed_sale_qty_enabled);

  const minRaw = hasMinValue ? source.min_sale_qty : fallback?.min_sale_qty;
  const maxRaw = hasMaxValue ? source.max_sale_qty : fallback?.max_sale_qty;
  const allowedRaw = hasAllowedValues ? source.allowed_sale_quantities : fallback?.allowed_sale_quantities;

  const minParsed = parsePositiveQuantityValue(minRaw);
  if (minParsed.error) {
    return { config: null, error: `Quantidade minima: ${minParsed.error}.` };
  }
  const maxParsed = parsePositiveQuantityValue(maxRaw);
  if (maxParsed.error) {
    return { config: null, error: `Quantidade maxima: ${maxParsed.error}.` };
  }
  const allowedParsed = parseAllowedSaleQuantitiesValue(allowedRaw);
  if (allowedParsed.error) {
    return { config: null, error: `Quantidades permitidas: ${allowedParsed.error}.` };
  }

  const minQty = minEnabled ? minParsed.value : null;
  const maxQty = maxEnabled ? maxParsed.value : null;
  const allowedQuantities = allowedEnabled ? allowedParsed.values : [];

  if (minEnabled && minQty === null) {
    return { config: null, error: "Informe a quantidade minima para ativar esta regra." };
  }

  if (maxEnabled && maxQty === null) {
    return { config: null, error: "Informe a quantidade maxima para ativar esta regra." };
  }

  if (minQty !== null && maxQty !== null && maxQty < minQty) {
    return { config: null, error: "A quantidade maxima nao pode ser menor que a minima." };
  }

  if (allowedEnabled && allowedQuantities.length === 0) {
    return { config: null, error: "Informe ao menos uma quantidade permitida." };
  }

  if (minEnabled && allowedEnabled) {
    return { config: null, error: "Nao e permitido ativar Quantidade minima e Intervalo de Quantidades ao mesmo tempo." };
  }

  if (allowedQuantities.length > 0 && minQty !== null && allowedQuantities.some((value) => value < minQty)) {
    return { config: null, error: "A lista de quantidades permitidas possui valores abaixo da quantidade minima." };
  }

  if (allowedQuantities.length > 0 && maxQty !== null && allowedQuantities.some((value) => value > maxQty)) {
    return { config: null, error: "A lista de quantidades permitidas possui valores acima da quantidade maxima." };
  }

  return {
    config: {
      min_sale_qty_enabled: minEnabled,
      min_sale_qty: minQty,
      max_sale_qty_enabled: maxEnabled,
      max_sale_qty: maxQty,
      allowed_sale_qty_enabled: allowedEnabled,
      allowed_sale_quantities: allowedEnabled ? allowedQuantities : null,
    },
    error: null as string | null,
  };
}

export async function criarProdutoComoOPainel(entrada: EntradaDaGravacao) {
  const { supabase, user, obterContexto, req } = entrada;
  const ctx = await obterContexto();
  if (!canCompany(ctx, user.id, "products.catalog.write")) {
    return NextResponse.json({ error: "Sem permissão." }, { status: 403 });
  }

  const body = (entrada.body && typeof entrada.body === "object" ? entrada.body : null) as Record<string, unknown> | null;
  const patch = pickWritablePatch(body);
  if (Object.prototype.hasOwnProperty.call(patch, "description")) {
    patch.description = sanitizeProductDescription(patch.description);
  }
  if (Object.prototype.hasOwnProperty.call(patch, "custom_fields_config")) {
    patch.custom_fields_config = normalizeProductCustomFieldsConfig(patch.custom_fields_config);
  }
  /* Quais serviços da biblioteca este produto oferece. Chega como lista de
     {service_id, enabled}; o cadastro do serviço em si não vem por aqui. */
  const vinculosDeServico = lerVinculosDeServico((body as Record<string, unknown> | null)?.optional_service_links);
  const { tiers: parsedPriceTiers, error: priceTierError } = validateAndNormalizePriceTiers(body?.price_tiers);
  if (priceTierError) {
    return NextResponse.json({ error: priceTierError }, { status: 400 });
  }

  const name = typeof patch.name === "string" ? patch.name.trim() : "";
  if (!name) return NextResponse.json({ error: "Nome é obrigatório." }, { status: 400 });

  const { config: saleQuantityConfig, error: saleQuantityError } = buildSaleQuantityConfig(patch);
  if (saleQuantityError || !saleQuantityConfig) {
    return NextResponse.json({ error: saleQuantityError || "Erro ao validar quantidade minima/maxima." }, { status: 400 });
  }
  const { config: productionConfig, error: productionConfigError } = buildProductionConfig(patch);
  if (productionConfigError || !productionConfig) {
    return NextResponse.json({ error: productionConfigError || "Erro ao validar dados de producao." }, { status: 400 });
  }

  const { images: normalizedImages, error: imageError } = normalizeProductImagesInput(body?.images);
  if (imageError) {
    return NextResponse.json({ error: imageError }, { status: 400 });
  }
  const validImageIds = new Set(normalizedImages.map((image) => image.id));

  const writeUserId = ctx.ownerId ?? ctx.companyId;
  const erroFiscalNaCriacao = await normalizarCamposFiscais(patch, createAdminClient(), String(writeUserId));
  if (erroFiscalNaCriacao) return NextResponse.json({ error: erroFiscalNaCriacao }, { status: 400 });
  const erroDoVideoNaCriacao = await normalizarVideoDoProduto(patch, createAdminClient(), String(writeUserId));
  if (erroDoVideoNaCriacao) return NextResponse.json({ error: erroDoVideoNaCriacao }, { status: 400 });
  const erroDoMinimoNaCriacao = normalizarValorMinimo(patch);
  if (erroDoMinimoNaCriacao) return NextResponse.json({ error: erroDoMinimoNaCriacao }, { status: 400 });
  const priceOnRequest = Boolean(patch.price_on_request);
  const useVariationPriceOnly = Boolean(patch.use_variation_price_only);
  const normalizedPriceTiers = priceOnRequest ? [] : parsedPriceTiers;
  const tierSaleLimitsValidation = validatePriceTiersAgainstSaleLimits(
    normalizedPriceTiers,
    saleQuantityConfig
  );
  if (tierSaleLimitsValidation.error) {
    return NextResponse.json({ error: tierSaleLimitsValidation.error }, { status: 400 });
  }

  // Parse costs and price
  const estimatedBaseCost = typeof patch.estimated_base_cost === "number" ? patch.estimated_base_cost : parseMoneyInput(patch.estimated_base_cost);
  
  // Determine cost_total
  let finalCostTotal = estimatedBaseCost;
  const materials = Array.isArray(body?.materials)
    ? (body?.materials as CompositionMaterialInput[])
    : [];
  const processes = Array.isArray(body?.processes)
    ? (body?.processes as CompositionProcessInput[])
    : [];
  const attributes = sanitizeAttributesWithImages(
    Array.isArray(body?.attributes) ? (body?.attributes as ProductAttributeInput[]) : [],
    validImageIds
  );
  const variations = Array.isArray(body?.variations) ? body?.variations : [];
  const inventoryScope = normalizeInventoryScope(patch.inventory_scope);
  const trackInventory = Boolean(patch.track_inventory);
  const inventoryAttributeNameRaw =
    typeof patch.inventory_attribute_name === "string" ? patch.inventory_attribute_name.trim() : "";
  const inventoryAttributeName = inventoryScope === "option" && inventoryAttributeNameRaw ? inventoryAttributeNameRaw : null;
  const optionStockRows = extractOptionStockRows(attributes, {
    productId: "pending",
    userId: writeUserId,
    inventoryScope,
    inventoryAttributeName,
  });
  const productStock =
    typeof patch.stock === "number"
      ? patch.stock
      : parseMoneyInput(patch.stock);
  const totalAllocatedStock = sumAllocatedOptionStock(optionStockRows);
  if (trackInventory && inventoryScope === "option" && totalAllocatedStock > productStock + 0.000001) {
    return NextResponse.json(
      {
        error: `Estoque das variações (${totalAllocatedStock}) não pode ser maior que o estoque geral (${productStock}).`,
      },
      { status: 400 }
    );
  }

  const hasComposition = (Array.isArray(materials) && materials.length > 0) || (Array.isArray(processes) && processes.length > 0);

  if (hasComposition) {
      // If composition is provided, prefer the calculated cost from frontend or recalculate
      if (typeof patch.cost_total === 'number') {
          finalCostTotal = patch.cost_total;
      } else {
          const matCost = materials.reduce((acc, m) => acc + (Number(m.cost_total) || 0), 0);
          const procCost = processes.reduce((acc, p) => acc + (Number(p.cost_total) || 0), 0);
          finalCostTotal = matCost + procCost;
      }
  }

  /* Saldo inicial pelo extrato: na regra "por produto" o produto nasce com
     zero e o número digitado entra como movimento (inventory_movements), que
     o gatilho do banco soma ao saldo. Antes o saldo nascia escrito direto,
     sem movimento nenhum que o explicasse no histórico. */
  const saldoInicialPorMovimento =
    inventoryScope === "product" && Number.isFinite(productStock) && Math.abs(productStock) > 0.000001
      ? productStock
      : 0;

  let payload: Record<string, unknown> = { 
    ...patch, 
    ...(saldoInicialPorMovimento ? { stock: 0 } : {}),
    user_id: writeUserId,
    estimated_base_cost: estimatedBaseCost,
    cost_total: finalCostTotal,
    ...saleQuantityConfig,
    ...productionConfig,
    track_inventory: trackInventory,
    inventory_scope: inventoryScope,
    inventory_attribute_name: inventoryAttributeName,
  };

  const primaryImageUrl =
    normalizedImages.find((image) => image.is_primary)?.image_url ||
    normalizedImages[0]?.image_url ||
    (typeof patch.image_url === "string" ? patch.image_url : "");
  payload = {
    ...payload,
    image_url: primaryImageUrl || null,
  };

  if (!priceOnRequest && !useVariationPriceOnly) {
    const manualPrice = typeof patch.price === "number" ? patch.price : parseMoneyInput(patch.price);
    payload = { ...payload, price: manualPrice };
  } else {
    payload = { ...payload, price: 0 };
  }

  const { data, error } = await supabase.from("products").insert(payload).select("id").single();
  if (error) return NextResponse.json({ error: error.message }, { status: 400 });

  if (data?.id) {
    const productId = data.id;
    const errors: string[] = [];

    if (saldoInicialPorMovimento) {
      const { error: saldoError } = await supabase.from("inventory_movements").insert({
        product_id: productId,
        quantity: Math.abs(saldoInicialPorMovimento),
        type: saldoInicialPorMovimento > 0 ? "adjustment_in" : "adjustment_out",
        reference_type: "product_create",
        notes: "Saldo inicial do cadastro",
        user_id: user.id,
      });
      if (saldoError) errors.push(`Initial stock: ${saldoError.message}`);
    }

    if (normalizedPriceTiers.length > 0) {
      const tiersPayload = normalizedPriceTiers.map((tier) => ({
        product_id: productId,
        user_id: writeUserId,
        min_qty: tier.min_qty,
        max_qty: tier.max_qty,
        unit_price: tier.unit_price,
        sort_order: tier.sort_order ?? 0,
        active: tier.active ?? true,
      }));
      const { error: tiersError } = await supabase.from("product_price_tiers").insert(tiersPayload);
      if (tiersError) errors.push(`Price tiers: ${tiersError.message}`);
    }

    if (normalizedImages.length > 0) {
      const imagesPayload = normalizedImages.map((image) => ({
        id: image.id,
        product_id: productId,
        user_id: writeUserId,
        image_url: image.image_url,
        sort_order: image.sort_order,
        is_primary: image.is_primary,
      }));
      const { error: imagesError } = await supabase.from("product_images").insert(imagesPayload);
      if (imagesError) errors.push(`Images: ${imagesError.message}`);
    }

    if (hasComposition) {
      // Insert materials
      if (Array.isArray(materials) && materials.length > 0) {
        /* Estoque v2: as colunas de baixa da linha (quando sai, opção,
           quantidade por variação, célula) vão junto; fora do v2, nada. */
        const { data: noV2 } = await createAdminClient().rpc("estoque_v2_ligado", { p_empresa: writeUserId });
        const materialsPayload = materials.map((m) => ({
          ...linhaDaComposicaoParaGravar(m as unknown as Record<string, unknown>, noV2 === true),
          product_id: productId,
          user_id: writeUserId,
          material_id: m.material_id,
          consumption_qty: m.consumption_qty,
          waste_pct: m.waste_pct,
          cost_total: m.cost_total,
          consumption_base: baseDeConsumo(m.consumption_base),
          pieces_per_unit:
            typeof m.pieces_per_unit === "number" && m.pieces_per_unit > 0 ? m.pieces_per_unit : null,
        }));
        const { error: matError } = await supabase.from("product_materials").insert(materialsPayload);
        if (matError) errors.push(`Materials: ${matError.message}`);
      }

      // Insert processes
      if (Array.isArray(processes) && processes.length > 0) {
        const processesPayload = processes.map((p) => ({
          product_id: productId,
          user_id: writeUserId,
          kind: p.kind,
          minutes: p.minutes,
          setup_minutes: p.setup_minutes,
          cost_total: p.cost_total,
          machine_id: p.machine_id || null,
          role_id: p.role_id || null,
          process_name: p.process_name || "Processo",
          consumption_base: baseDeConsumo(p.consumption_base),
        }));
        const { error: procError } = await supabase.from("product_processes").insert(processesPayload);
        if (procError) errors.push(`Processes: ${procError.message}`);
      }
    }

    // Insert attributes
    if (attributes.length > 0) {
      const attrsPayloadBase = attributes.map((a: ProductAttributeInput) => ({
        product_id: productId,
        name: a.name,
        options: a.options,
        position: typeof a.position === "number" ? a.position : null,
      }));
      const attrsPayload = attributes.map((a: ProductAttributeInput, index: number) => ({
        ...attrsPayloadBase[index],
        allow_multiple: a.allow_multiple ?? false,
        is_required: a.is_required ?? false,
        display_style: normalizeDisplayStyle(a.display_style),
        show_in_store: a.show_in_store !== false,
      }));
      const { error: attrError } = await supabase.from("product_attributes").insert(attrsPayload);
      if (attrError) {
        if (
          attrError.message.includes("allow_multiple") ||
          attrError.message.includes("is_required") ||
          attrError.message.includes("position") ||
          attrError.message.includes("display_style") ||
          attrError.message.includes("show_in_store")
        ) {
          const fallbackPayload = attrsPayloadBase.map((item) => {
            const { position: droppedPosition, ...rest } = item;
            void droppedPosition;
            return rest;
          });
          const { error: fallbackError } = await supabase.from("product_attributes").insert(fallbackPayload);
          if (fallbackError) errors.push(`Attributes: ${fallbackError.message}`);
        } else {
          errors.push(`Attributes: ${attrError.message}`);
        }
      }
    }

    await syncProductOptionStocks(supabase, {
      productId,
      userId: writeUserId,
      inventoryScope,
      inventoryAttributeName,
      attributes,
    });
    await syncProductOptionRecipes(supabase, { productId, userId: writeUserId, attributes });
    if (vinculosDeServico.length > 0) {
      await syncProductServiceLinks(supabase, { productId, userId: writeUserId, links: vinculosDeServico });
    }

    // Insert variations
    if (variations.length > 0) {
      const varsPayload = variations.map((v: ProductVariationInput) => ({
        id: crypto.randomUUID(),
        product_id: productId,
        combination: v.combination,
        sku: v.sku,
        price: v.price,
        stock: v.stock,
        cost_adjustment: v.cost_adjustment
      }));
      const { error: varError } = await supabase.from("product_variations").insert(varsPayload);
      if (varError) errors.push(`Variations: ${varError.message}`);
      /* O saldo inicial de cada combinação também fica no extrato. */
      const saldosIniciais = varError
        ? []
        : varsPayload
            .filter((v) => Math.abs(Number(v.stock) || 0) > 0.000001)
            .map((v) => ({
              user_id: writeUserId,
              product_id: productId,
              variation_id: v.id,
              combination: v.combination,
              quantity: Math.abs(Number(v.stock) || 0),
              type: (Number(v.stock) || 0) > 0 ? "adjustment_in" : "adjustment_out",
              reference_type: "product_create",
              notes: "Saldo inicial do cadastro",
              balance_after: Number(v.stock) || 0,
            }));
      if (saldosIniciais.length > 0) {
        const { error: movError } = await supabase.from("product_variation_stock_movements").insert(saldosIniciais);
        if (movError) errors.push(`Variation stock movements: ${movError.message}`);
      }
    }

    if (errors.length > 0) {
      console.error("Errors inserting product data:", errors);
      // We don't fail the request completely, but log it.
      // Ideally we should transaction this or rollback, but Supabase HTTP API doesn't support transactions easily without RPC.
    }
  }

  avisarLoja(writeUserId, req);
  return NextResponse.json({ ok: true, id: (data as { id?: unknown } | null)?.id ?? null });
}

export async function alterarProdutoComoOPainel(entrada: EntradaDaGravacao) {
  const { supabase, user, obterContexto, req } = entrada;
  const body = (entrada.body && typeof entrada.body === "object" ? entrada.body : null) as { id?: unknown; patch?: unknown } | null;
  const id = typeof body?.id === "string" ? body.id : "";
  if (!id) return NextResponse.json({ error: "Campo 'id' e obrigatorio." }, { status: 400 });

  let patch = pickWritablePatch(body?.patch);
  // O saldo geral nunca entra pelo editor: na regra por produto ele vive do
  // extrato de movimentos, e nas regras por opção/combinação é a soma das
  // partes, calculada pelo banco. Regravar aqui desfazia vendas.
  delete patch.stock;
  if (Object.prototype.hasOwnProperty.call(patch, "description")) {
    patch.description = sanitizeProductDescription(patch.description);
  }
  if (Object.prototype.hasOwnProperty.call(patch, "custom_fields_config")) {
    patch.custom_fields_config = normalizeProductCustomFieldsConfig(patch.custom_fields_config);
  }

  const rawPatch = body?.patch as Record<string, unknown> | null;
  const hasImages = Boolean(rawPatch) && Object.prototype.hasOwnProperty.call(rawPatch, "images");
  const { images: normalizedImages, error: imageError } = normalizeProductImagesInput(
    hasImages ? rawPatch?.images : undefined
  );
  if (imageError) {
    return NextResponse.json({ error: imageError }, { status: 400 });
  }

  const validImageIds = new Set(normalizedImages.map((image) => image.id));
  const attributes = Array.isArray(rawPatch?.attributes)
    ? hasImages
      ? sanitizeAttributesWithImages(rawPatch.attributes as ProductAttributeInput[], validImageIds)
      : (rawPatch.attributes as ProductAttributeInput[])
    : null;
  const variations = Array.isArray(rawPatch?.variations) ? rawPatch?.variations : null;

  /* Quais serviços da biblioteca este produto oferece. Lista ausente = a tela
     não mexeu no assunto; lista vazia = o produto não oferece nenhum. */
  const temVinculosDeServico =
    Boolean(rawPatch) && Object.prototype.hasOwnProperty.call(rawPatch, "optional_service_links");
  const vinculosDeServico = lerVinculosDeServico(rawPatch?.optional_service_links);

  const hasPriceTiers = Boolean(rawPatch) && Object.prototype.hasOwnProperty.call(rawPatch, "price_tiers");
  const hasSaleQuantityPatch =
    Boolean(rawPatch) && saleQuantityFields.some((field) => Object.prototype.hasOwnProperty.call(rawPatch, field));

  const { tiers: parsedPriceTiers, error: priceTierError } = validateAndNormalizePriceTiers(
    hasPriceTiers ? rawPatch?.price_tiers : undefined
  );
  if (priceTierError) {
    return NextResponse.json({ error: priceTierError }, { status: 400 });
  }

  if (!Object.keys(patch).length && !attributes && !variations && !hasPriceTiers && !hasImages) {
    return NextResponse.json({ error: "Nada para atualizar." }, { status: 400 });
  }

  const ctx = await obterContexto();
  /* Tres trabalhos diferentes na mesma rota, e cada um pergunta pela sua
     tela: publicar na loja e da loja, mexer so no saldo e do estoque, e o
     resto e da ficha do produto. */
  const permission = requiresCatalogWrite(patch)
    ? "catalog.products.write"
    : somenteEstoque(patch, { attributes, variations, hasPriceTiers, hasImages })
      ? "products.stock.write"
      : "products.catalog.write";
  if (!canCompany(ctx, user.id, permission)) {
    return NextResponse.json({ error: "Sem permissao." }, { status: 403 });
  }

  const tenantIds = ctx.tenantIds?.length ? ctx.tenantIds : [user.id];
  const { data: existing, error: existingError } = await supabase
    .from("products")
    .select(
      "id, user_id, product_kind, price, price_on_request, use_variation_price_only, estimated_base_cost, cost_total, stock, unit_type, pricing_reference_width_m, pricing_reference_height_m, min_sale_qty_enabled, min_sale_qty, max_sale_qty_enabled, max_sale_qty, allowed_sale_qty_enabled, allowed_sale_quantities, inventory_scope, inventory_attribute_name, track_inventory, product_processes(cost_total), product_materials(cost_total, quando_sai)"
    )
    .eq("id", id)
    .in("user_id", tenantIds)
    .maybeSingle();

  if (existingError) return NextResponse.json({ error: existingError.message }, { status: 400 });
  if (!existing) return NextResponse.json({ error: "Produto nao encontrado." }, { status: 404 });
  if ((existing as { product_kind?: unknown }).product_kind === "digital") {
    return NextResponse.json({ error: "Este é um produto digital: edite em Produtos digitais." }, { status: 409 });
  }

  const erroFiscal = await normalizarCamposFiscais(patch, createAdminClient(), String((existing as { user_id: string }).user_id));
  if (erroFiscal) return NextResponse.json({ error: erroFiscal }, { status: 400 });
  const erroDoVideo = await normalizarVideoDoProduto(patch, createAdminClient(), String((existing as { user_id: string }).user_id), String(id));
  if (erroDoVideo) return NextResponse.json({ error: erroDoVideo }, { status: 400 });
  const erroDoMinimo = normalizarValorMinimo(patch);
  if (erroDoMinimo) return NextResponse.json({ error: erroDoMinimo }, { status: 400 });

  // Recalculate cost_total if estimated_base_cost changed.
  let costUpdate: Record<string, unknown> = {};
  if (Object.prototype.hasOwnProperty.call(patch, "estimated_base_cost")) {
    const newEstimated =
      typeof patch.estimated_base_cost === "number"
        ? patch.estimated_base_cost
        : parseMoneyInput(patch.estimated_base_cost);

    const processesCost = ((existing as { product_processes?: CompositionCostRow[] | null } | null)?.product_processes || []).reduce(
      (acc, process) => acc + (Number(process.cost_total) || 0),
      0
    );
    /* Estoque v2: linha "só se a opção X" fica fora do custo base (só vale
       quando a opção é escolhida). Fora do v2 nenhuma linha é `se_opcao`. */
    const materialsCost = ((existing as { product_materials?: Array<CompositionCostRow & { quando_sai?: string | null }> | null } | null)?.product_materials || []).reduce(
      (acc, material) => (material.quando_sai === "se_opcao" ? acc : acc + (Number(material.cost_total) || 0)),
      0
    );

    /* A conta ciente de unidade: as linhas da ficha são por peça de
       referência, e no produto medido a unidade de venda é o m². A soma cega
       dobrava o custo do banner — e o número contaminado ia para as vendas. */
    const ex = existing as {
      unit_type?: string | null;
      pricing_reference_width_m?: number | null;
      pricing_reference_height_m?: number | null;
    };
    const newTotal = custoDoProduto({
      estimatedBaseCost: newEstimated,
      somaDaFicha: processesCost + materialsCost,
      unitType: ex.unit_type,
      referenceWidthM: ex.pricing_reference_width_m,
      referenceHeightM: ex.pricing_reference_height_m,
    });
    costUpdate = {
      estimated_base_cost: newEstimated,
      cost_total: newTotal,
    };
  }

  const hasPrice = Object.prototype.hasOwnProperty.call(patch, "price");
  const hasPriceOnRequest = Object.prototype.hasOwnProperty.call(patch, "price_on_request");
  const hasUseVariationPriceOnly = Object.prototype.hasOwnProperty.call(patch, "use_variation_price_only");
  const priceOnRequest = hasPriceOnRequest
    ? Boolean(patch.price_on_request)
    : Boolean((existing as { price_on_request?: unknown } | null)?.price_on_request);
  const useVariationPriceOnly = hasUseVariationPriceOnly
    ? Boolean(patch.use_variation_price_only)
    : Boolean((existing as { use_variation_price_only?: unknown } | null)?.use_variation_price_only);
  const normalizedPriceTiers = priceOnRequest ? [] : parsedPriceTiers;

  let saleQuantityConfigForValidation = {
    min_sale_qty_enabled: Boolean((existing as { min_sale_qty_enabled?: unknown } | null)?.min_sale_qty_enabled),
    min_sale_qty: (() => {
      const raw = (existing as { min_sale_qty?: unknown } | null)?.min_sale_qty;
      if (raw === null || raw === undefined || raw === "") return null;
      const parsed = Number(raw);
      return Number.isFinite(parsed) ? parsed : null;
    })(),
    max_sale_qty_enabled: Boolean((existing as { max_sale_qty_enabled?: unknown } | null)?.max_sale_qty_enabled),
    max_sale_qty: (() => {
      const raw = (existing as { max_sale_qty?: unknown } | null)?.max_sale_qty;
      if (raw === null || raw === undefined || raw === "") return null;
      const parsed = Number(raw);
      return Number.isFinite(parsed) ? parsed : null;
    })(),
    allowed_sale_qty_enabled: Boolean(
      (existing as { allowed_sale_qty_enabled?: unknown } | null)?.allowed_sale_qty_enabled
    ),
    allowed_sale_quantities: (() => {
      const raw = (existing as { allowed_sale_quantities?: unknown } | null)?.allowed_sale_quantities;
      if (!Array.isArray(raw)) return null;
      const values = raw
        .map((value) => Number(value))
        .filter((value) => Number.isFinite(value) && value > 0)
        .map((value) => Number(value.toFixed(3)));
      return values.length > 0 ? Array.from(new Set(values)).sort((a, b) => a - b) : null;
    })(),
  };
  const productionConfigFallback = {
    production_time_value: (() => {
      const raw = (existing as { production_time_value?: unknown } | null)?.production_time_value;
      if (raw === null || raw === undefined || raw === "") return null;
      const parsed = Number(raw);
      return Number.isFinite(parsed) ? Number(parsed.toFixed(3)) : null;
    })(),
    production_time_unit:
      normalizeProductionTimeUnit(
        (existing as { production_time_unit?: unknown } | null)?.production_time_unit
      ) || null,
    production_template_url:
      typeof (existing as { production_template_url?: unknown } | null)?.production_template_url === "string"
        ? String((existing as { production_template_url?: unknown }).production_template_url).trim()
        : null,
    production_template_name:
      typeof (existing as { production_template_name?: unknown } | null)?.production_template_name === "string"
        ? String((existing as { production_template_name?: unknown }).production_template_name).trim()
        : null,
    production_template_format:
      normalizeProductionTemplateLegacyFormat(
        (existing as { production_template_format?: unknown } | null)?.production_template_format
      ) || null,
    production_templates: Array.isArray((existing as { production_templates?: unknown } | null)?.production_templates)
      ? (existing as { production_templates?: unknown }).production_templates
      : [],
  };

  const inventoryScope = normalizeInventoryScope(
    Object.prototype.hasOwnProperty.call(patch, "inventory_scope")
      ? patch.inventory_scope
      : (existing as { inventory_scope?: unknown } | null)?.inventory_scope
  );
  const trackInventory = Object.prototype.hasOwnProperty.call(patch, "track_inventory")
    ? Boolean(patch.track_inventory)
    : Boolean((existing as { track_inventory?: unknown } | null)?.track_inventory);
  const inventoryAttributeNameRaw = Object.prototype.hasOwnProperty.call(patch, "inventory_attribute_name")
    ? (typeof patch.inventory_attribute_name === "string" ? patch.inventory_attribute_name.trim() : "")
    : (typeof (existing as { inventory_attribute_name?: unknown } | null)?.inventory_attribute_name === "string"
      ? String((existing as { inventory_attribute_name?: unknown }).inventory_attribute_name).trim()
      : "");
  const inventoryAttributeName = inventoryScope === "option" && inventoryAttributeNameRaw ? inventoryAttributeNameRaw : null;
  const stockValueForValidation = Object.prototype.hasOwnProperty.call(patch, "stock")
    ? (typeof patch.stock === "number" ? patch.stock : parseMoneyInput(patch.stock))
    : Number((existing as { stock?: unknown } | null)?.stock || 0);
  const attributesForValidation = attributes ?? [];
  const optionStockRowsForValidation = extractOptionStockRows(attributesForValidation, {
    productId: id,
    userId:
      typeof (existing as { user_id?: unknown } | null)?.user_id === "string"
        ? String((existing as { user_id?: unknown }).user_id)
        : (ctx.ownerId ?? ctx.companyId),
    inventoryScope,
    inventoryAttributeName,
  });
  // A antiga régua "soma das opções <= estoque geral" morreu: o geral agora É
  // a soma, mantida pelo banco. Não há mais o que validar aqui.
  void stockValueForValidation;
  void optionStockRowsForValidation;

  // Regra "por opção" sem dizer qual atributo controla desativa o controle em
  // silêncio — melhor recusar com o caminho claro.
  if (trackInventory && inventoryScope === "option" && !inventoryAttributeName) {
    return NextResponse.json(
      { error: 'Com estoque "por opção", escolha qual atributo controla o estoque (ex.: Cor) no card Estoque.' },
      { status: 400 }
    );
  }

  // Estoque por combinação exige uma escolha por atributo: com duas opções
  // marcadas, a baixa não sabe qual combinação usar.
  if (trackInventory && inventoryScope === "variant" && attributes) {
    const multiplos = attributesForValidation
      .filter((attribute) => Boolean((attribute as { allow_multiple?: boolean | null }).allow_multiple))
      .map((attribute) => (typeof attribute.name === "string" && attribute.name ? attribute.name : "atributo"));
    if (multiplos.length > 0) {
      return NextResponse.json(
        {
          error: `Com estoque por combinação, cada atributo aceita uma escolha só. Desligue "Múltipla escolha" em: ${multiplos.join(", ")}.`,
        },
        { status: 400 }
      );
    }
  }

  if (hasSaleQuantityPatch) {
    const saleQtyPatchInput: Record<string, unknown> = {};
    for (const field of saleQuantityFields) {
      if (Object.prototype.hasOwnProperty.call(patch, field)) {
        saleQtyPatchInput[field] = patch[field];
      }
    }

    const { config, error: saleQuantityError } = buildSaleQuantityConfig(
      saleQtyPatchInput,
      saleQuantityConfigForValidation as unknown as Record<string, unknown>
    );
    if (saleQuantityError || !config) {
      return NextResponse.json(
        { error: saleQuantityError || "Erro ao validar quantidade minima/maxima." },
        { status: 400 }
      );
    }

    saleQuantityConfigForValidation = config;
    patch = { ...patch, ...config };
  }

  const hasProductionPatch =
    Boolean(rawPatch) &&
    ["production_time_value", "production_time_unit", "production_template_url", "production_template_name", "production_template_format", "production_templates"].some(
      (field) => Object.prototype.hasOwnProperty.call(rawPatch, field)
    );

  if (hasProductionPatch) {
    const productionPatchInput: Record<string, unknown> = {};
    if (rawPatch) {
      for (const field of [
        "production_time_value",
        "production_time_unit",
        "production_template_url",
        "production_template_name",
        "production_template_format",
        "production_templates",
      ]) {
        if (Object.prototype.hasOwnProperty.call(rawPatch, field)) {
          productionPatchInput[field] = rawPatch[field];
        }
      }
    }

    const { config, error: productionConfigError } = buildProductionConfig(
      productionPatchInput,
      productionConfigFallback
    );
    if (productionConfigError || !config) {
      return NextResponse.json(
        { error: productionConfigError || "Erro ao validar dados de producao." },
        { status: 400 }
      );
    }

    patch = { ...patch, ...config };
  }

  if (!priceOnRequest && (hasPriceTiers || hasSaleQuantityPatch)) {
    let tiersForValidation: ProductPriceTier[] = normalizedPriceTiers;

    if (!hasPriceTiers) {
      const { data: existingTiers, error: existingTiersError } = await supabase
        .from("product_price_tiers")
        .select("min_qty, max_qty, unit_price, sort_order, active")
        .eq("product_id", id)
        .order("sort_order", { ascending: true });

      if (existingTiersError) {
        const errorMsg = String(existingTiersError.message || "").toLowerCase();
        const isMissingRelation =
          (errorMsg.includes("product_price_tiers") && errorMsg.includes("does not exist")) ||
          errorMsg.includes("could not find the relation");
        if (!isMissingRelation) {
          return NextResponse.json({ error: existingTiersError.message }, { status: 400 });
        }
        tiersForValidation = [];
      } else {
        tiersForValidation =
          (existingTiers || []).map((tier, index) => ({
            min_qty: Number(tier.min_qty) || 0,
            max_qty: tier.max_qty === null || tier.max_qty === undefined ? null : Number(tier.max_qty) || 0,
            unit_price: Number(tier.unit_price) || 0,
            sort_order: typeof tier.sort_order === "number" ? tier.sort_order : index,
            active: tier.active ?? true,
          })) || [];
      }
    }

    const tierSaleLimitsValidation = validatePriceTiersAgainstSaleLimits(
      tiersForValidation,
      saleQuantityConfigForValidation
    );
    if (tierSaleLimitsValidation.error) {
      return NextResponse.json({ error: tierSaleLimitsValidation.error }, { status: 400 });
    }
  }

  let patchWithPricing: Record<string, unknown> = { ...patch, ...costUpdate };
  patchWithPricing = {
    ...patchWithPricing,
    track_inventory: trackInventory,
    inventory_scope: inventoryScope,
    inventory_attribute_name: inventoryAttributeName,
  };
  if (hasPriceOnRequest || hasPrice || hasUseVariationPriceOnly) {
    if (priceOnRequest || useVariationPriceOnly) {
      patchWithPricing = { ...patchWithPricing, price: 0 };
    } else if (hasPrice) {
      const manualPrice = typeof patch.price === "number" ? patch.price : parseMoneyInput(patch.price);
      patchWithPricing = { ...patchWithPricing, price: manualPrice };
    }
  }

  if (hasImages) {
    const primaryImageUrl =
      normalizedImages.find((image) => image.is_primary)?.image_url ||
      normalizedImages[0]?.image_url ||
      null;
    patchWithPricing = { ...patchWithPricing, image_url: primaryImageUrl };
  }

  const { data, error } = await supabase
    .from("products")
    .update(patchWithPricing)
    .eq("id", id)
    .in("user_id", tenantIds)
    .select("id")
    .maybeSingle();

  if (error) return NextResponse.json({ error: error.message }, { status: 400 });
  if (!data) return NextResponse.json({ error: "Produto nao encontrado." }, { status: 404 });

  if (hasImages) {
    const { error: deleteImagesError } = await supabase.from("product_images").delete().eq("product_id", id);
    if (deleteImagesError) return NextResponse.json({ error: deleteImagesError.message }, { status: 400 });

    if (normalizedImages.length > 0) {
      const writeUserId = (existing as { user_id?: unknown } | null)?.user_id;
      const imagesPayload = normalizedImages.map((image) => ({
        id: image.id,
        product_id: id,
        user_id: typeof writeUserId === "string" ? writeUserId : (ctx.ownerId ?? ctx.companyId),
        image_url: image.image_url,
        sort_order: image.sort_order,
        is_primary: image.is_primary,
      }));
      const { error: insertImagesError } = await supabase.from("product_images").insert(imagesPayload);
      if (insertImagesError) return NextResponse.json({ error: insertImagesError.message }, { status: 400 });
    }
  }

  // Update attributes and variations.
  if (attributes && variations) {
    await supabase.from("product_attributes").delete().eq("product_id", id);

    if (attributes.length > 0) {
      const attrsPayloadBase = attributes.map((attribute: ProductAttributeInput) => ({
        product_id: id,
        name: attribute.name,
        options: attribute.options,
        position: typeof attribute.position === "number" ? attribute.position : null,
      }));
      const attrsPayload = attributes.map((attribute: ProductAttributeInput, index: number) => ({
        ...attrsPayloadBase[index],
        allow_multiple: attribute.allow_multiple ?? false,
        is_required: attribute.is_required ?? false,
        display_style: normalizeDisplayStyle(attribute.display_style),
        show_in_store: attribute.show_in_store !== false,
      }));
      const { error: attrError } = await supabase.from("product_attributes").insert(attrsPayload);
      if (
        attrError &&
        (attrError.message.includes("allow_multiple") ||
          attrError.message.includes("is_required") ||
          attrError.message.includes("position") ||
          attrError.message.includes("display_style") ||
          attrError.message.includes("show_in_store"))
      ) {
        const fallbackPayload = attrsPayloadBase.map((item) => {
          const { position: droppedPosition, ...rest } = item;
          void droppedPosition;
          return rest;
        });
        await supabase.from("product_attributes").insert(fallbackPayload);
      }
    }

    const writeUserId =
      typeof (existing as { user_id?: unknown } | null)?.user_id === "string"
        ? String((existing as { user_id?: unknown }).user_id)
        : (ctx.ownerId ?? ctx.companyId);
    await syncProductVariations(supabase, {
      productId: id,
      userId: writeUserId,
      variations,
    });
    await syncProductOptionStocks(supabase, {
      productId: id,
      userId: writeUserId,
      inventoryScope,
      inventoryAttributeName,
      attributes,
    });
    await syncProductOptionRecipes(supabase, { productId: id, userId: writeUserId, attributes });
  }

  /* Os serviços oferecidos chegam sempre que a tela do produto grava, mesmo
     numa gravação que não mexeu nas variações. */
  if (temVinculosDeServico) {
    const donoDoProduto =
      typeof (existing as { user_id?: unknown } | null)?.user_id === "string"
        ? String((existing as { user_id?: unknown }).user_id)
        : (ctx.ownerId ?? ctx.companyId);
    await syncProductServiceLinks(supabase, {
      productId: id,
      userId: donoDoProduto,
      links: vinculosDeServico,
    });
  }

  if (hasPriceTiers) {
    const { error: deleteTierError } = await supabase.from("product_price_tiers").delete().eq("product_id", id);
    if (deleteTierError) return NextResponse.json({ error: deleteTierError.message }, { status: 400 });

    if (normalizedPriceTiers.length > 0) {
      const writeUserId = (existing as { user_id?: unknown } | null)?.user_id;
      const tiersPayload = normalizedPriceTiers.map((tier) => ({
        product_id: id,
        user_id: typeof writeUserId === "string" ? writeUserId : (ctx.ownerId ?? ctx.companyId),
        min_qty: tier.min_qty,
        max_qty: tier.max_qty,
        unit_price: tier.unit_price,
        sort_order: tier.sort_order ?? 0,
        active: tier.active ?? true,
      }));
      const { error: insertTierError } = await supabase.from("product_price_tiers").insert(tiersPayload);
      if (insertTierError) return NextResponse.json({ error: insertTierError.message }, { status: 400 });
    }
  }

  avisarLoja(ctx.ownerId ?? ctx.companyId ?? user.id, req);
  return NextResponse.json({ ok: true });
}

export async function excluirProdutoComoOPainel(entrada: EntradaDaGravacao) {
  const { supabase, user, obterContexto, req } = entrada;
  const body = (entrada.body && typeof entrada.body === "object" ? entrada.body : null) as { id?: unknown } | null;
  const id = typeof body?.id === "string" ? body.id : "";
  if (!id) return NextResponse.json({ error: "Campo 'id' é obrigatório." }, { status: 400 });

  const ctx = await obterContexto();
  if (!canCompany(ctx, user.id, "products.catalog.write")) {
    return NextResponse.json({ error: "Sem permissão." }, { status: 403 });
  }

  const tenantIds = ctx.tenantIds?.length ? ctx.tenantIds : [user.id];
  const { data, error } = await supabase
    .from("products")
    .delete()
    .eq("id", id)
    .in("user_id", tenantIds)
    /* Produto digital se apaga só pela área Produtos digitais. */
    .neq("product_kind", "digital")
    .select("id")
    .maybeSingle();

  if (error) return NextResponse.json({ error: error.message }, { status: 400 });
  if (!data) return NextResponse.json({ error: "Produto não encontrado." }, { status: 404 });

  avisarLoja(ctx.ownerId ?? ctx.companyId ?? user.id, req);
  return NextResponse.json({ ok: true });
}
