import { BadRequestException, NotFoundException } from '@nestjs/common';
import type { PrismaService } from 'src/prisma/prisma.service';

/**
 * Cambia la fecha de emisión de un recibo de cobro y la arrastra a lo que se
 * registró con ella: el asiento contable (fecha y período) y los movimientos de
 * tesorería que generó el cobro. Así el libro mayor, el extracto de la cuenta y
 * los reportes de cobranza siguen diciendo lo mismo.
 *
 * Antes la acción "Modificar fecha" cambiaba `fecha_registro`, un campo que solo
 * se ve en el renglón "reg." de la lista: la fecha que usan los reportes, el
 * asiento y la tesorería quedaba igual. Y los recibos multi ni la tenían.
 *
 * Se rechaza (sin tocar nada) cuando el cambio dejaría algo inconsistente:
 *  - recibo anulado, fecha futura o igual a la actual, motivo vacío;
 *  - comisión del cobrador ya liquidada sobre ese recibo;
 *  - recibo ya incluido en una rendición de cobranza;
 *  - movimiento de tesorería ya conciliado con el extracto del banco;
 *  - con contabilidad: fecha actual o nueva en un período cerrado/bloqueado (tenga
 *    o no asiento el recibo); asiento sin período abierto para la fecha nueva.
 *
 * No recalcula intereses moratorios ni descuentos: quedan como se cobraron.
 */

export const ORIGENES_TES_RECIBO = ['recibos_multi', 'recibos_cobro'];

// Fecha calendario en la zona del servidor (Paraguay): fecha_emision es un
// instante y un recibo de las 22 hs ya es el día siguiente en UTC.
const ymd = (d: Date) =>
  `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;

export interface CambioFechaReciboResultado {
  numero_recibo: string | null;
  fecha_anterior: string;
  fecha_nueva: string;
  asientos: { numero_anterior: number; numero: number; periodo_anterior: string; periodo_nuevo: string }[];
  movimientos_tesoreria: number;
}

export async function cambiarFechaEmisionRecibo(
  prisma: PrismaService,
  params: { reciboId: string; empresaId: string; fecha: string; motivo: string; hoy?: Date },
): Promise<CambioFechaReciboResultado> {
  const { reciboId, empresaId, fecha } = params;
  const motivo = params.motivo?.trim() ?? '';

  if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha ?? '')) {
    throw new BadRequestException('La fecha tiene que tener el formato AAAA-MM-DD');
  }
  if (motivo.length < 5) {
    throw new BadRequestException('Indicá el motivo del cambio de fecha (mínimo 5 caracteres)');
  }

  const recibo = await prisma.recibos_cobro.findFirst({
    where: { id: reciboId, empresa_id: empresaId },
    select: { id: true, numero_recibo: true, estado: true, fecha_emision: true, rendicion_id: true },
  });
  if (!recibo) throw new NotFoundException('Recibo no encontrado');
  if (recibo.estado?.toLowerCase() === 'anulado') {
    throw new BadRequestException('No se puede cambiar la fecha de un recibo anulado');
  }

  // Mediodía local, igual que al crear el recibo con una fecha elegida.
  const nueva = new Date(`${fecha}T12:00:00`);
  const anterior = recibo.fecha_emision;
  const hoy = ymd(params.hoy ?? new Date());
  if (fecha > hoy) throw new BadRequestException('La fecha de emisión no puede ser futura');
  if (fecha === ymd(anterior)) throw new BadRequestException('El recibo ya tiene esa fecha');

  if (recibo.rendicion_id) {
    throw new BadRequestException(
      'El recibo ya está incluido en una rendición de cobranza; cambiar su fecha descuadraría la rendición',
    );
  }

  const comisionLiquidada = await prisma.comisiones.count({
    where: { recibo_id: reciboId, estado: 'liquidada' },
  });
  if (comisionLiquidada > 0) {
    throw new BadRequestException('La comisión del cobrador sobre este recibo ya fue liquidada');
  }

  const movimientos = await prisma.tes_movimientos.findMany({
    where: {
      empresa_id: empresaId,
      origen_id: reciboId,
      origen_tipo: { in: ORIGENES_TES_RECIBO },
      estado: { not: 'ANULADO' },
    },
    select: { id: true },
  });
  if (movimientos.length) {
    const conciliados = await prisma.tes_extracto_lineas.count({
      where: { movimiento_id: { in: movimientos.map((m) => m.id) } },
    });
    if (conciliados > 0) {
      throw new BadRequestException(
        'El cobro ya está conciliado con el extracto del banco; desconcilialo antes de cambiar la fecha',
      );
    }
  }

  // Períodos contables (solo si la empresa usa contabilidad, es decir, tiene
  // períodos): ni la fecha actual ni la nueva pueden caer en un mes cerrado,
  // tenga o no asiento el recibo. Mover un cobro dentro o fuera de un mes
  // cerrado cambiaría reportes que ya se dieron por cerrados.
  const periodoDe = (f: string) =>
    prisma.cont_periodos.findFirst({
      where: { empresa_id: empresaId, fecha_inicio: { lte: new Date(`${f}T00:00:00Z`) }, fecha_fin: { gte: new Date(`${f}T00:00:00Z`) } },
      select: { mes: true, anio: true, estado: true },
    });
  for (const [cual, f] of [['actual', ymd(anterior)], ['nueva', fecha]] as const) {
    const p = await periodoDe(f);
    if (p && !['ABIERTO', 'AJUSTE'].includes(p.estado)) {
      throw new BadRequestException(
        `La fecha ${cual} (${f}) cae en el período contable ${p.mes}/${p.anio}, que está ${p.estado}`,
      );
    }
  }

  // Asientos confirmados del recibo: cambian de fecha y, si cae en otro mes, de período.
  const documentos = await prisma.cont_documentos.findMany({
    where: { empresa_id: empresaId, origen_id: reciboId, origen_tipo: { in: ORIGENES_TES_RECIBO }, estado: 'CONFIRMADO' },
    select: { id: true },
  });
  const asientos = documentos.length
    ? await prisma.cont_asientos.findMany({
        where: { empresa_id: empresaId, documento_id: { in: documentos.map((d) => d.id) }, estado: 'CONFIRMADO' },
        select: { id: true, numero: true, periodo: { select: { id: true, mes: true, anio: true, estado: true } } },
      })
    : [];

  let periodoNuevo: { id: string; mes: number; anio: number } | null = null;
  if (asientos.length) {
    for (const a of asientos) {
      if (a.periodo && !['ABIERTO', 'AJUSTE'].includes(a.periodo.estado)) {
        throw new BadRequestException(
          `El asiento N° ${a.numero} está en el período ${a.periodo.mes}/${a.periodo.anio}, que está ${a.periodo.estado}`,
        );
      }
    }
    periodoNuevo = await prisma.cont_periodos.findFirst({
      where: {
        empresa_id: empresaId,
        estado: { in: ['ABIERTO', 'AJUSTE'] },
        fecha_inicio: { lte: new Date(`${fecha}T00:00:00Z`) },
        fecha_fin: { gte: new Date(`${fecha}T00:00:00Z`) },
      },
      select: { id: true, mes: true, anio: true },
    });
    if (!periodoNuevo) {
      throw new BadRequestException(`No hay un período contable abierto para el ${fecha}`);
    }
  }

  const renumerados = new Map<string, number>();
  await prisma.$transaction(async (tx) => {
    await tx.recibos_cobro.update({ where: { id: reciboId }, data: { fecha_emision: nueva } });
    if (movimientos.length) {
      await tx.tes_movimientos.updateMany({
        where: { id: { in: movimientos.map((m) => m.id) } },
        data: { fecha: new Date(`${fecha}T00:00:00Z`) },
      });
    }
    if (asientos.length && periodoNuevo) {
      const destino = periodoNuevo;
      for (const a of asientos) {
        // El número de asiento es único por período: si pasa a otro mes, toma el
        // siguiente libre de ese mes (el anterior queda en la auditoría).
        let numero = a.numero;
        if (a.periodo?.id !== destino.id) {
          const ultimo = await tx.cont_asientos.findFirst({
            where: { empresa_id: empresaId, periodo_id: destino.id },
            orderBy: { numero: 'desc' },
            select: { numero: true },
          });
          numero = (ultimo?.numero ?? 0) + 1;
        }
        await tx.cont_asientos.update({
          where: { id: a.id },
          data: { fecha: new Date(`${fecha}T00:00:00Z`), periodo_id: destino.id, numero, updated_at: new Date() },
        });
        renumerados.set(a.id, numero);
      }
    }
  });

  return {
    numero_recibo: recibo.numero_recibo,
    fecha_anterior: ymd(anterior),
    fecha_nueva: fecha,
    asientos: asientos.map((a) => ({
      numero_anterior: a.numero,
      numero: renumerados.get(a.id) ?? a.numero,
      periodo_anterior: a.periodo ? `${a.periodo.mes}/${a.periodo.anio}` : '',
      periodo_nuevo: periodoNuevo ? `${periodoNuevo.mes}/${periodoNuevo.anio}` : '',
    })),
    movimientos_tesoreria: movimientos.length,
  };
}
