import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';

/**
 * Deshace los efectos de una remisión: repone el stock que descontó (sólo la
 * remisión sin factura lo mueve), libera `cantidad_aplicada_remision` en las
 * facturas y la deja Anulada.
 *
 * Lo usan la anulación manual (`NotaRemisionService.anular`) y el sync de
 * eventos SIFEN al aprobarse un ECAN/EINU. Vive aparte para que
 * `MiddlewareSifenModule` no dependa de `NotaRemisionModule` (que ya depende de él).
 */
@Injectable()
export class ReversionRemisionService {
  constructor(private readonly prisma: PrismaService) {}

  /**
   * Idempotente: marca Anulado sólo si no lo estaba, y sólo en ese caso revierte.
   * Una remisión anulada a mano y cancelada después en SIFEN no repone el stock dos veces.
   */
  async revertirRemision(remisionId: string, empresaId: string): Promise<{ revertida: boolean; stockRepuesto: number }> {
    return this.prisma.$transaction(async (tx) => {
      const { count } = await tx.nota_remision_cab.updateMany({
        where: {
          id: remisionId,
          empresa_id: empresaId,
          OR: [{ estado: null }, { estado: { not: 'Anulado' } }],
        },
        data: { estado: 'Anulado', updated_at: new Date() },
      });
      if (count === 0) return { revertida: false, stockRepuesto: 0 };

      const rem = await tx.nota_remision_cab.findUnique({
        where: { id: remisionId },
        select: { dest: true, dpunexp: true, dnumdoc: true },
      });
      const numero = rem ? `${rem.dest}-${rem.dpunexp}-${rem.dnumdoc}` : undefined;

      await this.liberarAplicacionesFactura(tx, remisionId);
      const stockRepuesto = await this.reponerStock(tx, remisionId, numero);
      return { revertida: true, stockRepuesto };
    });
  }

  /**
   * Revierte `cantidad_aplicada_remision` en facturas. Un solo findMany con `in`
   * y los decrementos agregados por factura_det_id → 1 UPDATE por factura_det
   * (equivalente con GREATEST(0,...) porque sólo hay decrementos).
   */
  private async liberarAplicacionesFactura(tx: any, remisionId: string): Promise<void> {
    const detalles = await tx.nota_remision_det.findMany({
      where: { nota_remision_cab_id: remisionId },
      select: { id: true },
    });
    const detIds = detalles.map((d: { id: string }) => d.id);
    if (detIds.length === 0) return;

    const aplicaciones = await tx.nota_remision_det_aplicacion.findMany({
      where: { nota_remision_det_id: { in: detIds } },
    });
    const decrementoPorFacturaDet = new Map<string, number>();
    for (const aplic of aplicaciones) {
      decrementoPorFacturaDet.set(
        aplic.factura_det_id,
        (decrementoPorFacturaDet.get(aplic.factura_det_id) ?? 0) + Number(aplic.cantidad_aplicada),
      );
    }
    for (const [facturaDetId, decremento] of decrementoPorFacturaDet) {
      await tx.$executeRaw`
        UPDATE factura_det
        SET cantidad_aplicada_remision = GREATEST(0, cantidad_aplicada_remision - ${decremento})
        WHERE id = ${facturaDetId}::uuid`;
    }
  }

  /** Repone el stock descontado por la remisión. Retorna cuántos ítems repuso. */
  private async reponerStock(tx: any, remisionId: string, numeroRemision?: string): Promise<number> {
    const movs = await tx.movimientos_inventario.findMany({
      where: { documento_origen: 'nota_remision', documento_id: remisionId },
    });
    if (movs.length === 0) return 0;
    const tipoMov = await this.tipoMovimiento(tx, 'ENTRADA_REM_ANULADA', 'Reposición por anulación de remisión', 1);

    for (const mov of movs) {
      const stockActual = await tx.stock_deposito.findUnique({
        where: { deposito_id_producto_id: { deposito_id: mov.deposito_origen_id, producto_id: mov.producto_id } },
      });
      const nueva = (stockActual ? Number(stockActual.cantidad_disponible) : 0) + Number(mov.cantidad);
      await tx.stock_deposito.upsert({
        where: { deposito_id_producto_id: { deposito_id: mov.deposito_origen_id, producto_id: mov.producto_id } },
        create: { deposito_id: mov.deposito_origen_id, producto_id: mov.producto_id, cantidad_disponible: Number(mov.cantidad) },
        update: { cantidad_disponible: nueva, updated_at: new Date() },
      });
      await tx.movimientos_inventario.create({
        data: {
          deposito_origen_id: mov.deposito_origen_id,
          deposito_destino_id: mov.deposito_origen_id,
          producto_id: mov.producto_id,
          tipo_movimiento_id: tipoMov.id,
          cantidad: Number(mov.cantidad),
          precio_unitario: mov.precio_unitario,
          documento_origen: 'nota_remision_anulacion',
          documento_id: remisionId,
          observaciones: numeroRemision ? `Reposición por anulación de remisión ${numeroRemision}` : 'Reposición por anulación de remisión',
        },
      });
    }
    return movs.length;
  }

  /** Tipo de movimiento de inventario (find-or-create). */
  private async tipoMovimiento(tx: any, codigo: string, descripcion: string, afecta: number): Promise<{ id: string }> {
    let tipo = await tx.tipo_movimiento_inventario.findFirst({ where: { codigo } });
    if (!tipo) {
      tipo = await tx.tipo_movimiento_inventario.upsert({
        where: { codigo },
        update: {},
        create: { codigo, descripcion, afecta_stock: afecta },
      });
    }
    return tipo;
  }
}
