/**
 * Notas recebidas: as que OUTRAS empresas emitiram contra o CNPJ desta empresa.
 *
 * A SEFAZ entrega em duas etapas. Primeiro o resumo (quem emitiu, valor, chave)
 * e, só depois que a empresa se manifesta, o XML autorizado completo — que é o
 * que vale para o contador. Por isso cada linha guarda `completa` e a
 * manifestação, e a tela fala nesses termos: "só o resumo" e "nota completa".
 *
 * Duas armadilhas:
 *
 * - A SEFAZ limita quantas vezes cada CNPJ consulta a distribuição. Pedir busca
 *   sem parar faz ela bloquear o CNPJ. Por isso a busca sob demanda respeita o
 *   intervalo devolvido pela Spedy e a rotina automática roda de hora em hora.
 * - Desconhecer ou recusar uma nota é definitivo na SEFAZ. A ciência é a única
 *   que pode ser trocada depois, e é ela que libera o XML sem tomar partido.
 */

import type { AmbienteFiscal } from "@/lib/fiscal/ambiente";
import { lerPreferencias, type EmissorGuardado } from "@/lib/fiscal/emissor";
import {
  baixarArquivoDaRecebida,
  listarNotasRecebidas,
  manifestarNotaRecebida,
  obterConfiguracoes,
  alterarConfiguracoes,
  obterNotaRecebida,
  sincronizarRecebidas as pedirSincronizacao,
  situacaoDaSincronizacao,
  type ModeloRecebido,
  type SpedyNotaRecebida,
} from "@/lib/spedy/api";
import { ErroDaSpedy } from "@/lib/spedy/cliente";

import { carregarEmissor, spedyDaGestao, spedyDoEmissor, type Admin } from "./base";
import { BUCKET_FISCAL, ErroFiscal } from "./notas";

export type ManifestacaoGuardada = "nenhuma" | "ciencia" | "confirmada" | "desconhecida" | "nao_realizada" | "recusada";

export type RecebidaGuardada = {
  id: string;
  empresa_id: string;
  ambiente: AmbienteFiscal;
  modelo: ModeloRecebido;
  spedy_id: string;
  chave: string | null;
  nsu: number | null;
  status: "autorizada" | "denegada" | "cancelada";
  completa: boolean;
  manifestacao: ManifestacaoGuardada;
  manifestada_em: string | null;
  justificativa: string | null;
  emitente: { nome: string | null; razaoSocial: string | null; documento: string | null } | null;
  valor: number;
  emitida_em: string | null;
  operacao: string | null;
  resumo: Record<string, unknown> | null;
  xml_path: string | null;
  pdf_path: string | null;
  vista_em: string | null;
  avisada_em: string | null;
  created_at: string;
  updated_at: string;
};

export type TipoDeManifestacao = "ciencia" | "confirmada" | "desconhecida" | "nao_realizada";

const STATUS_DA_SPEDY: Record<string, RecebidaGuardada["status"]> = {
  authorized: "autorizada",
  denied: "denegada",
  canceled: "cancelada",
};

const MANIFESTACAO_DA_SPEDY: Record<string, ManifestacaoGuardada> = {
  none: "nenhuma",
  acknowledged: "ciencia",
  confirmed: "confirmada",
  unknown: "desconhecida",
  notPerformed: "nao_realizada",
  rejected: "recusada",
};

export const MANIFESTACAO_PARA_SPEDY: Record<TipoDeManifestacao, "acknowledged" | "confirmed" | "unknown" | "notPerformed"> = {
  ciencia: "acknowledged",
  confirmada: "confirmed",
  desconhecida: "unknown",
  nao_realizada: "notPerformed",
};

/** O que cada resposta significa para quem opera, sem jargão da SEFAZ. */
export const RESPOSTAS_DA_RECEBIDA: Array<{
  chave: TipoDeManifestacao;
  rotulo: string;
  explicacao: string;
  exigeMotivo: boolean;
  definitiva: boolean;
}> = [
  {
    chave: "ciencia",
    rotulo: "Dar ciência",
    explicacao: "Avisa que você viu a nota, sem dizer se concorda. Libera o arquivo completo e pode ser trocada depois.",
    exigeMotivo: false,
    definitiva: false,
  },
  {
    chave: "confirmada",
    rotulo: "Confirmar a operação",
    explicacao: "Você reconhece a compra e confirma que ela aconteceu. Não tem volta.",
    exigeMotivo: false,
    definitiva: true,
  },
  {
    chave: "desconhecida",
    rotulo: "Não reconheço esta nota",
    explicacao: "Para nota indevida ou em nome da empresa sem que ela tenha comprado. Não tem volta.",
    exigeMotivo: true,
    definitiva: true,
  },
  {
    chave: "nao_realizada",
    rotulo: "A operação não aconteceu",
    explicacao: "A compra era para você, mas não se concretizou (mercadoria recusada, por exemplo). Não tem volta.",
    exigeMotivo: true,
    definitiva: true,
  },
];

const numero = (valor: unknown) => {
  const n = Number(valor);
  return Number.isFinite(n) ? n : 0;
};
const texto = (valor: unknown) => (typeof valor === "string" && valor.trim() ? valor.trim() : null);

function emitenteDaSpedy(nota: SpedyNotaRecebida) {
  const emissor = nota.issuer ?? null;
  if (!emissor) return null;
  return {
    nome: texto(emissor.name) ?? texto(emissor.legalName),
    razaoSocial: texto(emissor.legalName),
    documento: texto(emissor.federalTaxNumber),
  };
}

function linhaDaSpedy(nota: SpedyNotaRecebida, modelo: ModeloRecebido, empresaId: string, ambiente: AmbienteFiscal) {
  return {
    empresa_id: empresaId,
    ambiente,
    modelo,
    spedy_id: nota.id,
    chave: texto(nota.accessKey),
    nsu: Number.isFinite(Number(nota.nsu)) ? Number(nota.nsu) : null,
    status: STATUS_DA_SPEDY[String(nota.status ?? "")] ?? "autorizada",
    completa: nota.isComplete === true,
    manifestacao: MANIFESTACAO_DA_SPEDY[String(nota.manifestation?.status ?? "")] ?? "nenhuma",
    manifestada_em: texto(nota.manifestation?.date),
    justificativa: texto(nota.manifestation?.justification),
    emitente: emitenteDaSpedy(nota),
    valor: numero(nota.amount),
    emitida_em: texto(nota.issuedOn) ?? texto(nota.creationTime),
    operacao: texto(nota.operationType),
    resumo: nota as unknown as Record<string, unknown>,
    updated_at: new Date().toISOString(),
  };
}

/** Recebimento ligado na empresa? A resposta vale para a tela e para a rotina. */
export function recebimentoLigado(emissor: EmissorGuardado | null) {
  if (!emissor) return false;
  return lerPreferencias(emissor.preferencias).receberNotas === true;
}

async function emissorPronto(admin: Admin, empresaId: string, ambiente: AmbienteFiscal) {
  const emissor = await carregarEmissor(admin, empresaId, ambiente);
  if (!emissor || !emissor.spedy_empresa_id || !emissor.chave_cifrada) return null;
  return emissor;
}

/**
 * Liga ou desliga a busca automática na SEFAZ. `desde` é o ponto de corte: a
 * SEFAZ só guarda ~90 dias, então datas mais antigas são recusadas por ela.
 */
export async function mudarRecebimento(
  admin: Admin,
  input: { empresaId: string; ambiente: AmbienteFiscal; ligado: boolean; desde?: string | null }
) {
  const emissor = await emissorPronto(admin, input.empresaId, input.ambiente);
  if (!emissor) throw new ErroFiscal("Configure a empresa antes de receber notas.", 409);

  const limite = new Date();
  limite.setUTCDate(limite.getUTCDate() - 85);
  const pedido = input.desde ? new Date(`${input.desde}T00:00:00Z`) : new Date();
  const desde = (Number.isFinite(pedido.getTime()) && pedido > limite ? pedido : limite).toISOString().slice(0, 10);

  /* Ligar o recebimento é configuração da empresa: só a chave da titular mexe ali. */
  const ctx = spedyDaGestao(emissor);
  const atuais = await obterConfiguracoes(ctx, emissor.spedy_empresa_id as string);
  const bloco = { ...((atuais?.productInvoice ?? {}) as Record<string, unknown>) };
  bloco.inbound = input.ligado ? { enabled: true, startDate: desde } : { enabled: false, startDate: null };
  await alterarConfiguracoes(ctx, emissor.spedy_empresa_id as string, { productInvoice: bloco });

  const preferencias = { ...lerPreferencias(emissor.preferencias), receberNotas: input.ligado, receberDesde: input.ligado ? desde : null };
  await admin
    .from("fiscal_emissores")
    .update({ preferencias, updated_at: new Date().toISOString() })
    .eq("id", emissor.id);
  return { ligado: input.ligado, desde: input.ligado ? desde : null };
}

/**
 * Traz da Spedy o que já chegou e guarda aqui. `pedirBusca` manda a Spedy
 * consultar a SEFAZ agora — respeitando o limite dela: se vier 429, devolve
 * quando dá para tentar de novo em vez de estourar na cara de quem clicou.
 */
export async function sincronizarNotasRecebidas(
  admin: Admin,
  input: { empresaId: string; ambiente: AmbienteFiscal; pedirBusca?: boolean; desde?: string | null }
): Promise<{ ligado: boolean; novas: number; atualizadas: number; esperarSegundos: number | null; ultimaBusca: string | null }> {
  const emissor = await emissorPronto(admin, input.empresaId, input.ambiente);
  if (!emissor) return { ligado: false, novas: 0, atualizadas: 0, esperarSegundos: null, ultimaBusca: null };
  if (!recebimentoLigado(emissor)) return { ligado: false, novas: 0, atualizadas: 0, esperarSegundos: null, ultimaBusca: null };

  const ctx = spedyDoEmissor(emissor);
  let esperarSegundos: number | null = null;
  let ultimaBusca: string | null = null;

  if (input.pedirBusca) {
    try {
      await pedirSincronizacao(ctx, "nfe");
    } catch (erro) {
      if (erro instanceof ErroDaSpedy && erro.status === 429) esperarSegundos = 60;
      else throw erro;
    }
  }
  try {
    const situacao = await situacaoDaSincronizacao(ctx, "nfe");
    ultimaBusca = texto(situacao?.lastSyncAt);
    const proxima = texto(situacao?.nextAllowedSyncAt);
    if (proxima) {
      const faltam = Math.ceil((new Date(proxima).getTime() - Date.now()) / 1000);
      if (Number.isFinite(faltam) && faltam > 0) esperarSegundos = faltam;
    }
  } catch {
    /* A situação é informativa: sem ela a listagem continua valendo. */
  }

  const desde = input.desde ?? lerPreferencias(emissor.preferencias).receberDesde ?? null;
  let novas = 0;
  let atualizadas = 0;

  for (const modelo of ["nfe", "nfse"] as ModeloRecebido[]) {
    let cursor: string | null = null;
    for (let pagina = 0; pagina < 10; pagina += 1) {
      let resposta;
      try {
        resposta = await listarNotasRecebidas(ctx, modelo, { cursor, pageSize: 50, initialDate: desde });
      } catch (erro) {
        /* Plano sem o recurso, ou modelo que a conta não assina: segue o outro. */
        if (erro instanceof ErroDaSpedy && (erro.status === 403 || erro.status === 404)) break;
        throw erro;
      }
      const itens = resposta?.items ?? [];
      if (!itens.length) break;

      const { data: existentes } = await admin
        .from("fiscal_notas_recebidas")
        .select("id,spedy_id,completa,manifestacao,status")
        .eq("empresa_id", input.empresaId)
        .eq("ambiente", input.ambiente)
        .in("spedy_id", itens.map((item) => item.id));
      const porId = new Map((existentes ?? []).map((linha) => [String(linha.spedy_id), linha]));

      for (const item of itens) {
        const linha = linhaDaSpedy(item, modelo, input.empresaId, input.ambiente);
        const antiga = porId.get(item.id);
        if (!antiga) {
          const { error } = await admin.from("fiscal_notas_recebidas").insert(linha);
          if (!error) novas += 1;
          continue;
        }
        const mudou = antiga.completa !== linha.completa || antiga.manifestacao !== linha.manifestacao || antiga.status !== linha.status;
        if (!mudou) continue;
        await admin.from("fiscal_notas_recebidas").update(linha).eq("id", antiga.id);
        atualizadas += 1;
      }

      cursor = resposta?.nextCursor ?? null;
      if (!cursor) break;
    }
  }

  return { ligado: true, novas, atualizadas, esperarSegundos, ultimaBusca };
}

export type FiltroDeRecebidas = {
  situacao?: "todas" | "sem_resposta" | "respondidas" | "canceladas";
  busca?: string | null;
  limite?: number;
};

export async function listarRecebidasGuardadas(
  admin: Admin,
  input: { empresaId: string; ambiente: AmbienteFiscal; filtro?: FiltroDeRecebidas }
) {
  const filtro = input.filtro ?? {};
  let consulta = admin
    .from("fiscal_notas_recebidas")
    .select("*")
    .eq("empresa_id", input.empresaId)
    .eq("ambiente", input.ambiente)
    .order("emitida_em", { ascending: false, nullsFirst: false })
    .limit(Math.min(200, Math.max(10, filtro.limite ?? 100)));

  if (filtro.situacao === "sem_resposta") consulta = consulta.eq("manifestacao", "nenhuma").neq("status", "cancelada");
  if (filtro.situacao === "respondidas") consulta = consulta.neq("manifestacao", "nenhuma");
  if (filtro.situacao === "canceladas") consulta = consulta.eq("status", "cancelada");

  const { data, error } = await consulta;
  if (error) throw new ErroFiscal(`Falha ao ler as notas recebidas: ${error.message}`, 500);
  const lista = (data ?? []) as RecebidaGuardada[];
  const busca = String(filtro.busca ?? "").trim().toLowerCase();
  if (!busca) return lista;
  return lista.filter((nota) => {
    const alvo = [nota.emitente?.nome, nota.emitente?.razaoSocial, nota.emitente?.documento, nota.chave].filter(Boolean).join(" ").toLowerCase();
    return alvo.includes(busca);
  });
}

export async function carregarRecebida(admin: Admin, empresaId: string, notaId: string): Promise<RecebidaGuardada> {
  const { data } = await admin.from("fiscal_notas_recebidas").select("*").eq("id", notaId).maybeSingle();
  if (!data || data.empresa_id !== empresaId) throw new ErroFiscal("Nota recebida não encontrada.", 404);
  return data as RecebidaGuardada;
}

/** Responde à SEFAZ. Resposta síncrona: se ela recusar, nada é gravado aqui. */
export async function responderRecebida(
  admin: Admin,
  input: { empresaId: string; ambiente: AmbienteFiscal; notaId: string; tipo: TipoDeManifestacao; motivo?: string | null; usuarioId: string | null }
): Promise<RecebidaGuardada> {
  const nota = await carregarRecebida(admin, input.empresaId, input.notaId);
  if (nota.modelo !== "nfe") throw new ErroFiscal("Só a nota de produto recebida tem resposta ao emissor.", 409);
  const regra = RESPOSTAS_DA_RECEBIDA.find((r) => r.chave === input.tipo);
  if (!regra) throw new ErroFiscal("Resposta desconhecida.", 400);

  const motivo = String(input.motivo ?? "").replace(/\s+/g, " ").trim();
  if (regra.exigeMotivo && (motivo.length < 15 || motivo.length > 255)) {
    throw new ErroFiscal("Escreva o motivo com 15 a 255 letras. Ele vai para a SEFAZ junto com a resposta.", 400);
  }
  if (nota.manifestacao !== "nenhuma" && nota.manifestacao !== "ciencia") {
    throw new ErroFiscal("Esta nota já teve uma resposta definitiva e não dá para mudar.", 409);
  }

  const emissor = await emissorPronto(admin, input.empresaId, input.ambiente);
  if (!emissor) throw new ErroFiscal("Configure a empresa antes de responder.", 409);

  const resposta = await manifestarNotaRecebida(spedyDoEmissor(emissor), nota.spedy_id, {
    status: MANIFESTACAO_PARA_SPEDY[input.tipo],
    justification: regra.exigeMotivo ? motivo : null,
  });

  const agora = new Date().toISOString();
  await admin
    .from("fiscal_notas_recebidas")
    .update({
      manifestacao: MANIFESTACAO_DA_SPEDY[String(resposta?.manifestation?.status ?? "")] ?? input.tipo,
      manifestada_em: texto(resposta?.manifestation?.date) ?? agora,
      manifestada_por: input.usuarioId,
      justificativa: regra.exigeMotivo ? motivo : null,
      completa: resposta?.isComplete === true ? true : nota.completa,
      vista_em: nota.vista_em ?? agora,
      updated_at: agora,
    })
    .eq("id", nota.id);
  return carregarRecebida(admin, input.empresaId, nota.id);
}

function caminhoDaRecebida(nota: RecebidaGuardada, tipo: "pdf" | "xml") {
  const data = new Date(nota.emitida_em ?? nota.created_at);
  const ano = Number.isFinite(data.getTime()) ? data.getUTCFullYear() : new Date().getUTCFullYear();
  const mes = String((Number.isFinite(data.getTime()) ? data.getUTCMonth() : new Date().getUTCMonth()) + 1).padStart(2, "0");
  return `${nota.empresa_id}/${nota.ambiente}/recebidas/${ano}/${mes}/${nota.id}.${tipo}`;
}

/** O arquivo da nota recebida, guardado aqui depois da primeira busca. */
export async function arquivoDaRecebida(
  admin: Admin,
  input: { empresaId: string; ambiente: AmbienteFiscal; notaId: string; tipo: "pdf" | "xml" }
): Promise<{ conteudo: ArrayBuffer; nome: string; tipoMime: string }> {
  const nota = await carregarRecebida(admin, input.empresaId, input.notaId);
  const nome = `${nota.modelo === "nfe" ? "NF-e" : "NFS-e"}-recebida${nota.chave ? `-${nota.chave}` : ""}.${input.tipo}`;
  const tipoMime = input.tipo === "pdf" ? "application/pdf" : "application/xml";

  const guardado = input.tipo === "pdf" ? nota.pdf_path : nota.xml_path;
  if (guardado) {
    const { data } = await admin.storage.from(BUCKET_FISCAL).download(guardado);
    if (data) return { conteudo: await data.arrayBuffer(), nome, tipoMime };
  }
  if (input.tipo === "pdf" && !nota.completa) {
    throw new ErroFiscal("O DANFE só fica disponível depois que você responde à nota e a SEFAZ libera o arquivo completo.", 409);
  }

  const emissor = await emissorPronto(admin, input.empresaId, input.ambiente);
  if (!emissor) throw new ErroFiscal("Configure a empresa antes de baixar.", 409);
  const conteudo = await baixarArquivoDaRecebida(spedyDoEmissor(emissor), nota.modelo, nota.spedy_id, input.tipo);

  if (nota.completa) {
    const caminho = caminhoDaRecebida(nota, input.tipo);
    const envio = await admin.storage
      .from(BUCKET_FISCAL)
      .upload(caminho, new Uint8Array(conteudo), { contentType: tipoMime, upsert: true });
    if (!envio.error) {
      await admin
        .from("fiscal_notas_recebidas")
        .update({
          [input.tipo === "pdf" ? "pdf_path" : "xml_path"]: caminho,
          arquivos_em: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        })
        .eq("id", nota.id);
    }
  }
  return { conteudo, nome, tipoMime };
}

/** Atualiza uma nota recebida a partir da Spedy (usado depois de responder). */
export async function conferirRecebida(admin: Admin, input: { empresaId: string; ambiente: AmbienteFiscal; notaId: string }) {
  const nota = await carregarRecebida(admin, input.empresaId, input.notaId);
  const emissor = await emissorPronto(admin, input.empresaId, input.ambiente);
  if (!emissor) return nota;
  const resposta = await obterNotaRecebida(spedyDoEmissor(emissor), nota.modelo, nota.spedy_id);
  if (!resposta?.id) return nota;
  await admin
    .from("fiscal_notas_recebidas")
    .update(linhaDaSpedy(resposta, nota.modelo, input.empresaId, input.ambiente))
    .eq("id", nota.id);
  return carregarRecebida(admin, input.empresaId, nota.id);
}

export async function marcarRecebidasComoVistas(admin: Admin, input: { empresaId: string; ambiente: AmbienteFiscal; ids?: string[] }) {
  const agora = new Date().toISOString();
  let consulta = admin
    .from("fiscal_notas_recebidas")
    .update({ vista_em: agora, updated_at: agora })
    .eq("empresa_id", input.empresaId)
    .eq("ambiente", input.ambiente)
    .is("vista_em", null);
  if (input.ids?.length) consulta = consulta.in("id", input.ids);
  await consulta;
}

/**
 * O que o sino deve mostrar: nota nova que ainda não virou aviso. Marca na
 * mesma hora para o aviso não repetir em cada aba aberta.
 */
export async function avisosDeRecebidas(admin: Admin, input: { empresaId: string; ambiente: AmbienteFiscal; limite?: number }) {
  const { data } = await admin
    .from("fiscal_notas_recebidas")
    .select("id,modelo,chave,valor,emitente,emitida_em,status,manifestacao,created_at")
    .eq("empresa_id", input.empresaId)
    .eq("ambiente", input.ambiente)
    .is("avisada_em", null)
    .order("created_at", { ascending: false })
    .limit(Math.min(20, Math.max(1, input.limite ?? 10)));
  const avisos = data ?? [];
  if (avisos.length) {
    await admin
      .from("fiscal_notas_recebidas")
      .update({ avisada_em: new Date().toISOString() })
      .in("id", avisos.map((aviso) => aviso.id));
  }
  return avisos.map((aviso) => ({
    id: String(aviso.id),
    modelo: aviso.modelo as ModeloRecebido,
    chave: (aviso.chave as string | null) ?? null,
    valor: numero(aviso.valor),
    emitente: ((aviso.emitente as RecebidaGuardada["emitente"])?.nome ?? "Fornecedor") as string,
    emitidaEm: (aviso.emitida_em as string | null) ?? (aviso.created_at as string),
    status: aviso.status as RecebidaGuardada["status"],
    respondida: aviso.manifestacao !== "nenhuma",
  }));
}

/** Quantas notas recebidas ainda não foram abertas — o número do menu. */
export async function contarRecebidasNovas(admin: Admin, input: { empresaId: string; ambiente: AmbienteFiscal }) {
  const { count } = await admin
    .from("fiscal_notas_recebidas")
    .select("id", { count: "exact", head: true })
    .eq("empresa_id", input.empresaId)
    .eq("ambiente", input.ambiente)
    .is("vista_em", null);
  return count ?? 0;
}
