﻿import { createClient } from '../../src/compat/supabase.js';
import * as movimentosStock from "./movimentos-stock.tsx";
import * as artigos from "./artigos.tsx";
import * as stockLotes from "./stock-lotes.tsx";
import { createLogger } from "./logger.tsx";
import * as facturas from "./facturas.tsx";
import * as kv from "./kv_store.tsx";
import { normalizeUtf8Deep, normalizeUtf8Text } from "./utf8-normalization.tsx";
import { validarDataEmissaoCronologica } from "./documento-cronologia.tsx";
import * as exerciciosEconomicos from "./exercicios-economicos.tsx";
import * as integracaoFiscal from "./integracao-fiscal.tsx";
import {
  assegurarSemReservaPosterior,
  executarComBloqueioNumeracao,
  libertarReservaNumero,
  libertarReservaNumeroDoDocumento,
  reservarNumeroSequencial,
  restaurarReservaNumeroDoDocumento,
  type EscopoNumeroDocumento,
} from "./document-numbering.tsx";
import {
  arquivarDocumentoEliminado,
  type ContextoEliminacaoDocumento,
} from "./document-deletion-audit.tsx";
import {
  assegurarSemLancamentoContabilDefinitivo,
  removerLancamentosContabeisRascunhoDoDocumento,
  restaurarLancamentosContabeisRascunho,
  verificarSemLancamentoContabilDefinitivo,
} from "./integridade-documentos.tsx";
import { aplicarTotaisFiscais } from "./validacao-fiscal.tsx";
import { artigoMovimentaStock } from "./artigo-classificacao.tsx";

const logger = createLogger('Notas');

const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;

interface NotaData {
  empresaId: string;
  empresaSnapshot?: Record<string, unknown>;
  exercicioEconomicoId?: string;
  anoExercicio?: number;
  tipo: 'credito' | 'debito';
  numeroDocumento: string;
  dataEmissao: string;
  estado: string;
  clienteId: string;
  cliente: {
    codigo: string;
    nome: string;
    nuit: string;
    telefone?: string;
    email?: string;
    endereco?: string;
  };
  moeda: string;
  condicaoPagamento: string;
  itens: any[];
  subtotal: number;
  totalImpostos: number;
  descontos: number;
  totalGeral: number;
  observacoes?: string;
  facturaId?: string;
  facturaReferencia?: string;
}

function obterPrefixoNota(tipo: 'credito' | 'debito'): string {
  return tipo === 'credito' ? 'Nota de Cr\u00e9dito' : 'Nota de D\u00e9bito';
}

function normalizarItensParaComparacao(itens: unknown): string {
  const ordenar = (valor: unknown): unknown => {
    if (Array.isArray(valor)) return valor.map(ordenar);
    if (valor && typeof valor === 'object') {
      return Object.keys(valor as Record<string, unknown>)
        .sort()
        .reduce((resultado, chave) => {
          resultado[chave] = ordenar((valor as Record<string, unknown>)[chave]);
          return resultado;
        }, {} as Record<string, unknown>);
    }
    return valor ?? null;
  };

  return JSON.stringify(ordenar(Array.isArray(itens) ? itens : []));
}

async function listarNotasInternas(empresaId: string) {
  const supabase = createClient(supabaseUrl, supabaseServiceKey);
  const { data, error } = await supabase
    .from('kv_store_7249dcd9')
    .select('*')
    .like('key', `nota:${empresaId}:%`);

  if (error) {
    throw new Error(error.message);
  }

  return (data || []).map((item: any) => ({
    id: item.key,
    ...item.value,
  }));
}

function obterAnoNota(nota: Pick<NotaData, 'dataEmissao' | 'numeroDocumento'>): number {
  const anoNumero = Number(String(nota.numeroDocumento || '').match(/\/(\d{4})/)?.[1]);
  if (Number.isFinite(anoNumero) && anoNumero > 0) return anoNumero;

  if (nota.dataEmissao?.includes('-')) {
    const ano = Number.parseInt(nota.dataEmissao.split('T')[0].split('-')[0], 10);
    if (Number.isFinite(ano) && ano > 0) return ano;
  }

  const dataEmissao = new Date(nota.dataEmissao);
  return Number.isFinite(dataEmissao.getFullYear()) ? dataEmissao.getFullYear() : new Date().getFullYear();
}

function extrairSequenciaNota(nota: Pick<NotaData, 'tipo' | 'numeroDocumento'>, ano: number): number | null {
  const prefixo = obterPrefixoNota(nota.tipo);
  const numeroDocumento = normalizeUtf8Text(String(nota.numeroDocumento || '')).trim();
  const match = numeroDocumento.match(new RegExp(`^${prefixo} n[º°]\\s*(\\d+)\\/${ano}$`, 'i'));
  if (!match) return null;

  const sequencia = Number.parseInt(match[1], 10);
  return Number.isFinite(sequencia) && sequencia > 0 ? sequencia : null;
}

async function validarNotaEhUltima(
  nota: NotaData,
  notaId: string,
  acao: 'editada' | 'eliminada',
): Promise<void> {
  const ano = obterAnoNota(nota);
  const notas = await listarNotasInternas(nota.empresaId);
  const notasOrdenadas = notas
    .filter((item: any) => item.tipo === nota.tipo && !['anulado', 'anulada'].includes(String(item.estado || '').toLowerCase()))
    .map((item: any) => ({
      nota: item,
      sequencia: extrairSequenciaNota(item, ano),
    }))
    .filter((item: any): item is { nota: any; sequencia: number } => item.sequencia !== null)
    .sort((a, b) => a.sequencia - b.sequencia);

  const indiceNota = notasOrdenadas.findIndex((item) => item.nota.id === notaId);
  const ultimaNota = notasOrdenadas[notasOrdenadas.length - 1]?.nota;

  if (ultimaNota && ultimaNota.id !== notaId) {
    throw new Error(
      `Apenas a ultima ${obterPrefixoNota(nota.tipo).toLowerCase()} (${ultimaNota.numeroDocumento}) pode ser ${acao}. Existem ${notasOrdenadas.length - indiceNota - 1} nota(s) posterior(es).`,
    );
  }
}

export async function preverProximoNumeroNota(
  empresaId: string,
  tipo: 'credito' | 'debito',
  anoExercicio = new Date().getFullYear(),
): Promise<string> {
  const anoAtual = anoExercicio;
  const notas = await listarNotasInternas(empresaId);
  const prefixo = obterPrefixoNota(tipo);
  const regex = new RegExp(`^${prefixo} n[º°] (\\d+)/${anoAtual}$`, 'i');
  let maxNumero = 0;

  for (const nota of notas) {
    if (nota.tipo !== tipo) continue;
    const numeroDocumento = normalizeUtf8Text(String(nota.numeroDocumento || '')).replace(/\s+\(Preview\)$/i, '');
    const match = numeroDocumento.match(regex);
    if (match) {
      const numero = parseInt(match[1], 10);
      if (numero > maxNumero) {
        maxNumero = numero;
      }
    }
  }

  const proximoNumero = maxNumero + 1;
  return `${prefixo} n\u00ba ${proximoNumero.toString().padStart(2, '0')}/${anoAtual} (Preview)`;
}

export async function gerarProximoNumeroNota(
  empresaId: string,
  tipo: 'credito' | 'debito',
  anoExercicio = new Date().getFullYear(),
  numeroPretendido?: string,
): Promise<string> {
  const numeroPreview = numeroPretendido || await preverProximoNumeroNota(empresaId, tipo, anoExercicio);
  return numeroPreview.replace(/\s+\(preview\)$/i, '').trim();
}

function gerarLoteTecnicoDevolucao(numeroDocumento: string, artigoCodigo: string): string {
  const numeroNormalizado = (numeroDocumento || 'DEV')
    .toUpperCase()
    .replace(/\s+/g, '-')
    .replace(/[^A-Z0-9_-]/g, '');
  const artigoNormalizado = (artigoCodigo || 'ARTIGO')
    .toUpperCase()
    .replace(/\s+/g, '-')
    .replace(/[^A-Z0-9_-]/g, '');

  return `DEV-${numeroNormalizado}-${artigoNormalizado}`;
}

async function resolverFacturaRelacionada(
  empresaId: string,
  facturaId?: string,
  facturaReferencia?: string,
) {
  if (facturaId) {
    const factura = await facturas.buscarFacturaPorId(facturaId);
    if (factura && factura.empresaId === empresaId) {
      return factura;
    }
  }

  if (facturaReferencia) {
    return await facturas.buscarFacturaPorNumero(empresaId, facturaReferencia);
  }

  return null;
}

async function construirPlanoReposicaoNotaCredito(
  data: NotaData,
  item: { artigoId?: string; quantidade?: number },
): Promise<{
  plano: Array<{
    loteId: string;
    lote?: string;
    dataValidade?: string;
    quantidade: number;
  }>;
  quantidadeSemRastreio: number;
}> {
  const quantidadeSolicitada = Number(item.quantidade || 0);
  if (!item.artigoId || quantidadeSolicitada <= 0) {
    return { plano: [], quantidadeSemRastreio: quantidadeSolicitada };
  }

  const movimentosVenda = await movimentosStock.listarMovimentos(data.empresaId, {
    artigoId: item.artigoId,
    tipo: 'saida',
    motivo: 'venda',
  });

  const referenciaDocumento = data.facturaReferencia?.trim().toLowerCase();
  const codigoCliente = data.cliente?.codigo?.trim().toLowerCase();

  const movimentosRelacionados = movimentosVenda.filter((movimento) => {
    if (referenciaDocumento) {
      return movimento.documentoNumero?.trim().toLowerCase() === referenciaDocumento;
    }

    if (codigoCliente) {
      return movimento.observacoes?.toLowerCase().includes(codigoCliente) || false;
    }

    return true;
  });

  const lotesConsumidos = movimentosRelacionados
    .sort((a, b) => {
      const ordemData = a.dataMovimento.localeCompare(b.dataMovimento);
      if (ordemData !== 0) return ordemData;
      return a.dataCriacao.localeCompare(b.dataCriacao);
    })
    .flatMap((movimento) => movimento.lotesConsumidos || [])
    .filter((lote) => lote.loteId && lote.quantidade > 0);

  let restante = quantidadeSolicitada;
  const plano: Array<{
    loteId: string;
    lote?: string;
    dataValidade?: string;
    quantidade: number;
  }> = [];

  for (const lote of lotesConsumidos) {
    if (restante <= 0) break;

    const quantidadeReposicao = Math.min(restante, lote.quantidade);
    plano.push({
      loteId: lote.loteId,
      lote: lote.lote,
      dataValidade: lote.dataValidade,
      quantidade: quantidadeReposicao,
    });
    restante -= quantidadeReposicao;
  }

  return {
    plano,
    quantidadeSemRastreio: restante,
  };
}

export async function createNota(data: NotaData) {
  data = await aplicarTotaisFiscais(data) as NotaData;
  const supabase = createClient(supabaseUrl, supabaseServiceKey);
  let reservaNumeroKey: string | null = null;
  let notaPersistida = false;

  try {
    // ✅ Requisito Financas/AT: uma nota (credito ou debito) nunca pode ser
    // emitida contra uma factura ja anulada — a factura anulada nao existe
    // mais para efeitos fiscais, nao ha nada para creditar/debitar contra ela.
    // A validacao de saldo (so para credito) reutiliza a mesma factura ja
    // resolvida aqui, para nao consultar a base de dados duas vezes.
    let facturaParaValidacao: Awaited<ReturnType<typeof resolverFacturaRelacionada>> = null;
    if (data.facturaId || data.facturaReferencia) {
      facturaParaValidacao = await resolverFacturaRelacionada(
        data.empresaId,
        data.facturaId,
        data.facturaReferencia,
      );

      if (facturaParaValidacao && String((facturaParaValidacao as any).estado || '').toLowerCase() === 'anulado') {
        throw new Error(
          `Nao e possivel emitir uma ${obterPrefixoNota(data.tipo)} contra a ${facturaParaValidacao.numeroDocumento}: essa factura ja esta anulada.`,
        );
      }
    }

    // ✅ Requisito Financas/AT: uma Nota de Credito nunca pode exceder o
    // saldo ainda por creditar da factura correspondente — nem sozinha, nem
    // somada a outras Notas de Credito ja emitidas (e nao anuladas) contra a
    // mesma factura.
    if (data.tipo === 'credito' && facturaParaValidacao) {
      const valorFactura = Math.abs(Number(facturaParaValidacao.totalGeral) || 0);
      const notasCreditoExistentes = (await listarNotasInternas(data.empresaId)).filter((notaExistente: any) =>
        notaExistente.tipo === 'credito' &&
        String(notaExistente.estado || '').toLowerCase() !== 'anulada' &&
        (notaExistente.facturaId === facturaParaValidacao.id ||
          (notaExistente.facturaReferencia &&
            notaExistente.facturaReferencia === facturaParaValidacao.numeroDocumento)),
      );
      const valorJaCreditado = notasCreditoExistentes.reduce(
        (soma: number, notaExistente: any) => soma + Math.abs(Number(notaExistente.totalGeral) || 0),
        0,
      );
      const valorEstaNota = Math.abs(Number(data.totalGeral) || 0);
      const saldoDisponivel = Math.round((valorFactura - valorJaCreditado) * 100) / 100;

      if (valorEstaNota > saldoDisponivel + 0.01) {
        throw new Error(
          `O valor desta Nota de Credito (${valorEstaNota.toFixed(2)} ${data.moeda}) excede o saldo ainda por creditar da ${facturaParaValidacao.numeroDocumento} ` +
          `(${saldoDisponivel.toFixed(2)} ${data.moeda} disponivel de um total de ${valorFactura.toFixed(2)} ${data.moeda}).`,
        );
      }
    }

    const exercicio = await exerciciosEconomicos.validarExercicioAberto(
      data.empresaId,
      data.dataEmissao,
      obterPrefixoNota(data.tipo),
    );

    const numeroDocumentoRecebido = normalizeUtf8Text(data.numeroDocumento);
    const notasExistentes = await listarNotasInternas(data.empresaId);
    const prefixo = obterPrefixoNota(data.tipo);
    const reservaNumero = await reservarNumeroSequencial<NotaData>({
      empresaId: data.empresaId,
      modulo: "faturacao-vendas",
      tipoDocumento: `nota_${data.tipo}`,
      ano: exercicio.ano,
      numeroPretendido: numeroDocumentoRecebido,
      documentosExistentes: notasExistentes.filter((nota) => nota.tipo === data.tipo),
      carregarDocumentosExistentes: async () =>
        (await listarNotasInternas(data.empresaId)).filter((nota) => nota.tipo === data.tipo),
      extrairSequencia: (nota, ano) => extrairSequenciaNota(nota, ano),
      formatarNumero: (sequencia, ano) => `${prefixo} n\u00ba ${sequencia.toString().padStart(2, '0')}/${ano}`,
    });
    reservaNumeroKey = reservaNumero.reservaKey;
    data.numeroDocumento = reservaNumero.numeroDocumento;

    data.exercicioEconomicoId = exercicio.id;
    data.anoExercicio = exercicio.ano;

    // 🔍 DEBUG: Verificar se numeroDocumento está chegando
    logger.debug(`numeroDocumento recebido:`, data.numeroDocumento);

    validarDataEmissaoCronologica(data, notasExistentes, {
      rotuloDocumento: obterPrefixoNota(data.tipo),
      tipo: data.tipo,
    });
    
    const notaId = `nota:${data.empresaId}:${crypto.randomUUID()}`;
    
    // 1️⃣ Inserir nota
    const { data: nota, error } = await supabase
      .from('kv_store_7249dcd9')
      .insert({
        key: notaId,
        value: {
          ...data,
          createdAt: new Date().toISOString(),
          updatedAt: new Date().toISOString(),
        },
      })
      .select()
      .single();

    if (error) {
      logger.error('Erro ao criar nota:', error);
      await libertarReservaNumero(reservaNumero.reservaKey).catch(() => null);
      return { success: false, error: error.message };
    }
    notaPersistida = true;

    logger.info(`Nota ${data.tipo.toUpperCase()} criada com sucesso:`, nota);
    
    // 2️⃣ Criar movimentos automáticos baseado no tipo
    logger.info(`Criando movimentos para nota ${data.tipo.toUpperCase()} ${data.numeroDocumento}`);
    
    const movimentosCriados = [];
    const errosMovimentos = [];
    
    for (const item of data.itens) {
      try {
        // Buscar dados completos do artigo
        const artigo = await artigos.buscarArtigoPorId(item.artigoId);
        
        if (!artigo) {
          logger.warn(`Artigo ${item.codigo} não encontrado, pulando movimento`);
          continue;
        }
        
        // Apenas criar movimento para PRODUTOS (não serviços) — mesma
        // classificacao partilhada usada por facturas.tsx/vendas-dinheiro.tsx,
        // incluindo a rede de seguranca do codigo SERV (antes esta verificacao
        // usava so artigo.tipo, sem essa rede de seguranca).
        if (!artigoMovimentaStock(artigo, item)) {
          logger.info(`Item ${item.codigo} é serviço, não cria movimento`);
          continue;
        }
        
        // ❌ NOTA DE DÉBITO NÃO MOVIMENTA ESTOQUE (é apenas cobrança adicional/ajuste financeiro)
        if (data.tipo === 'debito') {
          logger.info(`Nota de DÉBITO não movimenta estoque (ajuste financeiro)`);
          continue;
        }
        
        // Verificar se tem armazém configurado
        if (!artigo.armazemId) {
          logger.warn(`Produto ${item.codigo} sem armazém, não pode criar movimento`);
          errosMovimentos.push({
            artigoCodigo: item.codigo,
            erro: 'Produto sem armazém configurado'
          });
          continue;
        }
        
        // ✅ NOTA DE CRÉDITO → Cliente devolveu mercadoria → ENTRADA no estoque
        const tipoMovimento = 'entrada';
        const motivoMovimento = 'devolucao_cliente';
        const planoReposicao = await construirPlanoReposicaoNotaCredito(data, item);
        const loteTecnico =
          planoReposicao.quantidadeSemRastreio > 0
            ? gerarLoteTecnicoDevolucao(data.numeroDocumento, artigo.codigo)
            : undefined;
        
        logger.info(`Criando movimento ${tipoMovimento.toUpperCase()}: ${item.codigo} - ${item.quantidade} unidades`);
        logger.debug(`documentoNumero que será enviado:`, data.numeroDocumento);
        logger.debug(`documentoTipo que será enviado: Nota Crédito`);
        
        const resultadoMovimento = await movimentosStock.criarMovimento({
          empresaId: data.empresaId,
          artigoId: item.artigoId,
          tipo: tipoMovimento,
          quantidade: item.quantidade,
          motivo: motivoMovimento,
          armazemId: artigo.armazemId,
          lote: planoReposicao.plano.length === 0
            ? loteTecnico
            : (
                planoReposicao.plano.length === 1 && planoReposicao.quantidadeSemRastreio === 0
                  ? planoReposicao.plano[0].lote
                  : undefined
              ),
          loteId: planoReposicao.plano.length === 1 && planoReposicao.quantidadeSemRastreio === 0
            ? planoReposicao.plano[0].loteId
            : undefined,
          lotesConsumidos: planoReposicao.plano.length > 0
            ? planoReposicao.plano.map((lote) => ({
                loteId: lote.loteId,
                lote: lote.lote,
                dataValidade: lote.dataValidade,
                quantidade: lote.quantidade,
              }))
            : undefined,
          documentoTipo: 'Nota Crédito',
          documentoNumero: data.numeroDocumento,
          documentoId: notaId,
          dataMovimento: data.dataEmissao,
          observacoes: `Devolução de cliente ${data.cliente.nome} (${data.cliente.codigo})${data.facturaReferencia ? ` - Ref: ${data.facturaReferencia}` : ''}${loteTecnico ? ` - Lote técnico: ${loteTecnico}` : ''}`,
        });
        
        if (resultadoMovimento.success) {
          for (const lote of planoReposicao.plano) {
            const loteAtual = await stockLotes.buscarLotePorId(lote.loteId);
            if (!loteAtual) continue;

            await stockLotes.upsertLote({
              empresaId: loteAtual.empresaId,
              artigoId: loteAtual.artigoId,
              armazemId: loteAtual.armazemId,
              lote: loteAtual.lote,
              dataValidade: loteAtual.dataValidade,
              dataFabricacao: loteAtual.dataFabricacao,
              quantidade: loteAtual.quantidade + lote.quantidade,
              quantidadeReservada: loteAtual.quantidadeReservada,
              custoUnitario: loteAtual.custoUnitario,
              fornecedorId: loteAtual.fornecedorId,
              documentoCompraId: loteAtual.documentoCompraId,
              localizacao: loteAtual.localizacao,
              status: loteAtual.status,
            });
          }

          if (planoReposicao.quantidadeSemRastreio > 0 && loteTecnico) {
            const loteExistente = await stockLotes.buscarLotePorIdentidade(
              data.empresaId,
              item.artigoId,
              artigo.armazemId,
              loteTecnico,
            );

            await stockLotes.upsertLote({
              empresaId: data.empresaId,
              artigoId: item.artigoId,
              armazemId: artigo.armazemId,
              lote: loteTecnico,
              quantidade: (loteExistente?.quantidade || 0) + planoReposicao.quantidadeSemRastreio,
              quantidadeReservada: loteExistente?.quantidadeReservada || 0,
              custoUnitario: loteExistente?.custoUnitario || artigo.precoCompra || 0,
              status: loteExistente?.status || 'ativo',
            });
          }

          movimentosCriados.push({
            artigoCodigo: item.codigo,
            quantidade: item.quantidade,
            tipo: tipoMovimento,
            movimentoId: resultadoMovimento.data?.id
          });
          logger.info(`Movimento criado com sucesso para ${item.codigo}`);
        } else {
          errosMovimentos.push({
            artigoCodigo: item.codigo,
            erro: resultadoMovimento.error
          });
          logger.error(`Erro ao criar movimento para ${item.codigo}:`, resultadoMovimento.error);
        }
        
      } catch (error) {
        errosMovimentos.push({
          artigoCodigo: item.codigo,
          erro: error.message
        });
        logger.error(`Erro ao processar item ${item.codigo}:`, error);
      }
    }
    
    logger.info(`Resumo: ${movimentosCriados.length} movimentos criados, ${errosMovimentos.length} erros`);
    
    if (errosMovimentos.length > 0) {
      logger.warn(`Alguns movimentos falharam:`, errosMovimentos);
      await movimentosStock.eliminarMovimentosPorDocumento(
        data.empresaId,
        data.numeroDocumento,
        'Nota Crédito',
      ).catch(() => null);

      const agoraErro = new Date().toISOString();
      await kv.set(notaId, {
        ...data,
        id: notaId,
        estado: 'anulada',
        dataAnulacao: agoraErro,
        updatedAt: agoraErro,
        documentoPreservado: true,
        erroFinalizacao: errosMovimentos.map((item: any) => `${item.artigoCodigo}: ${item.erro}`).join('; '),
      });

      return {
        success: false,
        error: `A nota ${data.numeroDocumento} falhou na movimentação de stock e foi preservada como anulada: ${errosMovimentos.map((item: any) => item.erro).join('; ')}`,
      };
    }

    if (data.tipo === 'credito') {
      const facturaRelacionada = await resolverFacturaRelacionada(
        data.empresaId,
        data.facturaId,
        data.facturaReferencia,
      );

      if (facturaRelacionada) {
        // ✅ A nota já está persistida e os movimentos de stock já foram confirmados
        // nesta altura — uma falha aqui não pode fazer criarNota() reportar erro ao
        // cliente (isso levava a retries que criavam notas duplicadas). Regista-se
        // como pendência de integração em vez de propagar para o catch externo.
        try {
          await facturas.recalcularEstadoFactura(facturaRelacionada.id);
          await integracaoFiscal.resolverPendencia(data.empresaId, facturaRelacionada.id).catch(() => null);
        } catch (error) {
          logger.error(`Erro ao recalcular estado da factura ${facturaRelacionada.id} apos nota ${data.numeroDocumento}:`, error);
          await integracaoFiscal.registrarPendencia(
            { empresaId: data.empresaId, id: facturaRelacionada.id, numeroDocumento: facturaRelacionada.id, tipoDocumento: 'factura' },
            [`Falha ao recalcular estado apos nota de credito ${data.numeroDocumento}: ${error instanceof Error ? error.message : String(error)}`],
          ).catch(() => null);
        }
      }
    }

    return { success: true, data: normalizeUtf8Deep({ id: nota.key, ...nota.value }) };
  } catch (error) {
    logger.error('Erro ao criar nota:', error);
    if (!notaPersistida) {
      await libertarReservaNumero(reservaNumeroKey).catch(() => null);
    }
    return { success: false, error: String(error) };
  }
}

export async function getNotasByEmpresa(empresaId: string) {
  try {
    const notas = normalizeUtf8Deep(await listarNotasInternas(empresaId));

    logger.info(`${notas.length} notas encontradas para empresa ${empresaId}`);
    return { success: true, data: notas };
  } catch (error) {
    logger.error('Erro ao buscar notas:', error);
    return { success: false, error: String(error) };
  }
}

export async function getNotaById(notaId: string) {
  const supabase = createClient(supabaseUrl, supabaseServiceKey);

  try {
    const { data, error } = await supabase
      .from('kv_store_7249dcd9')
      .select('*')
      .eq('key', notaId)
      .maybeSingle(); // ✅ MUDOU DE .single() PARA .maybeSingle()

    if (error) {
      logger.error('Erro ao buscar nota:', error);
      return { success: false, error: error.message };
    }

    if (!data) {
      logger.error('Nota não encontrada:', notaId);
      return { success: false, error: 'Nota não encontrada' };
    }

    const nota = normalizeUtf8Deep({ id: data.key, ...data.value });

    logger.info('Nota encontrada:', nota);
    return { success: true, data: nota };
  } catch (error) {
    logger.error('Erro ao buscar nota:', error);
    return { success: false, error: String(error) };
  }
}

export async function podeAlterarNota(
  notaId: string,
  acao: 'editada' | 'eliminada',
): Promise<{ pode: boolean; motivo?: string }> {
  const resultado = await getNotaById(notaId);
  if (!resultado.success || !resultado.data) {
    return { pode: false, motivo: resultado.error || 'Nota nao encontrada.' };
  }

  const nota = resultado.data as NotaData & { id: string };
  if (String(nota.estado || '').toLowerCase() === 'anulado') {
    return { pode: false, motivo: 'Uma nota anulada e apenas consultada.' };
  }

  try {
    await validarNotaEhUltima(nota, notaId, acao);
    await exerciciosEconomicos.validarExercicioAberto(
      nota.empresaId,
      nota.dataEmissao,
      obterPrefixoNota(nota.tipo),
    );
  } catch (error) {
    return { pode: false, motivo: error instanceof Error ? error.message : String(error) };
  }

  return verificarSemLancamentoContabilDefinitivo({
    empresaId: nota.empresaId,
    documentoId: notaId,
    numeroDocumento: nota.numeroDocumento,
    rotuloDocumento: obterPrefixoNota(nota.tipo),
  });
}

/**
 * Verificar se nota pode ser anulada.
 *
 * Ao contrario de podeAlterarNota(id,'eliminada'), nao exige ser a ultima
 * (validarNotaEhUltima) - anular nao liberta o numero, por isso funciona em
 * qualquer posicao.
 */
export async function podeAnularNota(notaId: string): Promise<{ pode: boolean; motivo?: string }> {
  const resultado = await getNotaById(notaId);
  if (!resultado.success || !resultado.data) {
    return { pode: false, motivo: resultado.error || 'Nota nao encontrada.' };
  }

  const nota = resultado.data as NotaData & { id: string };
  if (String(nota.estado || '').toLowerCase() === 'anulado') {
    return { pode: false, motivo: 'Nota ja esta anulada.' };
  }

  try {
    await exerciciosEconomicos.validarExercicioAberto(
      nota.empresaId,
      nota.dataEmissao,
      obterPrefixoNota(nota.tipo),
    );
  } catch (error) {
    return { pode: false, motivo: error instanceof Error ? error.message : String(error) };
  }

  return verificarSemLancamentoContabilDefinitivo({
    empresaId: nota.empresaId,
    documentoId: notaId,
    numeroDocumento: nota.numeroDocumento,
    rotuloDocumento: obterPrefixoNota(nota.tipo),
  });
}

/**
 * Verificar se nota pode ser eliminada (removida para a lixeira).
 *
 * Ao contrario de podeAnularNota(), exige ser a ultima do seu tipo
 * (validarNotaEhUltima) - eliminar liberta o numero para reutilizacao, por
 * isso so pode ser a cauda da sequencia.
 */
export async function podeEliminarNota(notaId: string): Promise<{ pode: boolean; motivo?: string }> {
  const resultado = await getNotaById(notaId);
  if (!resultado.success || !resultado.data) {
    return { pode: false, motivo: resultado.error || 'Nota não encontrada.' };
  }

  const nota = resultado.data as NotaData & { id: string };
  if (String(nota.estado || '').toLowerCase() === 'anulada') {
    return { pode: false, motivo: 'Nota já está anulada.' };
  }

  try {
    await exerciciosEconomicos.validarExercicioAberto(
      nota.empresaId,
      nota.dataEmissao,
      obterPrefixoNota(nota.tipo),
    );
    await assegurarSemLancamentoContabilDefinitivo({
      empresaId: nota.empresaId,
      documentoId: notaId,
      numeroDocumento: nota.numeroDocumento,
      rotuloDocumento: obterPrefixoNota(nota.tipo),
    });
    await validarNotaEhUltima(nota, notaId, 'eliminada');
  } catch (error) {
    return { pode: false, motivo: error instanceof Error ? error.message : String(error) };
  }

  return { pode: true };
}

export async function updateNota(notaId: string, data: Partial<NotaData>) {
  const supabase = createClient(supabaseUrl, supabaseServiceKey);

  try {
    // Buscar nota existente
    const { data: existingNota, error: fetchError } = await supabase
      .from('kv_store_7249dcd9')
      .select('*')
      .eq('key', notaId)
      .maybeSingle(); // ✅ MUDOU DE .single() PARA .maybeSingle()

    if (fetchError) {
      logger.error('Erro ao buscar nota para atualizar:', fetchError);
      return { success: false, error: fetchError.message };
    }

    if (!existingNota) {
      logger.error('Nota não encontrada:', notaId);
      return { success: false, error: 'Nota não encontrada' };
    }

    const notaAnterior = existingNota.value || {};
    await assegurarSemLancamentoContabilDefinitivo({
      empresaId: notaAnterior.empresaId,
      documentoId: notaId,
      numeroDocumento: notaAnterior.numeroDocumento,
      rotuloDocumento: obterPrefixoNota(notaAnterior.tipo),
    });
    await validarNotaEhUltima(notaAnterior, notaId, 'editada');

    if (data.clienteId && data.clienteId !== notaAnterior.clienteId) {
      throw new Error('O cliente nao pode ser alterado na edicao de uma nota fiscal.');
    }
    if (data.numeroDocumento && data.numeroDocumento !== notaAnterior.numeroDocumento) {
      throw new Error('O numero de uma nota fiscal emitida nao pode ser alterado.');
    }
    await exerciciosEconomicos.validarExercicioAberto(
      notaAnterior.empresaId,
      data.dataEmissao || notaAnterior.dataEmissao,
      obterPrefixoNota(notaAnterior.tipo),
    );

    const notasExistentes = await listarNotasInternas(notaAnterior.empresaId);
    validarDataEmissaoCronologica(
      { ...notaAnterior, ...data, id: notaId, numeroDocumento: notaAnterior.numeroDocumento },
      notasExistentes,
      {
        rotuloDocumento: obterPrefixoNota(notaAnterior.tipo),
        tipo: notaAnterior.tipo,
        ignorarId: notaId,
      },
    );

    if (
      notaAnterior.tipo === 'credito' &&
      data.itens &&
      normalizarItensParaComparacao(data.itens) !== normalizarItensParaComparacao(notaAnterior.itens)
    ) {
      throw new Error(
        'As linhas de uma nota de credito que ja movimentou stock nao podem ser alteradas. Anule a nota e emita uma nova.',
      );
    }

    if (data.itens) {
      data = await aplicarTotaisFiscais({ ...notaAnterior, ...data }) as Partial<NotaData>;
    }

    // ✅ Mesma validacao de saldo aplicada em createNota — repetida aqui para
    // o caso (raro, mas possivel) de uma edicao alterar o valor da nota sem
    // passar pelo bloqueio de itens acima (ex: reenvio directo de totalGeral).
    if (notaAnterior.tipo === 'credito' && (notaAnterior.facturaId || notaAnterior.facturaReferencia)) {
      const facturaParaValidacao = await resolverFacturaRelacionada(
        notaAnterior.empresaId,
        notaAnterior.facturaId,
        notaAnterior.facturaReferencia,
      );

      if (facturaParaValidacao) {
        const valorFactura = Math.abs(Number(facturaParaValidacao.totalGeral) || 0);
        const notasCreditoExistentes = (await listarNotasInternas(notaAnterior.empresaId)).filter((notaExistente: any) =>
          notaExistente.id !== notaId &&
          notaExistente.tipo === 'credito' &&
          String(notaExistente.estado || '').toLowerCase() !== 'anulada' &&
          (notaExistente.facturaId === facturaParaValidacao.id ||
            (notaExistente.facturaReferencia &&
              notaExistente.facturaReferencia === facturaParaValidacao.numeroDocumento)),
        );
        const valorJaCreditado = notasCreditoExistentes.reduce(
          (soma: number, notaExistente: any) => soma + Math.abs(Number(notaExistente.totalGeral) || 0),
          0,
        );
        const valorEstaNota = Math.abs(Number(data.totalGeral ?? notaAnterior.totalGeral) || 0);
        const saldoDisponivel = Math.round((valorFactura - valorJaCreditado) * 100) / 100;

        if (valorEstaNota > saldoDisponivel + 0.01) {
          throw new Error(
            `O valor desta Nota de Credito (${valorEstaNota.toFixed(2)} ${notaAnterior.moeda}) excede o saldo ainda por creditar da ${facturaParaValidacao.numeroDocumento} ` +
            `(${saldoDisponivel.toFixed(2)} ${notaAnterior.moeda} disponivel de um total de ${valorFactura.toFixed(2)} ${notaAnterior.moeda}).`,
          );
        }
      }
    }

    // Atualizar nota
    const { data: updatedNota, error: updateError } = await supabase
      .from('kv_store_7249dcd9')
      .update({
        value: {
          ...existingNota.value,
          ...data,
          empresaSnapshot: existingNota.value?.empresaSnapshot || data.empresaSnapshot,
          empresaId: notaAnterior.empresaId,
          tipo: notaAnterior.tipo,
          numeroDocumento: notaAnterior.numeroDocumento,
          clienteId: notaAnterior.clienteId,
          cliente: notaAnterior.cliente,
          updatedAt: new Date().toISOString(),
        },
      })
      .eq('key', notaId)
      .select()
      .single();

    if (updateError) {
      logger.error('Erro ao atualizar nota:', updateError);
      return { success: false, error: updateError.message };
    }

    const facturaAnterior = await resolverFacturaRelacionada(
      notaAnterior.empresaId,
      notaAnterior.facturaId,
      notaAnterior.facturaReferencia,
    );
    const facturaAtual = await resolverFacturaRelacionada(
      notaAnterior.empresaId || data.empresaId || '',
      data.facturaId ?? notaAnterior.facturaId,
      data.facturaReferencia ?? notaAnterior.facturaReferencia,
    );

    if (notaAnterior.tipo === 'credito' && facturaAnterior) {
      await facturas.recalcularEstadoFactura(facturaAnterior.id);
    }

    const tipoAtual = data.tipo ?? notaAnterior.tipo;
    if (tipoAtual === 'credito' && facturaAtual) {
      await facturas.recalcularEstadoFactura(facturaAtual.id);
    }

    logger.info('Nota atualizada com sucesso:', updatedNota);
    return { success: true, data: normalizeUtf8Deep({ id: updatedNota.key, ...updatedNota.value }) };
  } catch (error) {
    logger.error('Erro ao atualizar nota:', error);
    return { success: false, error: String(error) };
  }
}

/**
 * Eliminar nota.
 *
 * Decisao de negocio: eliminar continua disponivel (gerido pela permissao
 * "deletar"), mas so para a ULTIMA nota do mesmo tipo, sem lancamento
 * contabilistico definitivo - nunca deixa buracos na numeracao fiscal. O
 * documento nunca e destruido: e movido para uma tabela de lixo
 * (arquivarDocumentoEliminado) antes de ser removido da listagem principal.
 * Anular continua a ser a unica forma de invalidar uma nota que nao seja a
 * ultima do seu tipo.
 */
export async function deleteNota(
  notaId: string,
  contexto: ContextoEliminacaoDocumento = {},
) {
  const supabase = createClient(supabaseUrl, supabaseServiceKey);

  try {
    const { data: notaData, error: fetchError } = await supabase
      .from('kv_store_7249dcd9')
      .select('*')
      .eq('key', notaId)
      .maybeSingle();

    if (fetchError) {
      logger.error('Erro ao buscar nota para excluir:', fetchError);
      return { success: false, error: fetchError.message };
    }

    if (!notaData) {
      logger.error('Nota não encontrada:', notaId);
      return { success: false, error: 'Nota não encontrada' };
    }

    const nota = notaData.value as NotaData;
    const ano = obterAnoNota(nota);
    const sequencia = extrairSequenciaNota(nota, ano);
    if (!sequencia) {
      return { success: false, error: 'Não foi possível identificar a sequência da nota.' };
    }

    const escopo: EscopoNumeroDocumento = {
      empresaId: nota.empresaId,
      modulo: 'faturacao-vendas',
      tipoDocumento: `nota_${nota.tipo}`,
      ano,
    };

    return await executarComBloqueioNumeracao(escopo, async () => {
      const actualResult = await getNotaById(notaId);
      if (!actualResult.success || !actualResult.data) {
        throw new Error('Nota não encontrada. A lista pode estar desactualizada.');
      }
      const actual = actualResult.data as NotaData & { id: string };

      await assegurarSemLancamentoContabilDefinitivo({
        empresaId: actual.empresaId,
        documentoId: notaId,
        numeroDocumento: actual.numeroDocumento,
        rotuloDocumento: obterPrefixoNota(actual.tipo),
      });
      await validarNotaEhUltima(actual, notaId, 'eliminada');
      await exerciciosEconomicos.validarExercicioAberto(
        actual.empresaId,
        actual.dataEmissao,
        obterPrefixoNota(actual.tipo),
      );

      const notas = (await listarNotasInternas(actual.empresaId)).filter((item) => item.tipo === actual.tipo);
      const sequencias = notas
        .map((item) => extrairSequenciaNota(item, ano))
        .filter((item): item is number => item !== null);
      await assegurarSemReservaPosterior(escopo, sequencia, sequencias);
      await libertarReservaNumeroDoDocumento(escopo, sequencia);

      let lancamentosRascunho: Array<{ key: string; value: any }> = [];
      let movimentosStockOriginais: movimentosStock.MovimentoStock[] = [];
      let documentoEliminado = false;
      let arquivoEliminacaoKey: string | null = null;
      try {
        arquivoEliminacaoKey = await arquivarDocumentoEliminado({
          chaveOriginal: notaId,
          empresaId: actual.empresaId,
          tipoDocumento: `nota_${actual.tipo}`,
          numeroDocumento: actual.numeroDocumento,
          documento: actual as unknown as Record<string, unknown>,
          contexto,
        });
        lancamentosRascunho = await removerLancamentosContabeisRascunhoDoDocumento({
          empresaId: actual.empresaId,
          documentoId: notaId,
          numeroDocumento: actual.numeroDocumento,
        });
        movimentosStockOriginais = await movimentosStock.listarMovimentos(actual.empresaId, {
          documentoNumero: actual.numeroDocumento,
          documentoTipo: actual.tipo === 'credito' ? 'Nota Crédito' : 'Nota Débito',
        });
        const resultadoMovimentos = await movimentosStock.eliminarMovimentosPorDocumento(
          actual.empresaId,
          actual.numeroDocumento,
          actual.tipo === 'credito' ? 'Nota Crédito' : 'Nota Débito',
        );
        if (!resultadoMovimentos.success) {
          throw new Error(resultadoMovimentos.error || 'Não foi possível eliminar os movimentos da nota.');
        }

        const { error } = await supabase.from('kv_store_7249dcd9').delete().eq('key', notaId);
        if (error) throw new Error(error.message);
        documentoEliminado = true;
      } catch (error) {
        const falhasRestauroStock: string[] = [];
        for (const movimento of movimentosStockOriginais) {
          const aindaExiste = await movimentosStock.buscarMovimentoPorId(movimento.id);
          if (aindaExiste) continue;

          const resultadoRestauro = await movimentosStock.criarMovimento({
            ...movimento,
            permitirConsumoExpirados: true,
          });
          if (!resultadoRestauro.success) {
            falhasRestauroStock.push(
              `${movimento.artigoCodigo}: ${resultadoRestauro.error || 'falha desconhecida'}`,
            );
          }
        }
        await restaurarReservaNumeroDoDocumento(escopo, sequencia, actual.numeroDocumento).catch(() => null);
        if (documentoEliminado) await kv.set(notaId, actual).catch(() => null);
        await restaurarLancamentosContabeisRascunho(lancamentosRascunho).catch(() => null);
        if (arquivoEliminacaoKey) await kv.del(arquivoEliminacaoKey).catch(() => null);
        if (falhasRestauroStock.length > 0) {
          throw new Error(
            `${String(error)}; falhou o restauro de stock: ${falhasRestauroStock.join('; ')}`,
          );
        }
        throw error;
      }

      if (actual.tipo === 'credito') {
        const facturaRelacionada = await resolverFacturaRelacionada(
          actual.empresaId,
          actual.facturaId,
          actual.facturaReferencia,
        );
        if (facturaRelacionada) {
          await facturas.recalcularEstadoFactura(facturaRelacionada.id).catch((error) => {
            logger.error('Nota eliminada, mas falhou a actualização do estado da factura relacionada:', error);
          });
        }
      }

      logger.info('Nota eliminada e número libertado:', notaId);
      return { success: true, data: normalizeUtf8Deep({ ...actual, eliminadoFisicamente: true }) };
    });
  } catch (error) {
    logger.error('Erro ao eliminar nota:', error);
    return { success: false, error: String(error) };
  }
}

/**
 * Anular nota - substitui a eliminacao. Preserva o documento, o numero e
 * o historico; reverte (nao apaga) movimentos de stock e lancamentos
 * rascunho, mesmo padrao de vendasDinheiro.anularVD(). Ao anular uma Nota
 * de Credito, recalcula o estado da factura relacionada (a activaAo pode
 * ter sido a razao dela estar marcada como anulada).
 */
export async function anularNota(notaId: string, motivo?: string) {
  const supabase = createClient(supabaseUrl, supabaseServiceKey);

  try {
    const resultado = await getNotaById(notaId);
    if (!resultado.success || !resultado.data) {
      return { success: false, error: resultado.error || "Nota não encontrada" };
    }
    const nota = resultado.data as NotaData & { id: string };

    if (String(nota.estado || "").toLowerCase() === "anulado") {
      return { success: false, error: "Nota já está anulada" };
    }

    await assegurarSemLancamentoContabilDefinitivo({
      empresaId: nota.empresaId,
      documentoId: notaId,
      numeroDocumento: nota.numeroDocumento,
      rotuloDocumento: obterPrefixoNota(nota.tipo),
    });
    await exerciciosEconomicos.validarExercicioAberto(
      nota.empresaId,
      nota.dataEmissao,
      obterPrefixoNota(nota.tipo),
    );

    const agora = new Date().toISOString();
    const { data: updatedNota, error: updateError } = await supabase
      .from("kv_store_7249dcd9")
      .update({
        value: {
          ...resultado.data,
          estado: "anulado",
          dataAnulacao: agora,
          motivoAnulacao: motivo,
          updatedAt: agora,
        },
      })
      .eq("key", notaId)
      .select()
      .single();

    if (updateError) {
      return { success: false, error: updateError.message };
    }

    await removerLancamentosContabeisRascunhoDoDocumento({
      empresaId: nota.empresaId,
      documentoId: notaId,
      numeroDocumento: nota.numeroDocumento,
    });

    const resultadoReversao = await movimentosStock.reverterMovimentosPorDocumento(
      nota.empresaId,
      nota.numeroDocumento,
      nota.tipo === "credito" ? "Nota Crédito" : "Nota Débito",
      {
        responsavel: "Sistema",
        observacoes: `Anulação da ${obterPrefixoNota(nota.tipo)} ${nota.numeroDocumento}. ${motivo?.trim() || "Sem motivo informado."}`,
        documentoTipoReversao: "anulacao_nota",
        documentoNumeroReversao: nota.numeroDocumento,
      },
    );
    if (!resultadoReversao.success) {
      logger.error("Nota anulada, mas falhou a reversao de movimentos de stock:", resultadoReversao.erros);
    }

    if (nota.tipo === "credito") {
      const facturaRelacionada = await resolverFacturaRelacionada(
        nota.empresaId,
        nota.facturaId,
        nota.facturaReferencia,
      );
      if (facturaRelacionada) {
        await facturas.recalcularEstadoFactura(facturaRelacionada.id).catch((error) => {
          logger.error("Nota anulada, mas falhou a actualizacao do estado da factura relacionada:", error);
        });
      }
    }

    logger.info(`Nota ${nota.numeroDocumento} anulada (motivo: ${motivo || "não informado"}).`);
    return { success: true, data: normalizeUtf8Deep({ id: updatedNota.key, ...updatedNota.value }) };
  } catch (error) {
    logger.error("Erro ao anular nota:", error);
    return { success: false, error: String(error) };
  }
}
