import { BadRequestException, Injectable, NotFoundException, Logger } from '@nestjs/common';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from '../prisma/prisma.service';
import { alcanceSucursalUsuario, filtroSucursalDeposito } from 'src/utils/alcance-sucursal';
import { AjustarStockDto, CreateStockDto, ReservarStockDto, TransferirStockDto } from './dto/create-stock.dto';

/** Costo y venta de un producto en un depósito (ver `preciosPorProducto`). */
export interface PreciosStock {
  costo: { valor: number | null; moneda: string; fuente: 'lote' | 'producto' };
  venta: { valor: number | null; moneda: string; fuente: 'lista' | 'producto'; lista: string | null };
}

@Injectable()
export class StockService {
  private readonly logger = new Logger(StockService.name);
  constructor(
    private readonly prisma: PrismaService,
    private readonly auditService: AuditService,
  ) {}

  async findByDeposito(deposito_id: string, empresa_id: string) {
    const filas = await this.prisma.stock_deposito.findMany({
      // El depósito llega por la URL: sin atarlo a la empresa del token,
      // cualquier usuario autenticado podía leer el stock de otro cliente.
      where: { deposito_id, deposito: { empresa_id } },
      include: { producto: true, deposito: true },
      orderBy: { producto: { descripcion: 'asc' } },
    });
    const precios = await this.preciosPorProducto(deposito_id, empresa_id, filas);
    return filas.map((f) => ({ ...f, precios: precios.get(f.producto_id) }));
  }

  /**
   * Costo y precio de venta de cada producto del depósito, para la pantalla de
   * Stock. Tres consultas para todo el depósito (no una por fila):
   *  - costo: promedio ponderado de los lotes con stock en ESTE depósito si el
   *    producto maneja lotes (lo que se pagó de verdad); si no, `precio_costo`.
   *  - venta: `precio_base` de la lista general de la empresa (activa, mayor
   *    prioridad); si el producto no está en ella, `productos.precio`.
   * Ni el producto ni el lote tienen moneda propia: se informa la principal de
   * la empresa (PYG si no hay). La lista sí trae la suya.
   */
  private async preciosPorProducto(
    deposito_id: string,
    empresa_id: string,
    filas: Array<{ producto_id: string; producto: { maneja_lote?: boolean | null; precio_costo?: unknown; precio?: unknown } | null }>,
  ): Promise<Map<string, PreciosStock>> {
    const productoIds = filas.map((f) => f.producto_id);
    const conLotes = filas.filter((f) => f.producto?.maneja_lote).map((f) => f.producto_id);

    const [lotes, lista, monedaEmpresa] = await Promise.all([
      conLotes.length
        ? this.prisma.stock_lote.findMany({
            where: {
              deposito_id,
              active: true,
              cantidad_disponible: { gt: 0 },
              lotes_producto: { producto_id: { in: conLotes }, activo: true },
            },
            select: { cantidad_disponible: true, lotes_producto: { select: { producto_id: true, costo_unitario: true } } },
          })
        : Promise.resolve([]),
      this.prisma.lista_precios.findFirst({
        where: { empresa_id, activa: true, tipo_aplicacion: 'general' },
        orderBy: [{ prioridad: 'desc' }, { fecha_inicio: 'desc' }],
        select: {
          id: true,
          nombre: true,
          moneda: true,
          lista_precios_productos: { where: { producto_id: { in: productoIds } }, select: { producto_id: true, precio_base: true } },
        },
      }),
      this.prisma.empresa_monedas.findFirst({
        where: { empresa_id, es_principal: true },
        select: { moneda: { select: { codigo: true } } },
      }),
    ]);

    const monedaBase = monedaEmpresa?.moneda?.codigo || 'PYG';
    const acumulado = new Map<string, { costo: number; cantidad: number }>();
    for (const l of lotes) {
      const pid = l.lotes_producto.producto_id;
      const cant = Number(l.cantidad_disponible);
      const prev = acumulado.get(pid) ?? { costo: 0, cantidad: 0 };
      acumulado.set(pid, { costo: prev.costo + Number(l.lotes_producto.costo_unitario) * cant, cantidad: prev.cantidad + cant });
    }
    const precioLista = new Map((lista?.lista_precios_productos ?? []).map((p) => [p.producto_id, Number(p.precio_base)]));

    const num = (v: unknown) => (v == null ? null : Number(v));
    const out = new Map<string, PreciosStock>();
    for (const f of filas) {
      const lote = acumulado.get(f.producto_id);
      const costo =
        lote && lote.cantidad > 0
          ? { valor: Math.round((lote.costo / lote.cantidad) * 100) / 100, moneda: monedaBase, fuente: 'lote' as const }
          : { valor: num(f.producto?.precio_costo), moneda: monedaBase, fuente: 'producto' as const };
      const enLista = precioLista.get(f.producto_id);
      const venta =
        lista && enLista != null
          ? { valor: enLista, moneda: lista.moneda || monedaBase, fuente: 'lista' as const, lista: lista.nombre }
          : { valor: num(f.producto?.precio), moneda: monedaBase, fuente: 'producto' as const, lista: null };
      out.set(f.producto_id, { costo, venta });
    }
    return out;
  }

  async findByProducto(producto_id: string, empresa_id: string) {
    return this.prisma.stock_deposito.findMany({
      where: {
        producto_id,
        deposito: { empresa_id },
      },
      include: { deposito: { include: { sucursal: true } } },
    });
  }

  async findOne(deposito_id: string, producto_id: string, empresa_id: string) {
    // findFirst y no findUnique porque la clave compuesta no admite filtrar
    // por la relación, y sin ese filtro se consultaba stock de otra empresa.
    return this.prisma.stock_deposito.findFirst({
      where: { deposito_id, producto_id, deposito: { empresa_id } },
      include: { producto: true, deposito: true },
    });
  }

  async updateLimites(
    id: string,
    data: { stock_minimo?: number; stock_maximo?: number; ubicacion?: string },
    user_id?: string,
    empresa_id?: string,
  ) {
    const existing = await this.prisma.stock_deposito.findUnique({
      where: { id },
      include: { producto: true, deposito: true },
    });
    if (!existing) throw new Error('Stock no encontrado');

    const updated = await this.prisma.stock_deposito.update({
      where: { id },
      data: {
        ...(data.stock_minimo !== undefined && { stock_minimo: data.stock_minimo }),
        ...(data.stock_maximo !== undefined && { stock_maximo: data.stock_maximo }),
        ...(data.ubicacion !== undefined && { ubicacion: data.ubicacion }),
        updated_at: new Date(),
      },
      include: { producto: true, deposito: true },
    });

    this.logger.log(`Stock límites actualizados: ${id}`, 'StockService');
    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'UPDATE',
      entity_type: 'stock_deposito',
      entity_id: id,
      descripcion: `Límites de stock actualizados | Producto: ${existing.producto?.descripcion} | Mín: ${data.stock_minimo} | Máx: ${data.stock_maximo}`,
      old_value: { stock_minimo: existing.stock_minimo, stock_maximo: existing.stock_maximo, ubicacion: existing.ubicacion },
      new_value: { stock_minimo: data.stock_minimo, stock_maximo: data.stock_maximo, ubicacion: data.ubicacion },
    });

    return { message: 'Límites actualizados', data: updated };
  }

  async upsertStock(dto: CreateStockDto, user_id?: string, empresa_id?: string) {
    const stock = await this.prisma.stock_deposito.upsert({
      where: { deposito_id_producto_id: { deposito_id: dto.deposito_id, producto_id: dto.producto_id } },
      create: {
        deposito_id: dto.deposito_id,
        producto_id: dto.producto_id,
        cantidad_disponible: dto.cantidad_disponible,
        stock_minimo: dto.stock_minimo || 0,
        stock_maximo: dto.stock_maximo,
        ubicacion: dto.ubicacion,
      },
      update: {
        cantidad_disponible: dto.cantidad_disponible,
        stock_minimo: dto.stock_minimo,
        stock_maximo: dto.stock_maximo,
        ubicacion: dto.ubicacion,
        updated_at: new Date(),
      },
      include: { producto: true, deposito: true },
    });
    this.logger.log(`Stock upsert: producto ${dto.producto_id} depósito ${dto.deposito_id}`, 'StockService');
    await this.auditService.log({
      empresa_id: empresa_id ?? (stock.deposito as any)?.empresa_id ?? undefined,
      user_id,
      action: 'UPDATE',
      entity_type: 'stock_deposito',
      entity_id: dto.producto_id,
      descripcion: `Stock actualizado | Producto: ${dto.producto_id} | Depósito: ${dto.deposito_id} | Cantidad: ${dto.cantidad_disponible}`,
      new_value: {
        deposito_id: dto.deposito_id,
        producto_id: dto.producto_id,
        cantidad_disponible: dto.cantidad_disponible,
        stock_minimo: dto.stock_minimo,
        stock_maximo: dto.stock_maximo,
      },
    });
    return { message: 'Stock actualizado exitosamente', data: stock };
  }

  async ajustarStock(dto: AjustarStockDto, usuario_id: string, tipo_codigo: string, empresa_id?: string) {
    // Buscar el tipo de movimiento por código
    const tipoMovimiento = await this.prisma.tipo_movimiento_inventario.findUnique({
      where: { codigo: tipo_codigo },
    });

    if (!tipoMovimiento) {
      throw new BadRequestException(`Tipo de movimiento '${tipo_codigo}' no encontrado`);
    }

    // Obtener empresa_id desde el depósito si no viene como parámetro
    const deposito = await this.prisma.depositos.findUnique({
      where: { id: dto.deposito_id },
      select: { empresa_id: true },
    });
    const resolvedEmpresaId = empresa_id ?? deposito?.empresa_id ?? undefined;

    const stock = await this.prisma.stock_deposito.findUnique({
      where: { deposito_id_producto_id: { deposito_id: dto.deposito_id, producto_id: dto.producto_id } },
    });

    const cantidadActual = stock ? Number(stock.cantidad_disponible) : 0;
    const nuevaCantidad = cantidadActual + dto.cantidad;

    if (nuevaCantidad < 0) {
      throw new BadRequestException('Stock insuficiente para realizar el ajuste');
    }

    const result = await this.prisma.$transaction(async (tx) => {
      const stockActualizado = await tx.stock_deposito.upsert({
        where: { deposito_id_producto_id: { deposito_id: dto.deposito_id, producto_id: dto.producto_id } },
        create: {
          deposito_id: dto.deposito_id,
          producto_id: dto.producto_id,
          cantidad_disponible: dto.cantidad,
        },
        update: {
          cantidad_disponible: nuevaCantidad,
          updated_at: new Date(),
        },
      });

      await tx.movimientos_inventario.create({
        data: {
          deposito_origen_id: dto.deposito_id,
          producto_id: dto.producto_id,
          tipo_movimiento_id: tipoMovimiento.id,
          cantidad: Math.abs(dto.cantidad),
          documento_origen: 'ajuste_manual',
          observaciones: dto.observaciones,
          usuario_id,
        },
      });

      return stockActualizado;
    });

    this.logger.log(
      `Stock ajustado: producto ${dto.producto_id} depósito ${dto.deposito_id} cantidad ${dto.cantidad}`,
      'StockService',
    );
    await this.auditService.log({
      empresa_id: resolvedEmpresaId,
      user_id: usuario_id,
      action: tipo_codigo === 'ENTRADA' ? 'CREATE' : 'UPDATE',
      entity_type: 'stock_ajuste',
      entity_id: dto.producto_id,
      descripcion: `Ajuste de stock (${tipo_codigo}) | Producto: ${dto.producto_id} | Depósito: ${dto.deposito_id} | Cantidad: ${dto.cantidad}`,
      old_value: { cantidad_disponible: cantidadActual },
      new_value: { cantidad_disponible: nuevaCantidad, observaciones: dto.observaciones },
    });
    return { message: 'Stock ajustado exitosamente', data: result };
  }

  async reservarStock(dto: ReservarStockDto, user_id?: string, empresa_id?: string) {
    // Escribe sobre el stock, así que el depósito tiene que ser de la empresa
    // del token: con los dos UUID se podía tocar el inventario de otro cliente.
    const stock = await this.prisma.stock_deposito.findFirst({
      where: {
        deposito_id: dto.deposito_id,
        producto_id: dto.producto_id,
        ...(empresa_id ? { deposito: { empresa_id } } : {}),
      },
    });

    if (!stock) throw new NotFoundException('Stock no encontrado');

    const disponible = Number(stock.cantidad_disponible) - Number(stock.cantidad_reservada);
    if (disponible < dto.cantidad) {
      throw new BadRequestException(`Stock disponible insuficiente. Disponible: ${disponible}`);
    }

    const result = await this.prisma.stock_deposito.update({
      where: { deposito_id_producto_id: { deposito_id: dto.deposito_id, producto_id: dto.producto_id } },
      data: {
        cantidad_reservada: { increment: dto.cantidad },
        updated_at: new Date(),
      },
    });

    this.logger.log(
      `Stock reservado: producto ${dto.producto_id} depósito ${dto.deposito_id} cantidad ${dto.cantidad}`,
      'StockService',
    );
    await this.auditService.log({
      user_id,
      action: 'UPDATE',
      entity_type: 'stock_reserva',
      entity_id: dto.producto_id,
      descripcion: `Stock reservado | Producto: ${dto.producto_id} | Depósito: ${dto.deposito_id} | Cantidad: ${dto.cantidad}`,
      old_value: { cantidad_reservada: Number(stock.cantidad_reservada) },
      new_value: { cantidad_reservada: Number(stock.cantidad_reservada) + dto.cantidad },
    });
    return { message: 'Stock reservado exitosamente', data: result };
  }

  async liberarReserva(dto: ReservarStockDto, user_id?: string, empresa_id?: string) {
    // Escribe sobre el stock, así que el depósito tiene que ser de la empresa
    // del token: con los dos UUID se podía tocar el inventario de otro cliente.
    const stock = await this.prisma.stock_deposito.findFirst({
      where: {
        deposito_id: dto.deposito_id,
        producto_id: dto.producto_id,
        ...(empresa_id ? { deposito: { empresa_id } } : {}),
      },
    });

    if (!stock) throw new NotFoundException('Stock no encontrado');

    const reservadoActual = Number(stock.cantidad_reservada);
    const cantidadLiberar = Math.min(dto.cantidad, reservadoActual);

    const result = await this.prisma.stock_deposito.update({
      where: { deposito_id_producto_id: { deposito_id: dto.deposito_id, producto_id: dto.producto_id } },
      data: {
        cantidad_reservada: { decrement: cantidadLiberar },
        updated_at: new Date(),
      },
    });

    this.logger.log(
      `Reserva liberada: producto ${dto.producto_id} depósito ${dto.deposito_id} cantidad ${cantidadLiberar}`,
      'StockService',
    );
    await this.auditService.log({
      user_id,
      action: 'UPDATE',
      entity_type: 'stock_reserva',
      entity_id: dto.producto_id,
      descripcion: `Reserva liberada | Producto: ${dto.producto_id} | Depósito: ${dto.deposito_id} | Cantidad: ${cantidadLiberar}`,
      old_value: { cantidad_reservada: reservadoActual },
      new_value: { cantidad_reservada: reservadoActual - cantidadLiberar },
    });
    return { message: 'Reserva liberada exitosamente', data: result };
  }

  async transferirStock(dto: TransferirStockDto, usuario_id: string, tipo_codigo: string, empresa_id?: string) {
    if (dto.deposito_origen_id === dto.deposito_destino_id) {
      throw new BadRequestException('El depósito origen y destino no pueden ser el mismo');
    }

    // Buscar el tipo de movimiento por código
    const tipoMovimiento = await this.prisma.tipo_movimiento_inventario.findUnique({
      where: { codigo: tipo_codigo },
    });

    if (!tipoMovimiento) {
      throw new BadRequestException(`Tipo de movimiento '${tipo_codigo}' no encontrado`);
    }

    const stockOrigen = await this.prisma.stock_deposito.findUnique({
      where: { deposito_id_producto_id: { deposito_id: dto.deposito_origen_id, producto_id: dto.producto_id } },
    });

    if (!stockOrigen) throw new NotFoundException('Stock en depósito origen no encontrado');

    const disponible = Number(stockOrigen.cantidad_disponible) - Number(stockOrigen.cantidad_reservada);
    if (disponible < dto.cantidad) {
      throw new BadRequestException(`Stock disponible insuficiente. Disponible: ${disponible}`);
    }

    const result = await this.prisma.$transaction(async (tx) => {
      // Reducir stock en origen
      await tx.stock_deposito.update({
        where: { deposito_id_producto_id: { deposito_id: dto.deposito_origen_id, producto_id: dto.producto_id } },
        data: {
          cantidad_disponible: { decrement: dto.cantidad },
          updated_at: new Date(),
        },
      });

      // Aumentar o crear stock en destino
      await tx.stock_deposito.upsert({
        where: { deposito_id_producto_id: { deposito_id: dto.deposito_destino_id, producto_id: dto.producto_id } },
        create: {
          deposito_id: dto.deposito_destino_id,
          producto_id: dto.producto_id,
          cantidad_disponible: dto.cantidad,
        },
        update: {
          cantidad_disponible: { increment: dto.cantidad },
          updated_at: new Date(),
        },
      });

      // Registrar movimiento
      const movimiento = await tx.movimientos_inventario.create({
        data: {
          deposito_origen_id: dto.deposito_origen_id,
          deposito_destino_id: dto.deposito_destino_id,
          producto_id: dto.producto_id,
          tipo_movimiento_id: tipoMovimiento.id,
          cantidad: dto.cantidad,
          documento_origen: 'transferencia',
          observaciones: dto.observaciones,
          usuario_id,
        },
      });

      return movimiento;
    });

    this.logger.log(
      `Transferencia stock: producto ${dto.producto_id} de ${dto.deposito_origen_id} a ${dto.deposito_destino_id} cantidad ${dto.cantidad}`,
      'StockService',
    );
    await this.auditService.log({
      empresa_id,
      user_id: usuario_id,
      action: 'UPDATE',
      entity_type: 'stock_transferencia',
      entity_id: dto.producto_id,
      descripcion: `Transferencia de stock | Producto: ${dto.producto_id} | Origen: ${dto.deposito_origen_id} | Destino: ${dto.deposito_destino_id} | Cantidad: ${dto.cantidad}`,
      old_value: {
        deposito_origen: dto.deposito_origen_id,
        cantidad_disponible: Number(stockOrigen.cantidad_disponible),
      },
      new_value: {
        deposito_destino: dto.deposito_destino_id,
        cantidad: dto.cantidad,
        observaciones: dto.observaciones,
      },
    });
    return { message: 'Transferencia realizada exitosamente', data: result };
  }

  async getReporteNiveles(
    empresa_id: string,
    params?: {
      page?: number;
      limit?: number;
      deposito_id?: string;
      sucursal_id?: string;
      estado?: string; // 'todos' | 'bajo_minimo' | 'sin_stock' | 'sobre_maximo' | 'ok'
      search?: string;
      /** Quién consulta. Define qué sucursales puede ver. */
      usuarioId?: string;
    },
  ) {
    const take = Math.max(1, Math.min(100, Number(params?.limit) || 25));
    const currentPage = Math.max(1, Number(params?.page) || 1);
    const skip = (currentPage - 1) * take;

    const where: any = {
      deposito: { empresa_id },
    };

    if (params?.deposito_id) where.deposito_id = params.deposito_id;

    // El alcance del usuario manda sobre la sucursal pedida por filtro: si pide
    // una que no tiene asignada, el reporte sale vacío.
    const alcance = await alcanceSucursalUsuario(this.prisma, params?.usuarioId, empresa_id);
    const filtroSucursal = filtroSucursalDeposito(alcance, params?.sucursal_id);
    if (filtroSucursal !== undefined) {
      where.deposito = { ...where.deposito, sucursal_id: filtroSucursal };
    }
    if (params?.search) {
      where.producto = {
        OR: [
          { descripcion: { contains: params.search, mode: 'insensitive' } },
          { cod_producto: { contains: params.search, mode: 'insensitive' } },
        ],
      };
    }

    // Filtros de estado se aplican post-query por la complejidad de las comparaciones
    const [items, total] = await this.prisma.$transaction([
      this.prisma.stock_deposito.findMany({
        where,
        include: {
          producto: { select: { id: true, descripcion: true, cod_producto: true } },
          deposito: { include: { sucursal: { select: { id: true, descripcion: true } } } },
        },
        orderBy: { producto: { descripcion: 'asc' } },
        skip,
        take,
      }),
      this.prisma.stock_deposito.count({ where }),
    ]);

    // KPIs globales (sin paginación)
    const allForKpis = await this.prisma.stock_deposito.findMany({
      where,
      select: { cantidad_disponible: true, cantidad_reservada: true, stock_minimo: true, stock_maximo: true },
    });

    const kpis = {
      totalProductos: allForKpis.length,
      sinStock: allForKpis.filter((s) => Number(s.cantidad_disponible) <= 0).length,
      bajoMinimo: allForKpis.filter((s) => {
        const disp = Number(s.cantidad_disponible);
        const min = Number(s.stock_minimo);
        return disp > 0 && min > 0 && disp <= min;
      }).length,
      sobreMaximo: allForKpis.filter((s) => {
        const disp = Number(s.cantidad_disponible);
        const max = Number(s.stock_maximo || 0);
        return max > 0 && disp > max;
      }).length,
      ok: allForKpis.filter((s) => {
        const disp = Number(s.cantidad_disponible);
        const min = Number(s.stock_minimo);
        const max = Number(s.stock_maximo || 0);
        return disp > 0 && (min <= 0 || disp > min) && (max <= 0 || disp <= max);
      }).length,
      totalDisponible: allForKpis.reduce((acc, s) => acc + Number(s.cantidad_disponible), 0),
      totalReservado: allForKpis.reduce((acc, s) => acc + Number(s.cantidad_reservada), 0),
    };

    return { data: items, total, page: currentPage, limit: take, kpis };
  }

  async getStockBajoMinimo(empresa_id: string, usuarioId?: string) {
    // Mismo alcance que el reporte de niveles: un usuario de una sucursal no
    // debería recibir alertas de reposición de depósitos que no maneja.
    const alcance = await alcanceSucursalUsuario(this.prisma, usuarioId, empresa_id);
    const filtroSucursal = filtroSucursalDeposito(alcance);
    return this.prisma.stock_deposito.findMany({
      where: {
        deposito: { empresa_id, ...(filtroSucursal !== undefined ? { sucursal_id: filtroSucursal } : {}) },
        cantidad_disponible: { lte: this.prisma.stock_deposito.fields.stock_minimo },
      },
      include: { producto: true, deposito: { include: { sucursal: true } } },
    });
  }
}
