import { BadRequestException, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';

type Tx = Prisma.TransactionClient;

type ItemCalc = {
  id: string;
  cantidad: number;
  fob_total_usd: number;
  peso_unit_kg: number;
  cbm_unit: number;
};

const CRITERIOS_VALIDOS = new Set(['FOB', 'PESO', 'CBM', 'CANTIDAD', 'IGUAL']);
const CRITERIO_LABEL: Record<string, string> = {
  FOB: 'FOB total',
  PESO: 'peso (kg)',
  CBM: 'volumen (CBM)',
  CANTIDAD: 'cantidad',
  IGUAL: 'partes iguales',
};

@Injectable()
export class ImportacionesCosteoService {
  async recalcular(tx: Tx, empresaId: string, embarqueId: string) {
    const [itemsRaw, componentes] = await Promise.all([
      tx.imp_embarque_items.findMany({
        where: { empresa_id: empresaId, embarque_id: embarqueId, activo: true },
        orderBy: [{ orden: 'asc' }, { created_at: 'asc' }],
      }),
      // Se excluyen los conceptos marcados como "no afecta costo" (hoy, el
      // anticipo de IRE): son impuesto a la renta, no costo de la mercadería.
      // Siguen existiendo como componente y se contabilizan, pero no entran al
      // prorrateo ni al costo unitario del ítem.
      tx.imp_componentes_costo.findMany({
        where: {
          empresa_id: empresaId,
          embarque_id: embarqueId,
          activo: true,
          estado: 'CONFIRMADO',
          concepto: { afecta_costo: true },
        },
      }),
    ]);

    const items: ItemCalc[] = itemsRaw.map((i: any) => ({
      id: i.id,
      cantidad: Number(i.cantidad ?? 0),
      fob_total_usd: Number(i.fob_total_usd ?? 0),
      peso_unit_kg: Number(i.peso_unit_kg ?? 0),
      cbm_unit: Number(i.cbm_unit ?? 0),
    }));

    const totalFob = items.reduce((acc, item) => acc + item.fob_total_usd, 0);
    const compartidos = componentes.filter((c: any) => !c.es_directo);

    if (compartidos.length > 0 && items.length === 0) {
      throw new BadRequestException('No se pueden prorratear costos compartidos sin ítems en el embarque');
    }

    // Pre-cálculo y validación de bases por criterio.
    const factoresPorCriterio = new Map<string, Map<string, number>>();
    for (const componente of compartidos) {
      const criterio = (componente.criterio_prorrateo || 'FOB').toUpperCase();
      if (!CRITERIOS_VALIDOS.has(criterio)) {
        throw new BadRequestException(`Criterio de prorrateo inválido: ${criterio}`);
      }
      if (factoresPorCriterio.has(criterio)) continue;
      factoresPorCriterio.set(criterio, this.calcularFactores(items, criterio));
    }

    const compartidosPorItem = new Map<string, number>();
    for (const item of items) compartidosPorItem.set(item.id, 0);

    for (const componente of compartidos) {
      const criterio = (componente.criterio_prorrateo || 'FOB').toUpperCase();
      const factores = factoresPorCriterio.get(criterio) ?? new Map<string, number>();
      const importeGs = Number(componente.importe_gs ?? 0);
      for (const item of items) {
        const factor = factores.get(item.id) ?? 0;
        compartidosPorItem.set(item.id, (compartidosPorItem.get(item.id) || 0) + importeGs * factor);
      }
    }

    let costoTotalEmbarque = 0;
    for (const item of items) {
      const costoDirecto = componentes
        .filter((c: any) => c.es_directo && c.item_id === item.id)
        .reduce((acc: number, c: any) => acc + Number(c.importe_gs ?? 0), 0);
      const costoCompartido = compartidosPorItem.get(item.id) ?? 0;
      const costoTotal = costoDirecto + costoCompartido;
      const costoUnitario = item.cantidad > 0 ? costoTotal / item.cantidad : 0;

      costoTotalEmbarque += costoTotal;

      await tx.imp_embarque_items.update({
        where: { id: item.id },
        data: {
          costo_directo_gs: this.round(costoDirecto, 4),
          costo_compartido_gs: this.round(costoCompartido, 4),
          costo_total_gs: this.round(costoTotal, 4),
          costo_unitario_gs: this.round(costoUnitario, 4),
          updated_at: new Date(),
        },
      });
    }

    await tx.imp_embarques.update({
      where: { id: embarqueId },
      data: {
        fob_total_usd: this.round(totalFob, 4),
        costo_total_gs: this.round(costoTotalEmbarque, 4),
        updated_at: new Date(),
      },
    });
  }

  private calcularFactores(items: ItemCalc[], criterio: string): Map<string, number> {
    const bases = items.map((item) => ({ id: item.id, base: this.baseSegunCriterio(item, criterio) }));
    const total = bases.reduce((acc, b) => acc + b.base, 0);
    if (total <= 0) {
      throw new BadRequestException(
        `No se puede prorratear por ${CRITERIO_LABEL[criterio]}: ningún ítem tiene valor cargado para este criterio`,
      );
    }
    const factores = new Map<string, number>();
    for (const { id, base } of bases) {
      factores.set(id, base / total);
    }
    return factores;
  }

  private baseSegunCriterio(item: ItemCalc, criterio: string): number {
    switch (criterio) {
      case 'FOB':
        return item.fob_total_usd;
      case 'PESO':
        return item.peso_unit_kg * item.cantidad;
      case 'CBM':
        return item.cbm_unit * item.cantidad;
      case 'CANTIDAD':
        return item.cantidad;
      case 'IGUAL':
        return 1;
      default:
        return 0;
    }
  }

  private round(value: number, decimals = 4) {
    const factor = 10 ** decimals;
    return Math.round((Number(value) || 0) * factor) / factor;
  }
}
