import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { alcanceSucursalUsuario } from 'src/utils/alcance-sucursal';
import { whereProductoPorRubro } from 'src/common/utils/rubros.util';
import { rubrosFiltroUsuario } from 'src/utils/alcance-rubro';

@Injectable()
export class InventarioFisicoService {
  constructor(private readonly prisma: PrismaService) {}

  // ── Sesiones ─────────────────────────────────────────────────────────────

  async crearSesion(empresaId: string, nombre: string) {
    return this.prisma.inventario_sesiones.create({
      data: { empresa_id: empresaId, nombre },
    });
  }

  async listarSesiones(empresaId: string, usuarioId?: string) {
    // La sesión no guarda depósito ni sucursal: eso vive en cada conteo. Así
    // que se filtra por los depósitos donde se contó. Una sesión recién creada
    // todavía no tiene conteos, y esconderla dejaría al usuario sin poder
    // trabajarla, así que esas se muestran igual.
    const alcance = await alcanceSucursalUsuario(this.prisma, usuarioId, empresaId);
    const filtroDepositos = alcance
      ? {
          OR: [
            { conteos: { some: { deposito_id: { in: alcance.depositoIds } } } },
            { conteos: { none: {} } },
          ],
        }
      : {};

    const sesiones = await this.prisma.inventario_sesiones.findMany({
      where: { empresa_id: empresaId, ...filtroDepositos },
      orderBy: { created_at: 'desc' },
      include: {
        _count: { select: { conteos: true } },
      },
    });
    return sesiones;
  }

  async cerrarSesion(sesionId: string, empresaId: string) {
    const sesion = await this.prisma.inventario_sesiones.findFirst({
      where: { id: sesionId, empresa_id: empresaId },
    });
    if (!sesion) throw new NotFoundException('Sesión no encontrada');
    if (sesion.estado === 'cerrado') throw new BadRequestException('La sesión ya está cerrada');

    return this.prisma.inventario_sesiones.update({
      where: { id: sesionId },
      data: { estado: 'cerrado', cerrado_at: new Date() },
    });
  }

  // ── Conteos ───────────────────────────────────────────────────────────────

  async registrarConteo(
    sesionId: string,
    empresaId: string,
    data: {
      deposito_id: string;
      categoria_id?: string;
      producto_id: string;
      cantidad_contada: number;
      usuario_id?: string;
    },
  ) {
    const sesion = await this.prisma.inventario_sesiones.findFirst({
      where: { id: sesionId, empresa_id: empresaId, estado: 'abierto' },
    });
    if (!sesion) throw new BadRequestException('Sesión no encontrada o ya cerrada');

    return this.prisma.inventario_conteos.upsert({
      where: {
        sesion_id_deposito_id_producto_id: {
          sesion_id: sesionId,
          deposito_id: data.deposito_id,
          producto_id: data.producto_id,
        },
      },
      update: {
        cantidad_contada: data.cantidad_contada,
        usuario_id: data.usuario_id,
        updated_at: new Date(),
      },
      create: {
        sesion_id: sesionId,
        deposito_id: data.deposito_id,
        categoria_id: data.categoria_id,
        producto_id: data.producto_id,
        cantidad_contada: data.cantidad_contada,
        usuario_id: data.usuario_id,
      },
    });
  }

  async listarConteos(
    sesionId: string,
    empresaId: string,
    filters: { depositoId?: string; categoriaId?: string },
  ) {
    const sesion = await this.prisma.inventario_sesiones.findFirst({
      where: { id: sesionId, empresa_id: empresaId },
    });
    if (!sesion) throw new NotFoundException('Sesión no encontrada');

    return this.prisma.inventario_conteos.findMany({
      where: {
        sesion_id: sesionId,
        ...(filters.depositoId ? { deposito_id: filters.depositoId } : {}),
        ...(filters.categoriaId ? { categoria_id: filters.categoriaId } : {}),
      },
      include: {
        producto: { select: { id: true, cod_producto: true, descripcion: true } },
        deposito: { select: { id: true, descripcion: true } },
        categoria: { select: { id: true, descripcion: true } },
      },
      orderBy: { updated_at: 'desc' },
    });
  }

  async eliminarConteo(sesionId: string, empresaId: string, productoId: string, depositoId: string) {
    const sesion = await this.prisma.inventario_sesiones.findFirst({
      where: { id: sesionId, empresa_id: empresaId, estado: 'abierto' },
    });
    if (!sesion) throw new BadRequestException('Sesión no encontrada o ya cerrada');

    return this.prisma.inventario_conteos.deleteMany({
      where: {
        sesion_id: sesionId,
        deposito_id: depositoId,
        producto_id: productoId,
      },
    });
  }

  // ── Cerrar zona y aplicar ajuste ──────────────────────────────────────────

  async cerrarZona(
    sesionId: string,
    empresaId: string,
    depositoId: string,
    categoriaId: string | null,
    mantenerStockNoContados = false,
    usuarioId?: string,
  ) {
    const sesion = await this.prisma.inventario_sesiones.findFirst({
      where: { id: sesionId, empresa_id: empresaId, estado: 'abierto' },
    });
    if (!sesion) throw new BadRequestException('Sesión no encontrada o ya cerrada');

    // Productos de la zona (depósito + categoría)
    const productos = await this.prisma.productos.findMany({
      where: {
        empresa_id: empresaId,
        deleted: false,
        active: true,
        ...(categoriaId ? { categoria_id: categoriaId } : {}),
      },
      select: { id: true },
    });

    const productosIds = productos.map((p) => p.id);

    // Conteos registrados para esta zona
    const conteos = await this.prisma.inventario_conteos.findMany({
      where: {
        sesion_id: sesionId,
        deposito_id: depositoId,
        ...(categoriaId ? { categoria_id: categoriaId } : {}),
        producto_id: { in: productosIds },
      },
    });

    const conteosMap = new Map(conteos.map((c) => [c.producto_id, Number(c.cantidad_contada)]));

    // Aplicar ajuste de stock + registrar la aplicación en historial (append-only)
    await this.prisma.$transaction(async (tx) => {
      for (const productoId of productosIds) {
        const contado = conteosMap.has(productoId);
        // Si mantenerStockNoContados=true, omitir productos sin conteo
        if (!contado && mantenerStockNoContados) continue;

        const cantidad = contado ? conteosMap.get(productoId)! : 0;

        await tx.stock_deposito.upsert({
          where: {
            deposito_id_producto_id: { deposito_id: depositoId, producto_id: productoId },
          },
          update: { cantidad_disponible: cantidad, updated_at: new Date() },
          create: {
            deposito_id: depositoId,
            producto_id: productoId,
            cantidad_disponible: cantidad,
          },
        });
      }

      await tx.inventario_zonas_aplicadas.create({
        data: {
          sesion_id: sesionId,
          deposito_id: depositoId,
          categoria_id: categoriaId,
          aplicada_por_usuario_id: usuarioId,
          total_productos: productosIds.length,
          contados: conteosMap.size,
          mantener_stock_no_contados: mantenerStockNoContados,
        },
      });
    });

    return {
      total_productos: productosIds.length,
      contados: conteosMap.size,
      en_cero: productosIds.length - conteosMap.size,
    };
  }

  /**
   * Historial de aplicaciones de ajuste por zona. Permite al frontend mostrar
   * "ya se aplicó N veces, la última el …" para evitar reaplicaciones accidentales.
   */
  async zonasAplicadas(sesionId: string, empresaId: string) {
    const sesion = await this.prisma.inventario_sesiones.findFirst({
      where: { id: sesionId, empresa_id: empresaId },
      select: { id: true },
    });
    if (!sesion) throw new NotFoundException('Sesión no encontrada');

    const rows = await this.prisma.inventario_zonas_aplicadas.findMany({
      where: { sesion_id: sesionId },
      orderBy: { aplicada_at: 'desc' },
      include: {
        deposito: { select: { id: true, descripcion: true } },
        categoria: { select: { id: true, descripcion: true } },
      },
    });

    // Enriquecer con nombre del usuario (consulta única).
    const usuarioIds = Array.from(
      new Set(rows.map((r) => r.aplicada_por_usuario_id).filter(Boolean) as string[]),
    );
    const usuarios = usuarioIds.length
      ? await this.prisma.usuario.findMany({
          where: { id: { in: usuarioIds } },
          select: { id: true, nombres: true, apellidos: true, username: true },
        })
      : [];
    const usuarioMap = new Map(usuarios.map((u) => [u.id, u]));

    return rows.map((r) => {
      const u = r.aplicada_por_usuario_id ? usuarioMap.get(r.aplicada_por_usuario_id) : null;
      const nombre = u
        ? [u.nombres, u.apellidos].filter(Boolean).join(' ').trim() || u.username
        : null;
      return {
        id: r.id,
        deposito_id: r.deposito_id,
        deposito_descripcion: r.deposito?.descripcion ?? null,
        categoria_id: r.categoria_id,
        categoria_descripcion: r.categoria?.descripcion ?? null,
        aplicada_at: r.aplicada_at,
        aplicada_por_nombre: nombre,
        total_productos: r.total_productos,
        contados: r.contados,
        mantener_stock_no_contados: r.mantener_stock_no_contados,
      };
    });
  }

  // ── Productos de zona para conteo ─────────────────────────────────────────

  async productosZona(
    empresaId: string,
    depositoId: string | undefined,
    categoriaIds: string[] | null,
    sesionId: string,
    busqueda?: string,
    skip = 0,
    take = 50,
    marcaIds?: string[],
    filtro?: 'contados' | 'pendientes',
    /** Quién consulta y el rubro del selector: mismo recorte que el catálogo de Productos. */
    contexto: { usuarioId?: string; rubroId?: string | null } = {},
  ) {
    // Pre-filtro por estado de conteo (requiere IDs contados/no contados)
    let filtroIds: string[] | undefined;
    if (filtro) {
      const conteados = await this.prisma.inventario_conteos.findMany({
        where: {
          sesion_id: sesionId,
          ...(depositoId ? { deposito_id: depositoId } : {}),
        },
        select: { producto_id: true },
        distinct: ['producto_id'],
      });
      filtroIds = conteados.map((c) => c.producto_id);
    }

    // Sin esto el conteo mostraba los productos de todos los rubros aunque el
    // selector tuviera uno elegido (11.867 en vez de los de Regalería).
    const porRubro = whereProductoPorRubro(
      await rubrosFiltroUsuario(this.prisma, contexto.usuarioId, empresaId, contexto.rubroId),
    );
    const where: any = {
      empresa_id: empresaId,
      deleted: false,
      active: true,
      ...porRubro,
      ...(categoriaIds && categoriaIds.length > 0 ? { categoria_id: { in: categoriaIds } } : {}),
      ...(marcaIds && marcaIds.length > 0 ? { marca_id: { in: marcaIds } } : {}),
      ...(filtro === 'contados' && filtroIds ? { id: { in: filtroIds } } : {}),
      ...(filtro === 'pendientes' && filtroIds ? { id: { notIn: filtroIds } } : {}),
      ...(busqueda
        ? {
            OR: [
              { descripcion: { contains: busqueda, mode: 'insensitive' } },
              { cod_producto: { contains: busqueda, mode: 'insensitive' } },
            ],
          }
        : {}),
    };

    const [total, productos] = await Promise.all([
      this.prisma.productos.count({ where }),
      this.prisma.productos.findMany({
        where,
        select: {
          id: true,
          cod_producto: true,
          descripcion: true,
          categoria_id: true,
          marcas: { select: { descripcion: true } },
        },
        orderBy: { descripcion: 'asc' },
        skip,
        take,
      }),
    ]);

    const productoIds = productos.map((p) => p.id);

    const [conteos, stocks] = await Promise.all([
      // conteos filtrados por depósito (si hay uno seleccionado)
      this.prisma.inventario_conteos.findMany({
        where: {
          sesion_id: sesionId,
          ...(depositoId ? { deposito_id: depositoId } : {}),
          producto_id: { in: productoIds },
        },
      }),
      // stock TOTAL global (suma de todos los depósitos)
      this.prisma.stock_deposito.groupBy({
        by: ['producto_id'],
        where: { producto_id: { in: productoIds } },
        _sum: { cantidad_disponible: true },
      }),
    ]);

    const conteosMap = new Map(conteos.map((c) => [c.producto_id, Number(c.cantidad_contada)]));
    const stockMap = new Map(stocks.map((s) => [s.producto_id, Number(s._sum.cantidad_disponible ?? 0)]));

    return {
      total,
      data: productos.map((p) => ({
        ...p,
        cantidad_contada: conteosMap.has(p.id) ? conteosMap.get(p.id) : null,
        contado: conteosMap.has(p.id),
        stock_sistema: stockMap.get(p.id) ?? 0,
      })),
    };
  }

  // ── Progreso por zonas ────────────────────────────────────────────────────

  async progresoPorZona(sesionId: string, empresaId: string) {
    const sesion = await this.prisma.inventario_sesiones.findFirst({
      where: { id: sesionId, empresa_id: empresaId },
    });
    if (!sesion) throw new NotFoundException('Sesión no encontrada');

    const conteos = await this.prisma.inventario_conteos.groupBy({
      by: ['deposito_id', 'categoria_id'],
      where: { sesion_id: sesionId },
      _count: { producto_id: true },
    });

    const depositoIds = [...new Set(conteos.map((c) => c.deposito_id))];
    const depositos = await this.prisma.depositos.findMany({
      where: { id: { in: depositoIds } },
      select: { id: true, descripcion: true },
    });

    const categoriaIds = [...new Set(conteos.map((c) => c.categoria_id).filter(Boolean))];
    const categorias = await this.prisma.categorias.findMany({
      where: { id: { in: categoriaIds } },
      select: { id: true, descripcion: true },
    });

    const depositoMap = new Map(depositos.map((d) => [d.id, d.descripcion]));
    const categoriaMap = new Map(categorias.map((c) => [c.id, c.descripcion]));

    return conteos.map((c) => ({
      deposito_id: c.deposito_id,
      deposito: depositoMap.get(c.deposito_id) ?? 'Sin depósito',
      categoria_id: c.categoria_id,
      categoria: c.categoria_id ? (categoriaMap.get(c.categoria_id) ?? 'Sin nombre') : 'Sin categoría',
      productos_contados: c._count.producto_id,
    }));
  }

  async productosNoContados(
    sesionId: string,
    empresaId: string,
    depositoId?: string,
    categoriaIds?: string[],
    busqueda?: string,
    skip = 0,
    take = 50,
  ) {
    const sesion = await this.prisma.inventario_sesiones.findFirst({
      where: { id: sesionId, empresa_id: empresaId },
    });
    if (!sesion) throw new NotFoundException('Sesión no encontrada');

    // IDs ya contados en esta sesión (con filtro de depósito si aplica)
    const conteados = await this.prisma.inventario_conteos.findMany({
      where: {
        sesion_id: sesionId,
        ...(depositoId ? { deposito_id: depositoId } : {}),
      },
      select: { producto_id: true },
      distinct: ['producto_id'],
    });
    const contadosIds = conteados.map((c) => c.producto_id);

    const where: any = {
      empresa_id: empresaId,
      deleted: false,
      active: true,
      id: { notIn: contadosIds },
      ...(categoriaIds && categoriaIds.length > 0 ? { categoria_id: { in: categoriaIds } } : {}),
      ...(busqueda
        ? {
            OR: [
              { descripcion: { contains: busqueda, mode: 'insensitive' } },
              { cod_producto: { contains: busqueda, mode: 'insensitive' } },
            ],
          }
        : {}),
    };

    const [total, productos] = await Promise.all([
      this.prisma.productos.count({ where }),
      this.prisma.productos.findMany({
        where,
        select: {
          id: true,
          cod_producto: true,
          descripcion: true,
          categoria_id: true,
          categoria: { select: { descripcion: true } },
        },
        orderBy: { descripcion: 'asc' },
        skip,
        take,
      }),
    ]);

    const productoIds = productos.map((p) => p.id);
    const stocks = depositoId
      ? await this.prisma.stock_deposito.findMany({
          where: { deposito_id: depositoId, producto_id: { in: productoIds } },
          select: { producto_id: true, cantidad_disponible: true },
        })
      : [];
    const stockMap = new Map(stocks.map((s) => [s.producto_id, Number(s.cantidad_disponible)]));

    return {
      total,
      data: productos.map((p) => ({
        id: p.id,
        cod_producto: p.cod_producto,
        descripcion: p.descripcion,
        categoria: p.categoria?.descripcion ?? null,
        stock_sistema: stockMap.get(p.id) ?? 0,
      })),
    };
  }

  // ── Planilla para imprimir ────────────────────────────────────────────────

  async getPlanillaData(empresaId: string, depositoId: string, categoriaId: string | null) {
    const productos = await this.prisma.productos.findMany({
      where: {
        empresa_id: empresaId,
        deleted: false,
        active: true,
        ...(categoriaId ? { categoria_id: categoriaId } : {}),
      },
      select: {
        id: true,
        cod_producto: true,
        descripcion: true,
        categoria: { select: { descripcion: true } },
      },
      orderBy: { descripcion: 'asc' },
    });

    const deposito = await this.prisma.depositos.findUnique({
      where: { id: depositoId },
      select: { descripcion: true },
    });

    return { deposito: deposito?.descripcion ?? '', productos };
  }
}
