/**
 * Qué hace una NC con su factura origen en el momento de emitirse.
 *
 * - ESTRICTO: se aplica contra las cuotas pendientes de la factura origen.
 * - FLEXIBLE: queda como crédito del cliente para aplicar desde un recibo,
 *   SALVO que cubra todo el saldo de su factura. Una NC de anulación total no
 *   tiene otro destino lógico que su propia factura: dejarla "flotando" como
 *   crédito solo hace que la factura anulada siga apareciendo en la cuenta
 *   corriente. En ese caso se aplica al emitir, y si sobra, el sobrante sí
 *   queda como crédito flexible (que es donde el modo tiene sentido).
 *
 * En los dos modos, lo aplicado a cuotas se descuenta de `rec_saldo_disponible`
 * de la NC: es lo que mira el recibo para ofrecerla. Antes ESTRICTO lo dejaba en
 * NULL (= su total) y una NC parcial se podía descontar dos veces, primero de la
 * factura al emitir y después del efectivo en el recibo.
 */

export type ModoAplicacionNc = 'ESTRICTO' | 'FLEXIBLE';

export type MotivoAplicacion = 'ESTRICTO' | 'FLEXIBLE_CUBRE_SALDO' | 'FLEXIBLE_CREDITO';

export interface DecisionAplicacion {
  /** Aplicar la NC contra las cuotas de la factura origen al emitir. */
  aplicarAhora: boolean;
  motivo: MotivoAplicacion;
  /** Monto que queda como crédito disponible para aplicar desde un recibo. */
  sobranteCredito: number;
}

/** Diferencia máxima (en la moneda) que se considera redondeo, no saldo real. */
const TOLERANCIA = 1;

export function decidirAplicacionAlEmitir(params: {
  modo: ModoAplicacionNc;
  montoNc: number;
  saldoFactura: number;
}): DecisionAplicacion {
  const montoNc = Math.max(0, params.montoNc);
  const saldo = Math.max(0, params.saldoFactura);

  if (params.modo === 'ESTRICTO') {
    // Lo que no entra en la factura (ya tenía cobros, o está cobrada) queda como
    // crédito: no hay otro lugar donde ponerlo.
    return { aplicarAhora: saldo > 0, motivo: 'ESTRICTO', sobranteCredito: Math.max(0, montoNc - saldo) };
  }

  const cubreSaldo = saldo > 0 && montoNc >= saldo - TOLERANCIA;
  if (!cubreSaldo) {
    return { aplicarAhora: false, motivo: 'FLEXIBLE_CREDITO', sobranteCredito: montoNc };
  }
  return {
    aplicarAhora: true,
    motivo: 'FLEXIBLE_CUBRE_SALDO',
    sobranteCredito: Math.max(0, montoNc - saldo),
  };
}

/** Lo que queda por cobrar de una cuota (forma mínima, tolera Decimal de Prisma). */
interface CuotaConSaldo {
  saldo_pendiente: unknown;
  dmoncuota: unknown;
}

/**
 * Saldo pendiente de cobro de un conjunto de cuotas no pagadas. Una cuota sin
 * `saldo_pendiente` cargado (creada antes de que existiera la columna) nunca se
 * cobró, así que vale su monto completo. Es la misma regla que usa la emisión de
 * la NC al aplicar contra las cuotas: la usan los dos lados para que la pantalla
 * explique exactamente lo que va a pasar.
 */
export function saldoPendienteDeCuotas(cuotas: CuotaConSaldo[]): number {
  return cuotas.reduce((acc, c) => acc + Math.max(0, saldoDeCuota(c)), 0);
}

export function saldoDeCuota(c: CuotaConSaldo): number {
  const pendiente = Number(c.saldo_pendiente);
  return pendiente > 0 ? pendiente : Number(c.dmoncuota) || 0;
}

/** Lo mínimo de la transacción de Prisma que usa la aplicación al emitir. */
interface TxAplicacion {
  factura_cuotas: {
    findMany(args: unknown): Promise<Array<{ id: string; saldo_pendiente: unknown; dmoncuota: unknown }>>;
    update(args: unknown): Promise<unknown>;
  };
  nc_aplicaciones_cuota: { create(args: unknown): Promise<unknown> };
  nota_credito_cab: { update(args: unknown): Promise<unknown> };
}

/** Lo mínimo de la transacción que usa la reversión. */
interface TxReversion {
  factura_cuotas: {
    findUnique(args: unknown): Promise<{ id: string; saldo_pendiente: unknown } | null>;
    update(args: unknown): Promise<unknown>;
  };
  nc_aplicaciones_cuota: {
    findMany(args: unknown): Promise<Array<{ factura_cuota_id: string; monto_aplicado: unknown }>>;
    deleteMany(args: unknown): Promise<unknown>;
  };
}

/**
 * Aplica la NC recién emitida contra las cuotas pendientes de su factura origen
 * (FIFO por número de cuota) cuando corresponde, dentro de la transacción de la
 * emisión. Cada cuota tocada queda registrada en `nc_aplicaciones_cuota`, que es
 * lo que usa la anulación de la NC para revertirla — vale igual para ESTRICTO y
 * para FLEXIBLE auto-aplicada.
 */
export async function aplicarNcAlEmitir(
  tx: TxAplicacion,
  params: { empresaId: string; notaCreditoId: string; facturaId: string; modo: ModoAplicacionNc; montoNc: number },
): Promise<DecisionAplicacion> {
  const cuotas = await tx.factura_cuotas.findMany({
    where: { factura_cab_id: params.facturaId, estado: { notIn: ['pagado', 'Pagado'] } },
    orderBy: { nro_cuota: 'asc' },
  });
  const decision = decidirAplicacionAlEmitir({
    modo: params.modo,
    montoNc: params.montoNc,
    saldoFactura: saldoPendienteDeCuotas(cuotas),
  });
  if (!decision.aplicarAhora) return decision;

  let restante = Math.max(0, Number(params.montoNc) || 0);
  for (const cuota of cuotas) {
    if (restante <= 0) break;
    const saldo = saldoDeCuota(cuota);
    if (saldo <= 0) continue;

    const aplicar = Math.min(restante, saldo);
    const nuevoSaldo = saldo - aplicar;
    await tx.factura_cuotas.update({
      where: { id: cuota.id },
      data: { saldo_pendiente: nuevoSaldo, estado: nuevoSaldo <= 0.009 ? 'pagado' : 'pendiente', updated_at: new Date() },
    });
    await tx.nc_aplicaciones_cuota.create({
      data: {
        empresa_id: params.empresaId,
        nota_credito_cab_id: params.notaCreditoId,
        factura_cuota_id: cuota.id,
        monto_aplicado: aplicar,
      },
    });
    restante -= aplicar;
  }

  // El recibo ofrece la NC según rec_saldo_disponible (NULL = su total). Ya
  // aplicada, lo único disponible es lo que no entró en las cuotas: sin esto se
  // podría volver a usar desde un recibo y acreditar dos veces lo mismo.
  await tx.nota_credito_cab.update({
    where: { id: params.notaCreditoId },
    data: { rec_saldo_disponible: Math.max(0, restante) },
  });
  return decision;
}

/**
 * Deshace lo que `aplicarNcAlEmitir` descontó de las cuotas (anulación de la NC
 * o rechazo de SIFEN). Devuelve el total repuesto. Quien llama decide qué hacer
 * con `rec_saldo_disponible` y si corresponde bloquear por cobros posteriores.
 */
export async function revertirNcDeCuotas(tx: TxReversion, notaCreditoId: string): Promise<number> {
  const aplicaciones = await tx.nc_aplicaciones_cuota.findMany({ where: { nota_credito_cab_id: notaCreditoId } });
  let repuesto = 0;
  for (const apl of aplicaciones) {
    const cuota = await tx.factura_cuotas.findUnique({ where: { id: apl.factura_cuota_id } });
    if (!cuota) continue;
    const saldoActual = Math.max(0, Number(cuota.saldo_pendiente) || 0);
    const monto = Number(apl.monto_aplicado) || 0;
    await tx.factura_cuotas.update({
      where: { id: cuota.id },
      data: { saldo_pendiente: saldoActual + monto, estado: 'pendiente', updated_at: new Date() },
    });
    repuesto += monto;
  }
  if (aplicaciones.length > 0) {
    await tx.nc_aplicaciones_cuota.deleteMany({ where: { nota_credito_cab_id: notaCreditoId } });
  }
  return repuesto;
}
