import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { AuditService } from '../audit/audit.service';
import {
  pendienteDeCargo,
  pendientePorCuota,
  repartirCobro,
  validarCarga,
  type RechazoCarga,
} from './interes-manual';

const MENSAJES: Record<RechazoCarga, string> = {
  MONTO_INVALIDO: 'El monto del interés debe ser mayor a cero',
  CUOTA_PAGADA: 'La cuota ya está pagada o anulada: no se le puede cargar interés',
  MOTIVO_REQUERIDO: 'Indicá el motivo del interés',
};

/** Lo que el cobro necesita saber del interés pendiente de las cuotas que va a cobrar. */
export interface PendienteInteresManual {
  /** Pendiente por cuota, solo de los cargos atados a una cuota. */
  porCuota: Map<string, number>;
  /** Pendiente de los cargos de la cuenta (sin cuota), por factura. */
  porCuenta: Map<string, number>;
  total: number;
}

@Injectable()
export class InteresManualService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
  ) {}

  /**
   * Interés pendiente que entra en un cobro: el de las cuotas que se están
   * cobrando, más el de la cuenta a la que pertenecen esas cuotas.
   */
  async pendienteParaCobro(
    empresaId: string,
    cuotaIds: string[],
    facturaIds: string[],
  ): Promise<PendienteInteresManual> {
    const vacio: PendienteInteresManual = { porCuota: new Map(), porCuenta: new Map(), total: 0 };
    if (cuotaIds.length === 0 && facturaIds.length === 0) return vacio;

    const cargos = await this.prisma.cob_interes_manual.findMany({
      where: {
        empresa_id: empresaId,
        estado: 'pendiente',
        OR: [
          { factura_cuota_id: { in: cuotaIds } },
          { factura_cuota_id: null, factura_cab_id: { in: facturaIds } },
        ],
      },
      select: { factura_cuota_id: true, factura_cab_id: true, monto: true, monto_cobrado: true, estado: true },
    });

    const porCuota = pendientePorCuota(cargos as any);
    const porCuenta = new Map<string, number>();
    for (const cargo of cargos) {
      if (cargo.factura_cuota_id) continue;
      const pendiente = pendienteDeCargo(cargo as any);
      if (pendiente <= 0) continue;
      porCuenta.set(cargo.factura_cab_id, (porCuenta.get(cargo.factura_cab_id) ?? 0) + pendiente);
    }
    const total =
      [...porCuota.values()].reduce((a, b) => a + b, 0) + [...porCuenta.values()].reduce((a, b) => a + b, 0);
    return { porCuota, porCuenta, total };
  }

  /** Cargos de una factura, para mostrarlos en Cuentas por Cobrar. */
  async listarPorFactura(empresaId: string, facturaId: string) {
    return this.prisma.cob_interes_manual.findMany({
      where: { empresa_id: empresaId, factura_cab_id: facturaId, estado: { not: 'anulado' } },
      orderBy: [{ created_at: 'asc' }],
      select: {
        id: true,
        factura_cuota_id: true,
        monto: true,
        monto_cobrado: true,
        motivo: true,
        estado: true,
        cobrado_at: true,
        created_at: true,
        factura_cuota: { select: { nro_cuota: true, dvenccuo: true } },
      },
    });
  }

  /**
   * Carga un interés. Sin `factura_cuota_id` es de la cuenta: se cobra en el
   * próximo cobro de esa factura, entero o de a partes.
   */
  async crear(
    empresaId: string,
    usuarioId: string | null,
    dto: { factura_cab_id?: string; factura_cuota_id?: string; monto: number; motivo?: string },
  ) {
    let facturaId = dto.factura_cab_id ?? null;
    let estadoCuota: string | null = null;
    let nroCuota: number | null = null;

    if (dto.factura_cuota_id) {
      const cuota = await this.prisma.factura_cuotas.findFirst({
        where: { id: dto.factura_cuota_id, factura_cab: { empresa_id: empresaId } },
        select: { id: true, estado: true, nro_cuota: true, factura_cab_id: true },
      });
      if (!cuota) throw new NotFoundException('Cuota no encontrada');
      facturaId = cuota.factura_cab_id;
      estadoCuota = cuota.estado;
      nroCuota = cuota.nro_cuota;
    } else {
      if (!facturaId) throw new BadRequestException('Indicá la cuenta o la cuota a la que va el interés');
      const factura = await this.prisma.factura_cab.findFirst({
        where: { id: facturaId, empresa_id: empresaId },
        select: { id: true },
      });
      if (!factura) throw new NotFoundException('Cuenta no encontrada');
    }

    const rechazo = validarCarga({ monto: dto.monto, motivo: dto.motivo, estadoCuota });
    if (rechazo) throw new BadRequestException(MENSAJES[rechazo]);

    const cargo = await this.prisma.cob_interes_manual.create({
      data: {
        empresa_id: empresaId,
        factura_cab_id: facturaId!,
        factura_cuota_id: dto.factura_cuota_id ?? null,
        monto: new Prisma.Decimal(dto.monto),
        motivo: dto.motivo?.trim() || null,
        creado_por: usuarioId,
      },
    });

    await this.audit.log({
      empresa_id: empresaId,
      user_id: usuarioId ?? undefined,
      action: 'CREATE',
      entity_type: 'cob_interes_manual',
      entity_id: cargo.id,
      descripcion: `Interés manual ${dto.monto} ${nroCuota ? `sobre la cuota ${nroCuota}` : 'sobre la cuenta'}`,
      new_value: { factura_cab_id: facturaId, monto: dto.monto, motivo: dto.motivo ?? null },
    });
    return cargo;
  }

  /** Anula un cargo mientras no se haya cobrado nada de él. */
  async anular(empresaId: string, id: string, usuarioId: string | null, motivo?: string) {
    const cargo = await this.prisma.cob_interes_manual.findFirst({ where: { id, empresa_id: empresaId } });
    if (!cargo) throw new NotFoundException('Interés no encontrado');
    if (cargo.estado === 'anulado') return cargo;
    if (Number(cargo.monto_cobrado) > 0) {
      throw new BadRequestException(
        'Ese interés ya se cobró en parte o en total: anulá el recibo que lo cobró para revertirlo',
      );
    }

    const actualizado = await this.prisma.cob_interes_manual.update({
      where: { id },
      data: {
        estado: 'anulado',
        anulado_por: usuarioId,
        anulado_at: new Date(),
        motivo_anulacion: motivo?.trim() || null,
        updated_at: new Date(),
      },
    });
    await this.audit.log({
      empresa_id: empresaId,
      user_id: usuarioId ?? undefined,
      action: 'DELETE',
      entity_type: 'cob_interes_manual',
      entity_id: id,
      descripcion: `Interés manual anulado (${cargo.monto})`,
      old_value: { monto: Number(cargo.monto), motivo_anulacion: motivo ?? null },
    });
    return actualizado;
  }

  /**
   * Cobra interés dentro de la TX del cobro. `monto` null = todo lo pendiente de
   * esas cuotas y de sus cuentas; un monto menor se reparte del cargo más viejo
   * al más nuevo y el resto queda pendiente. Devuelve lo efectivamente cobrado.
   */
  async cobrar(
    tx: Prisma.TransactionClient,
    empresaId: string,
    params: { cuotaIds: string[]; facturaIds: string[]; reciboId: string; monto?: number | null },
  ): Promise<{ total: number; porCuota: Map<string, number> }> {
    const vacio = { total: 0, porCuota: new Map<string, number>() };
    if (params.cuotaIds.length === 0 && params.facturaIds.length === 0) return vacio;

    const cargos = await tx.cob_interes_manual.findMany({
      where: {
        empresa_id: empresaId,
        estado: 'pendiente',
        OR: [
          { factura_cuota_id: { in: params.cuotaIds } },
          { factura_cuota_id: null, factura_cab_id: { in: params.facturaIds } },
        ],
      },
      orderBy: [{ created_at: 'asc' }],
      select: { id: true, factura_cuota_id: true, monto: true, monto_cobrado: true, estado: true },
    });

    const reparto = repartirCobro(cargos as any, params.monto ?? null);
    const porCuota = new Map<string, number>();
    let cobrado = 0;
    for (const item of reparto) {
      const cargo = cargos.find((c) => c.id === item.id)!;
      const nuevoCobrado = Number(cargo.monto_cobrado) + item.monto;
      const saldado = nuevoCobrado >= Number(cargo.monto) - 0.009;
      await tx.cob_interes_manual.update({
        where: { id: item.id },
        data: {
          monto_cobrado: new Prisma.Decimal(nuevoCobrado),
          estado: saldado ? 'cobrado' : 'pendiente',
          cobrado_at: saldado ? new Date() : null,
          updated_at: new Date(),
        },
      });
      await tx.cob_interes_manual_aplicacion.create({
        data: {
          empresa_id: empresaId,
          interes_id: item.id,
          recibo_cobro_id: params.reciboId,
          monto: new Prisma.Decimal(item.monto),
        },
      });
      cobrado += item.monto;
      if (cargo.factura_cuota_id) {
        porCuota.set(cargo.factura_cuota_id, (porCuota.get(cargo.factura_cuota_id) ?? 0) + item.monto);
      }
    }
    return { total: cobrado, porCuota };
  }

  /** Devuelve a pendiente exactamente lo que cobró un recibo que se anula. */
  async revertirDeRecibo(tx: Prisma.TransactionClient, empresaId: string, reciboId: string): Promise<number> {
    const aplicaciones = await tx.cob_interes_manual_aplicacion.findMany({
      where: { empresa_id: empresaId, recibo_cobro_id: reciboId },
    });
    let revertido = 0;
    for (const aplicacion of aplicaciones) {
      const cargo = await tx.cob_interes_manual.findUnique({ where: { id: aplicacion.interes_id } });
      if (!cargo) continue;
      const nuevoCobrado = Math.max(0, Number(cargo.monto_cobrado) - Number(aplicacion.monto));
      await tx.cob_interes_manual.update({
        where: { id: cargo.id },
        data: {
          monto_cobrado: new Prisma.Decimal(nuevoCobrado),
          // Un cargo anulado no vuelve solo: solo se reabre lo que estaba cobrado.
          estado: cargo.estado === 'anulado' ? 'anulado' : 'pendiente',
          cobrado_at: null,
          updated_at: new Date(),
        },
      });
      revertido += Number(aplicacion.monto);
    }
    if (aplicaciones.length > 0) {
      await tx.cob_interes_manual_aplicacion.deleteMany({
        where: { empresa_id: empresaId, recibo_cobro_id: reciboId },
      });
    }
    return revertido;
  }

  /**
   * Antes de regenerar las cuotas de una cuenta: los cargos de esas cuotas pasan
   * a la cuenta. Si no, la regeneración no puede borrar las cuotas (quedan
   * referenciadas) y además el cargo perdería su razón de ser.
   *
   * `soloPendientes` despega únicamente los cargos que todavía deben algo. Lo usa
   * el cobro cuando una cuota se salda debiendo interés: ahí las cuotas NO se
   * borran, así que despegar los cargos ya cobrados perdería su trazabilidad.
   */
  async liberarDeCuotas(
    tx: Prisma.TransactionClient,
    empresaId: string,
    cuotaIds: string[],
    opciones?: { soloPendientes?: boolean },
  ): Promise<number> {
    if (cuotaIds.length === 0) return 0;
    const res = await tx.cob_interes_manual.updateMany({
      where: {
        empresa_id: empresaId,
        factura_cuota_id: { in: cuotaIds },
        ...(opciones?.soloPendientes ? { estado: 'pendiente' } : {}),
      },
      data: { factura_cuota_id: null, updated_at: new Date() },
    });
    return res.count;
  }
}
