import type { SupabaseClient } from "@supabase/supabase-js";

import { mensagemDaTrava, notasQueTravamOPedido } from "@/lib/fiscal/servidor/travas";

/**
 * "Atualizar pelo orçamento": o pedido passa a ter os itens e o valor do
 * orçamento de onde nasceu.
 *
 * Mora aqui (e não só na rota) porque dois caminhos fazem a mesma coisa: o
 * botão da tela do pedido (/api/orders/[id]/sync-from-quote) e o assistente
 * (alterar_pedido). O código é o que estava na rota, sem mudança de regra.
 */

export type SyncMode = "preview" | "apply";

type ItemComparable = {
  product_id: string | null;
  title: string;
  description: string | null;
  quantity: number;
  unit_price: number;
  total_price: number;
  unit_type: string;
  width_m: number | null;
  height_m: number | null;
  length_m: number | null;
  /* Fora da comparacao de proposito: `isSameItem` lista os campos que o usuario
     ve no diff, e uma mudanca de custo nao e uma mudanca do item para ele. O
     custo viaja aqui so para virar fotografia no pedido. */
  unit_cost?: number | null;
  /* Também fora da comparação: o valor mínimo do item só explica o total, que
     já é comparado. Viaja para o pedido mostrar o aviso. */
  valor_minimo_aplicado?: number | null;
  valor_sem_minimo?: number | null;
  /* As escolhas do cliente (variação, opções, serviços): o contrato
     selected_options que o estoque, a ficha de produção e a nota leem.
     Entra na comparação — trocar a cor no orçamento é mudar o item. */
  selected_options?: Record<string, unknown> | null;
};

export type SyncChange = {
  index: number;
  type: "added" | "removed" | "changed";
  before: ItemComparable | null;
  after: ItemComparable | null;
};

function normalizeText(value: unknown) {
  return String(value || "").trim();
}

function normalizeNullableText(value: unknown) {
  const normalized = normalizeText(value);
  return normalized.length > 0 ? normalized : null;
}

function normalizeNumber(value: unknown) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) return 0;
  return parsed;
}

/**
 * Dinheiro em centavos e quantidade em milesimos, antes de comparar.
 *
 * Sem isto, um item identico aparecia como "alterado": o orcamento tinha
 * gravado 506.99999999999994 e o pedido 507, os dois vindos de 30 x 16,90 em
 * JavaScript. A tela mostrava as duas linhas iguais lado a lado e dizia que
 * mudou, e toda sincronizacao parecia mexer em tudo.
 *
 * Arredondar aqui tambem limpa o valor que vai ser gravado no pedido.
 */
function centavos(value: number) {
  return Math.round((Number.isFinite(value) ? value : 0) * 100) / 100;
}

function milesimos(value: number) {
  return Math.round((Number.isFinite(value) ? value : 0) * 1000) / 1000;
}

function normalizeNullableNumber(value: unknown) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) return null;
  return parsed;
}

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

/* Para comparar: sem nenhuma escolha = nada (um caminho grava {options: []},
   outro grava null — é o mesmo item). */
function escolhasParaComparar(value: Record<string, unknown> | null | undefined) {
  if (!value) return null;
  const options = Array.isArray(value.options) ? value.options : [];
  return options.length ? value : null;
}

/* JSON com as chaves em ordem: o mesmo objeto, gravado por caminhos
   diferentes, compara igual. */
function chaveEstavel(value: unknown): string {
  if (value === null || value === undefined) return "null";
  if (Array.isArray(value)) return `[${value.map(chaveEstavel).join(",")}]`;
  if (typeof value === "object") {
    const record = value as Record<string, unknown>;
    return `{${Object.keys(record)
      .sort()
      .filter((key) => record[key] !== undefined)
      .map((key) => `${JSON.stringify(key)}:${chaveEstavel(record[key])}`)
      .join(",")}}`;
  }
  return JSON.stringify(value);
}

function toComparableFromQuoteRow(row: Record<string, unknown>): ItemComparable {
  const quantity = milesimos(Math.max(0, normalizeNumber(row.quantity)));
  const unitPrice = Math.max(0, normalizeNumber(row.unit_price));
  const lineTotal = normalizeNumber(row.line_total);
  return {
    product_id: normalizeNullableText(row.product_id),
    title: normalizeText(row.title) || "Item",
    description: normalizeNullableText(row.description),
    quantity,
    unit_price: unitPrice,
    total_price: centavos(lineTotal > 0 ? lineTotal : quantity * unitPrice),
    unit_type: normalizeText(row.unit_type) || "unit",
    width_m: normalizeNullableNumber(row.width_m),
    height_m: normalizeNullableNumber(row.height_m),
    length_m: normalizeNullableNumber(row.length_m),
    unit_cost: normalizeNullableNumber(row.unit_cost),
    valor_minimo_aplicado: normalizeNullableNumber(row.valor_minimo_aplicado),
    valor_sem_minimo: normalizeNullableNumber(row.valor_sem_minimo),
    selected_options: selectedOptionsAsRecord(row.selected_options),
  };
}

function toComparableFromOrderRow(row: Record<string, unknown>): ItemComparable {
  return {
    product_id: normalizeNullableText(row.product_id),
    title: normalizeText(row.title) || "Item",
    description: normalizeNullableText(row.description),
    quantity: milesimos(Math.max(0, normalizeNumber(row.quantity))),
    unit_price: Math.max(0, normalizeNumber(row.unit_price)),
    total_price: centavos(Math.max(0, normalizeNumber(row.total_price))),
    unit_type: normalizeText(row.unit_type) || "unit",
    width_m: normalizeNullableNumber(row.width_m),
    height_m: normalizeNullableNumber(row.height_m),
    length_m: normalizeNullableNumber(row.length_m),
    selected_options: selectedOptionsAsRecord(row.selected_options),
  };
}

function isSameItem(a: ItemComparable, b: ItemComparable) {
  return (
    a.product_id === b.product_id &&
    a.title === b.title &&
    a.description === b.description &&
    a.quantity === b.quantity &&
    a.unit_price === b.unit_price &&
    a.total_price === b.total_price &&
    a.unit_type === b.unit_type &&
    a.width_m === b.width_m &&
    a.height_m === b.height_m &&
    a.length_m === b.length_m &&
    chaveEstavel(escolhasParaComparar(a.selected_options)) === chaveEstavel(escolhasParaComparar(b.selected_options))
  );
}

/**
 * Os itens do pedido na ordem dos itens do orçamento. O pedido não guarda a
 * posição do item (nascem todos no mesmo insert, com o mesmo created_at), e
 * a comparação é por índice. Cada item do orçamento, na ordem, pega o item do
 * pedido igual a ele; depois o de mesmo produto e mesma escolha; depois o de
 * mesmo produto; o que sobrar ocupa os lugares vazios na ordem em que veio, e
 * o resto fica no fim (sai do pedido). Posição sem par fica `null` ("entrou").
 */
export function alinharItensDoPedido(
  quoteRows: Record<string, unknown>[],
  orderRows: Record<string, unknown>[]
): Array<Record<string, unknown> | null> {
  const livres = orderRows.map((row, indice) => ({ row, indice }));
  const alinhados: Array<Record<string, unknown> | null> = quoteRows.map(() => null);
  const escolha = (row: Record<string, unknown>) => chaveEstavel(escolhasParaComparar(selectedOptionsAsRecord(row.selected_options)));
  const criterios: Array<(q: Record<string, unknown>, o: Record<string, unknown>) => boolean> = [
    (q, o) => isSameItem(toComparableFromQuoteRow(q), toComparableFromOrderRow(o)),
    (q, o) => normalizeNullableText(q.product_id) === normalizeNullableText(o.product_id) && escolha(q) === escolha(o),
    (q, o) => normalizeNullableText(q.product_id) !== null && normalizeNullableText(q.product_id) === normalizeNullableText(o.product_id),
  ];
  for (const criterio of criterios) {
    quoteRows.forEach((quoteRow, posicao) => {
      if (alinhados[posicao]) return;
      const achado = livres.findIndex((livre) => criterio(quoteRow, livre.row));
      if (achado < 0) return;
      alinhados[posicao] = livres[achado].row;
      livres.splice(achado, 1);
    });
  }
  for (let posicao = 0; posicao < alinhados.length && livres.length; posicao += 1) {
    if (!alinhados[posicao]) alinhados[posicao] = livres.shift()!.row;
  }
  return [...alinhados, ...livres.map((livre) => livre.row)];
}

function summarizeChanges(changes: SyncChange[]) {
  let added = 0;
  let removed = 0;
  let changed = 0;
  for (const change of changes) {
    if (change.type === "added") added += 1;
    if (change.type === "removed") removed += 1;
    if (change.type === "changed") changed += 1;
  }
  return { added, removed, changed, totalChanges: changes.length };
}

function toMoney(value: number) {
  return Number.isFinite(value) ? Math.max(0, value) : 0;
}

/** A resposta da rota: o status HTTP e o corpo JSON, iguais aos de antes. */
export async function atualizarPeloOrcamento(params: {
  supabase: SupabaseClient;
  userId: string;
  tenantIds: string[];
  orderId: string;
  mode: SyncMode;
}): Promise<{ status: number; corpo: Record<string, unknown> }> {
  const { supabase, userId, tenantIds, orderId: id, mode } = params;

  const { data: orderRow, error: orderError } = await supabase
    .from("orders")
    .select("id,user_id,quote_id,number,status,total_amount")
    .eq("id", id)
    .in("user_id", tenantIds)
    .maybeSingle();
  if (orderError) return { status: 400, corpo: { error: orderError.message } };
  if (!orderRow) return { status: 404, corpo: { error: "Pedido não encontrado." } };

  const order = orderRow as {
    id: string;
    user_id: string;
    quote_id: string;
    number: number | null;
    status: string;
    total_amount: number | null;
  };

  /* Nota fiscal valendo congela itens e valores do pedido. */
  const notasQueTravam = await notasQueTravamOPedido(order.id);
  if (notasQueTravam.length) {
    return { status: 409, corpo: { error: mensagemDaTrava(notasQueTravam, "mudar") } };
  }

  const { data: quoteHeaderRow, error: quoteHeaderError } = await supabase
    .from("quotes")
    .select("id,subtotal,discount,shipping,total")
    .eq("id", order.quote_id)
    .in("user_id", tenantIds)
    .maybeSingle();
  if (quoteHeaderError) return { status: 400, corpo: { error: quoteHeaderError.message } };
  if (!quoteHeaderRow) {
    return { status: 404, corpo: { error: "Orçamento vinculado não encontrado." } };
  }

  const quoteHeader = quoteHeaderRow as {
    subtotal: number | null;
    discount: number | null;
    shipping: number | null;
    total: number | null;
  };

  /**
   * Onde a atualizacao pelo orcamento e permitida.
   *
   * So valia em "Aguardando Producao", e a tela oferecia o botao em qualquer
   * situacao. Quem estava com o pedido em producao, que e exatamente quando o
   * cliente muda a quantidade, clicava e recebia uma recusa numa caixa do
   * navegador que o Chrome frequentemente nem mostra.
   *
   * Em producao passa a valer: os gatilhos de estoque devolvem o que sai e
   * baixam o que entra, entao o saldo continua certo. O que se perde e o
   * vinculo dos apontamentos de producao com o item antigo, e a tela avisa
   * isso antes de aplicar. Finalizado e cancelado continuam de fora: ali seria
   * reescrever pedido entregue.
   */
  const statusQuePermiteAtualizar = order.status === "pending_production" || order.status === "in_production";
  if (!statusQuePermiteAtualizar) {
    return {
      status: 409,
      corpo: {
        error:
          order.status === "canceled"
            ? "Pedido cancelado não é atualizado pelo orçamento."
            : "Pedido já finalizado não é atualizado pelo orçamento. Se precisar corrigir, volte ele para a produção antes.",
      },
    };
  }

  const { data: quoteItemsData, error: quoteItemsError } = await supabase
    .from("quote_items")
    .select(
      "id,position,product_id,title,description,quantity,unit_price,unit_cost,line_total,unit_type,width_m,height_m,length_m,valor_minimo_aplicado,valor_sem_minimo,selected_options"
    )
    .eq("quote_id", order.quote_id)
    .order("position", { ascending: true });
  if (quoteItemsError) return { status: 400, corpo: { error: quoteItemsError.message } };

  const { data: orderItemsData, error: orderItemsError } = await supabase
    .from("order_items")
    .select("id,product_id,title,description,quantity,unit_price,total_price,unit_type,width_m,height_m,length_m,valor_minimo_aplicado,valor_sem_minimo,selected_options,production_plan")
    .eq("order_id", order.id)
    .order("created_at", { ascending: true });
  if (orderItemsError) return { status: 400, corpo: { error: orderItemsError.message } };

  const quoteRows = (quoteItemsData as Record<string, unknown>[] | null) || [];
  /* order_items não tem posição: os itens nascem no mesmo insert (mesmo
     created_at) e voltavam em qualquer ordem. Comparar pelo índice casava o
     item 1 do orçamento com o item 2 do pedido: os dois "mudavam", o estoque
     devolvia e baixava tudo e o plano de produção se perdia. Agora cada item
     do orçamento acha o seu no pedido (mesmo produto; de preferência a mesma
     escolha e quantidade) e só o que sobra vai pela ordem. */
  const oldRows = alinharItensDoPedido(quoteRows, (orderItemsData as Record<string, unknown>[] | null) || []);
  const quoteItems = quoteRows.map(toComparableFromQuoteRow);
  const currentOrderItems = oldRows.map((row) => (row ? toComparableFromOrderRow(row) : null));

  const maxLen = Math.max(quoteItems.length, currentOrderItems.length);
  const changes: SyncChange[] = [];
  for (let index = 0; index < maxLen; index += 1) {
    const before = currentOrderItems[index] || null;
    const after = quoteItems[index] || null;
    if (!before && after) {
      changes.push({ index, type: "added", before: null, after });
      continue;
    }
    if (before && !after) {
      changes.push({ index, type: "removed", before, after: null });
      continue;
    }
    if (before && after && !isSameItem(before, after)) {
      changes.push({ index, type: "changed", before, after });
    }
  }

  const totalBefore = toMoney(Number(order.total_amount ?? currentOrderItems.reduce((sum, item) => sum + (item ? toMoney(item.total_price) : 0), 0)));
  const fallbackByItems = quoteItems.reduce((sum, item) => sum + toMoney(item.total_price), 0);
  const hasHeaderTotals =
    quoteHeader.total !== null ||
    quoteHeader.subtotal !== null ||
    quoteHeader.discount !== null ||
    quoteHeader.shipping !== null;
  const totalAfter = toMoney(
    Number(
      quoteHeader.total ??
        (hasHeaderTotals
          ? toMoney(Number(quoteHeader.subtotal ?? 0)) -
            toMoney(Number(quoteHeader.discount ?? 0)) +
            toMoney(Number(quoteHeader.shipping ?? 0))
          : fallbackByItems)
    )
  );
  const quoteSubtotal = toMoney(Number(quoteHeader.subtotal ?? fallbackByItems));
  const quoteDiscount = toMoney(Number(quoteHeader.discount ?? 0));
  const quoteShipping = toMoney(Number(quoteHeader.shipping ?? 0));
  const summary = summarizeChanges(changes);

  /* O ORCAMENTO MUDA SEM MEXER EM ITEM NENHUM.
     Acrescentar frete, ou dar desconto, nao acrescenta, remove nem altera
     item: muda so o total. A conta de mudancas olhava apenas a lista de
     itens, entao a tela dizia "nao ha nada para atualizar" e desligava o
     botao justamente quando havia. Este sinal viaja junto com o resumo. */
  const totalMudou = Math.abs(totalAfter - totalBefore) > 0.005;
  const resumo = {
    ...summary,
    totalChanged: totalMudou,
    itemsBefore: currentOrderItems.filter(Boolean).length,
    itemsAfter: quoteItems.length,
    totalBefore,
    totalAfter,
    quoteSubtotal,
    quoteDiscount,
    quoteShipping,
  };

  if (mode === "preview") {
    return {
      status: 200,
      corpo: {
        ok: true,
        order: {
          id: order.id,
          number: order.number,
          status: order.status,
        },
        summary: resumo,
        changes,
      },
    };
  }

  if (summary.totalChanges === 0 && !totalMudou) {
    return {
      status: 200,
      corpo: {
        ok: true,
        applied: false,
        message: "Pedido já está sincronizado com o orçamento.",
        summary: resumo,
        changes,
      },
    };
  }

  /* Quando so o total mudou, os itens ficam onde estao.
     Apagar e reinserir item identico faria os gatilhos de estoque devolverem
     e baixarem tudo de novo: o saldo termina certo, mas o extrato enche de
     movimento que nunca aconteceu, e a producao perde o vinculo do
     apontamento a troco de nada. */
  const itensMudaram = summary.totalChanges > 0;

  /* O plano de produção (rendimento, rodagem na bobina) é do item do pedido,
     não existe no orçamento: o item que continua no mesmo lugar, com o mesmo
     produto, leva o seu plano. Sem isto a baixa do estoque refeita pela
     atualização voltava à conta padrão. */
  const planoDaLinha = (index: number, productId: string | null) => {
    const anterior = oldRows[index];
    if (!anterior || !productId || anterior.product_id !== productId) return null;
    return (anterior.production_plan as Record<string, unknown> | null | undefined) ?? null;
  };
  const insertPayload = quoteItems.map((item, index) => ({
    order_id: order.id,
    product_id: item.product_id,
    title: item.title,
    description: item.description,
    quantity: item.quantity,
    unit_price: item.unit_price,
    total_price: item.total_price,
    unit_type: item.unit_type,
    width_m: item.width_m,
    height_m: item.height_m,
    length_m: item.length_m,
    /* Só o custo aprovado. Quem monta a fotografia (margem, origem, cadastro do
       produto quando o orçamento não trouxe custo) é o gatilho
       `congelar_custo_do_item_vendido` — um lugar só, que também cobre as três
       funções do banco que criam pedido e a próxima que alguém escrever. */
    calculated_cost: item.unit_cost && item.unit_cost > 0 ? item.unit_cost : null,
    valor_minimo_aplicado: item.valor_minimo_aplicado ?? null,
    valor_sem_minimo: item.valor_minimo_aplicado != null ? item.valor_sem_minimo ?? null : null,
    selected_options: item.selected_options ?? null,
    production_plan: planoDaLinha(index, item.product_id),
  }));

  if (itensMudaram) {
    const { error: deleteError } = await supabase.from("order_items").delete().eq("order_id", order.id);
    if (deleteError) return { status: 400, corpo: { error: deleteError.message } };

    if (insertPayload.length > 0) {
      const { error: insertError } = await supabase.from("order_items").insert(insertPayload);
      if (insertError) {
        const linhasAntigas = oldRows.filter((row): row is Record<string, unknown> => Boolean(row));
        if (linhasAntigas.length > 0) {
          const restorePayload = linhasAntigas.map((row) => ({
            order_id: row.order_id,
            product_id: row.product_id,
            title: row.title,
            description: row.description,
            quantity: row.quantity,
            unit_price: row.unit_price,
            total_price: row.total_price,
            unit_type: row.unit_type,
            width_m: row.width_m,
            height_m: row.height_m,
            length_m: row.length_m,
            valor_minimo_aplicado: row.valor_minimo_aplicado ?? null,
            valor_sem_minimo: row.valor_sem_minimo ?? null,
            selected_options: row.selected_options ?? null,
            production_plan: row.production_plan ?? null,
          }));
          await supabase.from("order_items").insert(restorePayload);
        }
        return { status: 400, corpo: { error: insertError.message } };
      }
    }
  }

  const nowIso = new Date().toISOString();
  const { error: updateOrderError } = await supabase
    .from("orders")
    .update({
      total_amount: totalAfter,
      updated_at: nowIso,
    })
    .eq("id", order.id)
    .in("user_id", tenantIds);
  if (updateOrderError) return { status: 400, corpo: { error: updateOrderError.message } };

  const note = itensMudaram
    ? `Pedido atualizado pelo orçamento vinculado. Itens +${summary.added} / -${summary.removed} / ~${summary.changed}. Total ${totalBefore.toFixed(2)} -> ${totalAfter.toFixed(2)}.`
    : `Pedido atualizado pelo orçamento vinculado. Itens sem mudança; só o valor final. Total ${totalBefore.toFixed(2)} -> ${totalAfter.toFixed(2)}.`;
  await supabase.from("order_history").insert({
    order_id: order.id,
    user_id: userId,
    status_from: order.status,
    status_to: order.status,
    notes: note,
  });

  return {
    status: 200,
    corpo: {
      ok: true,
      applied: true,
      message: itensMudaram
        ? "Pedido atualizado com sucesso pelo orçamento."
        : "Valor do pedido atualizado pelo orçamento. Os itens continuam os mesmos.",
      summary: resumo,
      changes,
    },
  };
}
