import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';

/** Una factura del funcionario pendiente de cobro, lista para descontar. */
export interface FacturaPendienteFuncionario {
  cuenta_cobrar_id: string;
  factura_id: string | null;
  numero: string | null;
  fecha_emision: Date | null;
  monto_total: number;
  saldo_pendiente: number;
  /** Cuánto se propone descontar en esta liquidación. */
  monto_sugerido: number;
}

/**
 * Consumo de funcionarios: lo que el empleado compra en la empresa y se le
 * descuenta del sueldo.
 *
 * El circuito completo es:
 *   1. Se le factura al funcionario como a cualquier cliente (cobro diferido),
 *      lo que deja la deuda en DEUDORES POR VENTAS.
 *   2. La liquidación le propone esas facturas como descuento (este servicio).
 *   3. Al cerrar, el descuento acredita la cuenta puente y se emite un recibo
 *      que la debita contra DEUDORES POR VENTAS, cancelando la deuda.
 *
 * Nada de esto corre si la empresa no lo habilitó en `config_rrhh`.
 */
@Injectable()
export class ConsumoFuncionarioService {
  private readonly logger = new Logger(ConsumoFuncionarioService.name);

  constructor(private readonly prisma: PrismaService) {}

  /** ¿La empresa habilitó el cobro de consumo por planilla? */
  async estaHabilitado(empresaId: string): Promise<boolean> {
    const config = await this.prisma.config_rrhh.findUnique({
      where: { empresa_id: empresaId },
      select: { consumo_funcionario_habilitado: true },
    });
    return config?.consumo_funcionario_habilitado ?? false;
  }

  /** Tope mensual por funcionario. 0 = sin tope. */
  async topeMensual(empresaId: string): Promise<number> {
    const config = await this.prisma.config_rrhh.findUnique({
      where: { empresa_id: empresaId },
      select: { consumo_funcionario_tope_mensual: true },
    });
    return Number(config?.consumo_funcionario_tope_mensual ?? 0);
  }

  /**
   * Facturas del funcionario pendientes de cobro, de la más vieja a la más
   * nueva (se cancela primero lo que más tiempo lleva abierto).
   *
   * `monto_sugerido` reparte el tope mensual entre las facturas, en ese orden:
   * si el tope es 50.000 y debe 30.000 + 40.000, sugiere 30.000 y 20.000. Sin
   * tope, sugiere el saldo completo de cada una.
   *
   * Importante: devuelve TODAS las facturas pendientes del funcionario, no solo
   * las de consumo. En la práctica un empleado puede tener también compras
   * grandes como cliente común (vimos una de 18 millones), que a ritmo de
   * descuento por sueldo no terminarían nunca. Por eso esto es una propuesta:
   * quien liquida decide cuáles deja y cuáles saca.
   */
  async facturasPendientes(
    empleadoId: string,
    empresaId: string,
  ): Promise<FacturaPendienteFuncionario[]> {
    const empleado = await this.prisma.rrhh_empleados.findFirst({
      where: { id: empleadoId, empresa_id: empresaId },
      select: { id: true, cliente_id: true },
    });
    if (!empleado) throw new NotFoundException('Empleado no encontrado');
    if (!empleado.cliente_id) return [];

    const cuentas = await this.prisma.cuentas_cobrar.findMany({
      where: {
        empresa_id: empresaId,
        cliente_id: empleado.cliente_id,
        saldo_pendiente: { gt: 0 },
        // 'anulada' queda afuera: la factura ya no se cobra.
        estado: { notIn: ['anulada', 'ANULADA'] },
        // RRHH la marcó para no cobrarla por planilla (ni en la desvinculación):
        // sigue como cuenta por cobrar común.
        excluir_descuento_planilla: false,
      },
      select: {
        id: true,
        factura_venta_id: true,
        fecha_emision: true,
        monto_total: true,
        saldo_pendiente: true,
        factura_cab: {
          select: { dest: true, dpunexp: true, dnumdoc: true, total_notas_credito: true },
        },
      },
      orderBy: [{ fecha_emision: 'asc' }, { created_at: 'asc' }],
    });

    // Las notas de crédito NO están descontadas de `saldo_pendiente`: el módulo
    // de NC trabaja sobre factura_cuotas y deja el ajuste para el momento de
    // leer. Si tomáramos el saldo crudo, a un funcionario con NC le
    // cobraríamos de más por planilla. Se prorratea la NC de cada factura
    // entre sus cuotas, de la más vieja a la más nueva, igual que hace
    // CobrosService.distribuirNcEnCuotas.
    const ncRestantePorFactura = new Map<string, number>();
    for (const c of cuentas) {
      const facturaId = c.factura_venta_id;
      if (!facturaId || ncRestantePorFactura.has(facturaId)) continue;
      ncRestantePorFactura.set(facturaId, Number(c.factura_cab?.total_notas_credito ?? 0));
    }

    const tope = await this.topeMensual(empresaId);
    let restante = tope > 0 ? tope : Number.POSITIVE_INFINITY;

    return cuentas.map((c) => {
      const saldoBruto = Number(c.saldo_pendiente ?? 0);
      const facturaId = c.factura_venta_id;
      const ncRestante = facturaId ? (ncRestantePorFactura.get(facturaId) ?? 0) : 0;
      const ncAplicada = Math.min(ncRestante, saldoBruto);
      if (facturaId) ncRestantePorFactura.set(facturaId, ncRestante - ncAplicada);

      const saldo = saldoBruto - ncAplicada;
      const sugerido = Math.max(0, Math.min(saldo, restante));
      restante -= sugerido;

      const f = c.factura_cab;
      return {
        cuenta_cobrar_id: c.id,
        factura_id: c.factura_venta_id,
        numero: f ? [f.dest, f.dpunexp, f.dnumdoc].filter(Boolean).join('-') : null,
        fecha_emision: c.fecha_emision,
        monto_total: Number(c.monto_total ?? 0),
        saldo_pendiente: saldo,
        monto_sugerido: sugerido,
      };
    });
  }

  /**
   * Saldo total pendiente del funcionario. Lo usa la desvinculación, que
   * descuenta todo lo que quede en el finiquito.
   */
  async saldoTotal(empleadoId: string, empresaId: string): Promise<number> {
    const facturas = await this.facturasPendientes(empleadoId, empresaId);
    return facturas.reduce((acc, f) => acc + f.saldo_pendiente, 0);
  }

  /**
   * Nota de crédito que al funcionario ya no se le puede descontar de ninguna
   * factura abierta: hay que devolvérsela.
   *
   * Pasa cuando devuelve mercadería que ya se le cobró por planilla. La NC
   * reduce la deuda, pero la deuda ya estaba saldada, así que queda a su favor.
   * Se calcula como el excedente de NC sobre el saldo que todavía puede
   * absorber — si la NC entra entera en facturas abiertas, no hay nada que
   * devolver, simplemente se le descuenta menos.
   */
  async saldoAFavor(empleadoId: string, empresaId: string): Promise<number> {
    const empleado = await this.prisma.rrhh_empleados.findFirst({
      where: { id: empleadoId, empresa_id: empresaId },
      select: { cliente_id: true },
    });
    if (!empleado?.cliente_id) return 0;

    // NC total por factura, contra el saldo bruto que esa factura todavía tiene.
    const cuentas = await this.prisma.cuentas_cobrar.findMany({
      where: {
        empresa_id: empresaId,
        cliente_id: empleado.cliente_id,
        estado: { notIn: ['anulada', 'ANULADA'] },
      },
      select: {
        factura_venta_id: true,
        saldo_pendiente: true,
        factura_cab: { select: { total_notas_credito: true } },
      },
    });

    const brutoPorFactura = new Map<string, number>();
    const ncPorFactura = new Map<string, number>();
    for (const c of cuentas) {
      const id = c.factura_venta_id;
      if (!id) continue;
      brutoPorFactura.set(id, (brutoPorFactura.get(id) ?? 0) + Number(c.saldo_pendiente ?? 0));
      ncPorFactura.set(id, Number(c.factura_cab?.total_notas_credito ?? 0));
    }

    let aFavor = 0;
    for (const [facturaId, nc] of ncPorFactura) {
      const bruto = brutoPorFactura.get(facturaId) ?? 0;
      if (nc > bruto) aFavor += nc - bruto;
    }
    return Math.round((aFavor + Number.EPSILON) * 100) / 100;
  }
}
