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

/** TTL por defecto de la reserva pre-pago, en minutos. */
export const RESERVA_TTL_MINUTOS_DEFAULT = 15;

export interface LineaReserva {
  productoId: string;
  cantidad: number;
}

/**
 * Fase 7.5 — Reserva de stock pre-pago.
 *
 * Antes, el stock se validaba en el checkout pero se reservaba recién al
 * confirmar el pago. En esa ventana (segundos con pago online, horas con
 * transferencia) el mismo stock podía venderse por el POS o por otro pedido:
 * oversell.
 *
 * Ahora, al crear la sesión de checkout se incrementa
 * `stock_deposito.cantidad_reservada` y se registra la reserva con un TTL.
 *
 * Ciclo de vida:
 *  - checkout            → `reservar()`
 *  - pago confirmado     → `liberar()` justo antes de `PedidosService.confirmar()`,
 *                          que vuelve a reservar el mismo stock a nombre del
 *                          pedido ERP. Sin esto la reserva quedaría duplicada.
 *  - cancelado / expirado→ `liberar()` (el stock vuelve a estar disponible)
 *  - vencidas            → el cron llama a `expirarVencidas()`
 */
@Injectable()
export class EcommerceStockReservaService {
  private readonly logger = new Logger(EcommerceStockReservaService.name);

  constructor(private readonly prisma: PrismaService) {}

  /**
   * Reserva stock para una sesión de checkout. Atómico: dentro de la
   * transacción se re-verifica el disponible con `FOR UPDATE`, de modo que dos
   * checkouts simultáneos por la última unidad no puedan reservarla ambos.
   *
   * Devuelve los productos que no se pudieron reservar (sin stock). Si la lista
   * vuelve vacía, la reserva fue total.
   *
   * `depositoId` es obligatorio: sin depósito no se puede reservar de forma
   * precisa. Si la config del ecommerce no lo define, el llamador debe omitir
   * la reserva (se degrada al comportamiento anterior).
   */
  async reservar(args: {
    empresaId: string;
    sessionId: string;
    depositoId: string;
    lineas: LineaReserva[];
    ttlMinutos?: number;
  }): Promise<{ ok: boolean; sinStock: string[] }> {
    const { empresaId, sessionId, depositoId, lineas } = args;
    const ttl = args.ttlMinutos ?? RESERVA_TTL_MINUTOS_DEFAULT;
    const expiraAt = new Date(Date.now() + ttl * 60_000);
    const sinStock: string[] = [];

    await this.prisma.$transaction(async (tx) => {
      for (const l of lineas) {
        if (!l.productoId || !(l.cantidad > 0)) continue;

        // Lock de la fila de stock: serializa checkouts concurrentes del mismo producto.
        const rows = await tx.$queryRaw<{ disponible: Prisma.Decimal }[]>`
          SELECT (cantidad_disponible - cantidad_reservada) AS disponible
          FROM stock_deposito
          WHERE deposito_id = ${depositoId}::uuid AND producto_id = ${l.productoId}::uuid
          FOR UPDATE
        `;
        const disponible = Number(rows[0]?.disponible ?? 0);

        if (disponible < l.cantidad) {
          sinStock.push(l.productoId);
          continue;
        }

        await tx.$executeRaw`
          UPDATE stock_deposito
          SET cantidad_reservada = cantidad_reservada + ${l.cantidad},
              updated_at = NOW()
          WHERE deposito_id = ${depositoId}::uuid AND producto_id = ${l.productoId}::uuid
        `;

        await tx.ecommerce_stock_reserva.create({
          data: {
            empresa_id: empresaId,
            session_id: sessionId,
            producto_id: l.productoId,
            deposito_id: depositoId,
            cantidad: new Prisma.Decimal(l.cantidad),
            expira_at: expiraAt,
          },
        });
      }
    });

    if (sinStock.length) {
      // Reserva parcial: liberamos lo tomado para no dejar stock trabado por una
      // sesión que el llamador va a rechazar.
      await this.liberar(sessionId);
      return { ok: false, sinStock };
    }

    this.logger.log(`Sesión ${sessionId}: ${lineas.length} línea(s) reservadas por ${ttl} min.`);
    return { ok: true, sinStock: [] };
  }

  /**
   * Libera las reservas activas de una sesión, devolviendo el stock al
   * disponible. Idempotente: las ya liberadas se ignoran.
   */
  async liberar(sessionId: string): Promise<number> {
    return this.prisma.$transaction(async (tx) => {
      const activas = await tx.ecommerce_stock_reserva.findMany({
        where: { session_id: sessionId, liberada: false },
        select: { id: true, producto_id: true, deposito_id: true, cantidad: true },
      });
      if (!activas.length) return 0;

      for (const r of activas) {
        // GREATEST evita que un descuadre previo deje la reserva en negativo.
        await tx.$executeRaw`
          UPDATE stock_deposito
          SET cantidad_reservada = GREATEST(0, cantidad_reservada - ${r.cantidad}),
              updated_at = NOW()
          WHERE deposito_id = ${r.deposito_id}::uuid AND producto_id = ${r.producto_id}::uuid
        `;
      }

      await tx.ecommerce_stock_reserva.updateMany({
        where: { id: { in: activas.map((r) => r.id) } },
        data: { liberada: true, liberada_at: new Date() },
      });

      return activas.length;
    });
  }

  /**
   * Expira las sesiones que siguen en `pendiente_pago` pasado el TTL: las marca
   * `expirado` y libera su stock. Lo invoca el cron.
   */
  async expirarVencidas(): Promise<{ sesiones: number; reservas: number }> {
    const vencidas = await this.prisma.ecommerce_stock_reserva.findMany({
      where: { liberada: false, expira_at: { lt: new Date() } },
      select: { session_id: true },
      distinct: ['session_id'],
    });
    if (!vencidas.length) return { sesiones: 0, reservas: 0 };

    let reservas = 0;
    let sesiones = 0;

    for (const { session_id } of vencidas) {
      const s = await this.prisma.ecommerce_checkout_session.findUnique({
        where: { id: session_id },
        select: { id: true, estado: true, codigo: true },
      });

      // Si ya no está esperando el pago (se pagó, se canceló, etc.) sólo
      // limpiamos la reserva: el stock ya lo maneja el pedido ERP.
      if (s && s.estado === 'pendiente_pago') {
        await this.prisma.ecommerce_checkout_session.update({
          where: { id: s.id },
          data: {
            estado: 'expirado',
            notas: 'Expirada por falta de pago dentro del plazo de reserva.',
            updated_at: new Date(),
          },
        });
        sesiones += 1;
        this.logger.log(`Sesión ${s.codigo} expirada por TTL: stock liberado.`);
      }

      reservas += await this.liberar(session_id);
    }

    return { sesiones, reservas };
  }
}
