import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import { alcanceSucursalUsuario } from 'src/utils/alcance-sucursal';

type Tx = Prisma.TransactionClient;

type LotesListQuery = {
  page?: string;
  limit?: string;
  sucursal_id?: string;
  producto_id?: string;
  deposito_id?: string;
  estado?: string;
  q?: string;
  numero_lote?: string;
  fecha_fabricacion_desde?: string;
  fecha_fabricacion_hasta?: string;
  fecha_vencimiento_desde?: string;
  fecha_vencimiento_hasta?: string;
  stock_desde?: string;
  stock_hasta?: string;
  costo_desde?: string;
  costo_hasta?: string;
};

type ErpSucursalConfig = {
  empresa_id: string;
  sucursal_id: string;
  maneja_lotes: boolean;
  metodo_costeo: string;
  alertar_lotes_vencidos: boolean;
  dias_alerta_vencimiento: number;
  compras_credito_requiere_cuotas: boolean;
  active: boolean;
};

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

  private getDefaultConfig(empresaId: string, sucursalId: string): ErpSucursalConfig {
    return {
      empresa_id: empresaId,
      sucursal_id: sucursalId,
      maneja_lotes: false,
      metodo_costeo: 'FIFO',
      alertar_lotes_vencidos: true,
      dias_alerta_vencimiento: 30,
      compras_credito_requiere_cuotas: false,
      active: true,
    };
  }

  private normalizeConfig(
    empresaId: string,
    sucursalId: string,
    configRaw?: Record<string, unknown> | null,
  ): ErpSucursalConfig {
    const fallback = this.getDefaultConfig(empresaId, sucursalId);
    const raw = configRaw || {};

    const metodoCosteoRaw = typeof raw.metodo_costeo === 'string' ? raw.metodo_costeo.trim().toUpperCase() : '';
    const metodoCosteo = metodoCosteoRaw || fallback.metodo_costeo;
    const diasRaw = Number(raw.dias_alerta_vencimiento);
    const diasAlerta = Number.isFinite(diasRaw) && diasRaw > 0 ? Math.trunc(diasRaw) : fallback.dias_alerta_vencimiento;

    return (
      {
        ...fallback,
        maneja_lotes: typeof raw.maneja_lotes === 'boolean' ? raw.maneja_lotes : fallback.maneja_lotes,
        metodo_costeo: metodoCosteo,
        alertar_lotes_vencidos:
          typeof raw.alertar_lotes_vencidos === 'boolean'
            ? raw.alertar_lotes_vencidos
            : fallback.alertar_lotes_vencidos,
        dias_alerta_vencimiento: diasAlerta,
        compras_credito_requiere_cuotas:
          typeof raw.compras_credito_requiere_cuotas === 'boolean'
            ? raw.compras_credito_requiere_cuotas
            : fallback.compras_credito_requiere_cuotas,
        active: typeof raw.active === 'boolean' ? raw.active : fallback.active,
      } as ErpSucursalConfig
    );
  }

  private async resolveSucursalConfig(
    empresaId: string,
    sucursalId: string,
    tx?: Tx,
  ): Promise<{ id: string; config: Record<string, unknown> | null }> {
    const db = tx ?? this.prisma;
    const sucursal = await db.empresas_sucursales.findFirst({
      where: { id: sucursalId, empresa_id: empresaId, active: true },
      select: {
        id: true,
        config: true,
      },
    });

    if (!sucursal) {
      throw new NotFoundException('Sucursal no encontrada para la empresa');
    }

    return {
      id: sucursal.id,
      config: (sucursal.config as Record<string, unknown> | null) || null,
    };
  }

  /**
   * `permitidas` acota el resultado al alcance del usuario (`null` = sin
   * restricción). Importa en las dos puntas: valida la sucursal que se pide, y
   * cambia el fallback — sin esto, un usuario sin sucursal en la query recibía
   * la primera sucursal de la empresa, que podía no ser la suya.
   */
  private async resolveSucursalOperativa(
    empresaId: string,
    params?: { sucursalId?: string | null; depositoId?: string | null },
    tx?: Tx,
    permitidas?: string[] | null,
  ): Promise<string | null> {
    const db = tx ?? this.prisma;
    const permite = (id: string) => !permitidas || permitidas.includes(id);

    const fromSucursal = params?.sucursalId || null;
    if (fromSucursal) {
      if (!permite(fromSucursal)) return null;
      const sucursal = await db.empresas_sucursales.findFirst({
        where: { id: fromSucursal, empresa_id: empresaId, active: true },
        select: { id: true },
      });
      if (sucursal?.id) return sucursal.id;
      return null;
    }

    const fromDeposito = params?.depositoId || null;
    if (fromDeposito) {
      const deposito = await db.depositos.findFirst({
        where: { id: fromDeposito, empresa_id: empresaId, active: true },
        select: { sucursal_id: true },
      });
      if (deposito?.sucursal_id) {
        return permite(deposito.sucursal_id) ? deposito.sucursal_id : null;
      }
    }

    const firstSucursal = await db.empresas_sucursales.findFirst({
      where: {
        empresa_id: empresaId,
        active: true,
        ...(permitidas ? { id: { in: permitidas } } : {}),
      },
      select: { id: true },
      orderBy: { created_at: 'asc' },
    });
    return firstSucursal?.id || null;
  }

  async getEmpresaConfig(empresaId: string, sucursalId: string, tx?: Tx): Promise<ErpSucursalConfig> {
    const sucursal = await this.resolveSucursalConfig(empresaId, sucursalId, tx);
    return this.normalizeConfig(empresaId, sucursal.id, sucursal.config);
  }

  async isEnabledForEmpresa(empresaId: string, sucursalId: string, tx?: Tx) {
    const db = tx ?? this.prisma;
    const config = await this.getEmpresaConfig(empresaId, sucursalId, tx);
    if (!config?.maneja_lotes || config?.active === false) return false;

    const empresa = await db.empresas.findUnique({
      where: { id: empresaId },
      select: { type: true },
    });

    if (empresa?.type === 'holding' || empresa?.type === 'reseller') {
      return true;
    }

    const moduleActive = await db.suscripcion_modulos.findFirst({
      where: {
        activo: true,
        suscripciones: {
          empresa_id: empresaId,
          estado: 'Activo',
        },
        modulos: {
          codigo: 'LOTES',
          active: true,
        },
      },
      select: { id: true },
    });

    return Boolean(moduleActive);
  }

  async ensureEnabledForEmpresa(empresaId: string, sucursalId: string, tx?: Tx) {
    const enabled = await this.isEnabledForEmpresa(empresaId, sucursalId, tx);
    if (!enabled) {
      throw new BadRequestException(
        'El módulo LOTES no está habilitado para esta sucursal (requiere configuración y módulo activo)',
      );
    }
  }

  async status(empresaId: string, sucursalId: string) {
    const config = await this.getEmpresaConfig(empresaId, sucursalId);
    const moduleEnabled = await this.isEnabledForEmpresa(empresaId, sucursalId);

    return {
      enabled: moduleEnabled,
      config,
    };
  }

  async createLoteIngreso(
    tx: Tx,
    params: {
      empresaId: string;
      productoId: string;
      compraId?: string | null;
      compraDetId?: string | null;
      depositoId: string;
      numeroLote: string;
      fechaFabricacion?: Date | null;
      fechaVencimiento?: Date | null;
      cantidad: number;
      costoUnitario: number;
      usuarioId?: string;
    },
  ) {
    const cantidad = this.round(this.toNumber(params.cantidad), 4);
    if (cantidad <= 0) return null;

    const lot = await tx.lotes_producto.create({
      data: {
        empresa_id: params.empresaId,
        producto_id: params.productoId,
        compra_id: params.compraId || null,
        compra_det_id: params.compraDetId || null,
        numero_lote: params.numeroLote,
        fecha_fabricacion: params.fechaFabricacion || null,
        fecha_vencimiento: params.fechaVencimiento || null,
        cantidad_inicial: cantidad,
        cantidad_disponible: cantidad,
        costo_unitario: this.round(this.toNumber(params.costoUnitario), 4),
        activo: true,
        usuario_id: params.usuarioId || null,
      },
    });

    await tx.stock_lote.create({
      data: {
        lote_id: lot.id,
        deposito_id: params.depositoId,
        cantidad_disponible: cantidad,
        active: true,
      },
    });

    return lot;
  }

  async revertCompraDetalleLote(
    tx: Tx,
    params: {
      compraDetId: string;
      depositoId: string;
    },
  ) {
    const lotes = await tx.lotes_producto.findMany({
      where: { compra_det_id: params.compraDetId },
      include: {
        stock_lote: {
          where: {
            deposito_id: params.depositoId,
          },
        },
      },
    });

    for (const lote of lotes) {
      const cantidadInicial = this.toNumber(lote.cantidad_inicial);
      const cantidadDisponibleLote = this.toNumber(lote.cantidad_disponible);
      const stockRecord = lote.stock_lote?.[0];
      const cantidadStock = this.toNumber(stockRecord?.cantidad_disponible);

      if (cantidadDisponibleLote + 0.0001 < cantidadInicial || cantidadStock + 0.0001 < cantidadInicial) {
        throw new BadRequestException(
          `No se puede revertir la compra: el lote ${lote.numero_lote} ya tuvo consumos y no está íntegro`,
        );
      }

      if (stockRecord) {
        await tx.stock_lote.update({
          where: { id: stockRecord.id },
          data: {
            cantidad_disponible: 0,
            active: false,
            updated_at: new Date(),
          },
        });
      }

      await tx.lotes_producto.update({
        where: { id: lote.id },
        data: {
          cantidad_disponible: 0,
          activo: false,
          updated_at: new Date(),
        },
      });
    }
  }

  async restoreFacturaDetLotes(
    tx: Tx,
    params: {
      facturaDetId: string;
      depositoId: string;
      cantidad: number;
    },
  ) {
    const cantidadRestaurar = this.round(this.toNumber(params.cantidad), 4);
    if (cantidadRestaurar <= 0) return [];

    const consumos = await tx.factura_det_lote.findMany({
      where: { factura_det_id: params.facturaDetId },
      include: {
        lotes_producto: {
          select: { id: true, numero_lote: true },
        },
      },
      orderBy: { created_at: 'asc' },
    });

    if (!consumos.length) return [];

    // Solo se puede devolver lo que todavía no se devolvió: una línea puede
    // recibir varias devoluciones (NC parciales, o NC seguida de anulación de la
    // factura). Sin descontar lo ya restaurado, cada llamada devolvía el consumo
    // original completo e inflaba el lote.
    const pendientePorConsumo = new Map(
      consumos.map((row) => [
        row.id,
        Math.max(0, this.round(this.toNumber(row.cantidad) - this.toNumber(row.cantidad_restaurada), 4)),
      ]),
    );
    const totalPendiente = this.round(
      Array.from(pendientePorConsumo.values()).reduce((acc, n) => acc + n, 0),
      4,
    );

    if (totalPendiente <= 0.0001) return [];

    if (totalPendiente + 0.0001 < cantidadRestaurar) {
      throw new BadRequestException(
        `No se puede revertir lotes: cantidad a devolver (${cantidadRestaurar}) supera lo pendiente de devolver (${totalPendiente})`,
      );
    }

    let remaining = cantidadRestaurar;
    const restored: Array<{ lote_id: string; numero_lote: string; cantidad: number }> = [];

    for (const consumo of consumos) {
      if (remaining <= 0.0001) break;

      const qtyPendiente = pendientePorConsumo.get(consumo.id) ?? 0;
      if (qtyPendiente <= 0) continue;

      const qtyRestore = Math.min(remaining, qtyPendiente);

      const stock = await tx.stock_lote.findUnique({
        where: {
          lote_id_deposito_id: {
            lote_id: consumo.lote_id,
            deposito_id: params.depositoId,
          },
        },
      });

      if (stock) {
        await tx.stock_lote.update({
          where: { id: stock.id },
          data: {
            cantidad_disponible: this.round(this.toNumber(stock.cantidad_disponible) + qtyRestore, 4),
            active: true,
            updated_at: new Date(),
          },
        });
      } else {
        await tx.stock_lote.create({
          data: {
            lote_id: consumo.lote_id,
            deposito_id: params.depositoId,
            cantidad_disponible: this.round(qtyRestore, 4),
            active: true,
          },
        });
      }

      await tx.lotes_producto.update({
        where: { id: consumo.lote_id },
        data: {
          cantidad_disponible: { increment: this.round(qtyRestore, 4) },
          activo: true,
          updated_at: new Date(),
        },
      });

      // Deja constancia de lo devuelto para que una segunda devolución sobre la
      // misma línea no vuelva a restaurar lo mismo.
      await tx.factura_det_lote.update({
        where: { id: consumo.id },
        data: { cantidad_restaurada: { increment: this.round(qtyRestore, 4) } },
      });

      restored.push({
        lote_id: consumo.lote_id,
        numero_lote: consumo.lotes_producto.numero_lote,
        cantidad: this.round(qtyRestore, 4),
      });

      remaining = this.round(remaining - qtyRestore, 4);
    }

    return restored;
  }

  async list(empresaId: string, query: LotesListQuery, usuarioId?: string) {
    const alcance = await alcanceSucursalUsuario(this.prisma, usuarioId, empresaId);
    const sucursalOperativa = await this.resolveSucursalOperativa(
      empresaId,
      { sucursalId: query.sucursal_id || null, depositoId: query.deposito_id || null },
      undefined,
      alcance?.sucursalIds ?? null,
    );
    if (!sucursalOperativa) {
      throw new NotFoundException('No se encontró sucursal activa para consultar lotes');
    }
    await this.ensureEnabledForEmpresa(empresaId, sucursalOperativa);

    const take = Math.max(1, Math.min(100, Number(query.limit) || 20));
    const currentPage = Math.max(1, Number(query.page) || 1);
    const skip = (currentPage - 1) * take;

    const today = new Date();
    const loteWhere: Prisma.lotes_productoWhereInput = {
      empresa_id: empresaId,
    };
    const where: Prisma.stock_loteWhereInput = {
      lotes_producto: loteWhere,
      depositos: {
        sucursal_id: sucursalOperativa,
      },
    };

    if (query.deposito_id) where.deposito_id = query.deposito_id;
    if (query.producto_id) {
      loteWhere.producto_id = query.producto_id;
    }

    const loteAnd: Prisma.lotes_productoWhereInput[] = [];
    const stockAnd: Prisma.stock_loteWhereInput[] = [];

    if (query.numero_lote?.trim()) {
      loteAnd.push({
        numero_lote: { contains: query.numero_lote.trim(), mode: 'insensitive' },
      });
    }

    if (query.q?.trim()) {
      const q = query.q.trim();
      loteAnd.push({
        OR: [
          { numero_lote: { contains: q, mode: 'insensitive' } },
          { productos: { descripcion: { contains: q, mode: 'insensitive' } } },
          { productos: { cod_producto: { contains: q, mode: 'insensitive' } } },
        ],
      });
    }

    const fechaFabDesde = this.parseDateBoundary(query.fecha_fabricacion_desde, false);
    const fechaFabHasta = this.parseDateBoundary(query.fecha_fabricacion_hasta, true);
    if (fechaFabDesde || fechaFabHasta) {
      loteAnd.push({
        fecha_fabricacion: {
          ...(fechaFabDesde ? { gte: fechaFabDesde } : {}),
          ...(fechaFabHasta ? { lte: fechaFabHasta } : {}),
        },
      });
    }

    const fechaVencDesde = this.parseDateBoundary(query.fecha_vencimiento_desde, false);
    const fechaVencHasta = this.parseDateBoundary(query.fecha_vencimiento_hasta, true);
    if (fechaVencDesde || fechaVencHasta) {
      loteAnd.push({
        fecha_vencimiento: {
          ...(fechaVencDesde ? { gte: fechaVencDesde } : {}),
          ...(fechaVencHasta ? { lte: fechaVencHasta } : {}),
        },
      });
    }

    const costoDesde = this.parseNumeric(query.costo_desde);
    const costoHasta = this.parseNumeric(query.costo_hasta);
    if (costoDesde !== null || costoHasta !== null) {
      loteAnd.push({
        costo_unitario: {
          ...(costoDesde !== null ? { gte: costoDesde } : {}),
          ...(costoHasta !== null ? { lte: costoHasta } : {}),
        },
      });
    }

    const stockDesde = this.parseNumeric(query.stock_desde);
    const stockHasta = this.parseNumeric(query.stock_hasta);
    if (stockDesde !== null || stockHasta !== null) {
      stockAnd.push({
        cantidad_disponible: {
          ...(stockDesde !== null ? { gte: stockDesde } : {}),
          ...(stockHasta !== null ? { lte: stockHasta } : {}),
        },
      });
    }

    const estado = String(query.estado || '').toLowerCase();
    if (estado === 'agotado') {
      stockAnd.push({ cantidad_disponible: { lte: 0 } });
    } else if (estado === 'vencido') {
      stockAnd.push({ cantidad_disponible: { gt: 0 } });
      loteAnd.push({ fecha_vencimiento: { lt: today } });
    } else if (estado === 'vigente') {
      stockAnd.push({ cantidad_disponible: { gt: 0 } });
      loteAnd.push(
        {
          OR: [{ fecha_vencimiento: null }, { fecha_vencimiento: { gte: today } }],
        },
      );
    }

    if (loteAnd.length > 0) {
      loteWhere.AND = loteAnd;
    }

    if (stockAnd.length > 0) {
      where.AND = stockAnd;
    }

    const [items, total] = await this.prisma.$transaction([
      this.prisma.stock_lote.findMany({
        where,
        include: {
          depositos: {
            select: {
              id: true,
              descripcion: true,
              codigo: true,
            },
          },
          lotes_producto: {
            select: {
              id: true,
              numero_lote: true,
              fecha_fabricacion: true,
              fecha_vencimiento: true,
              cantidad_inicial: true,
              cantidad_disponible: true,
              costo_unitario: true,
              productos: {
                select: {
                  id: true,
                  descripcion: true,
                  cod_producto: true,
                },
              },
              compra_id: true,
              compra_det_id: true,
            },
          },
        },
        orderBy: [
          { lotes_producto: { fecha_vencimiento: 'asc' } },
          { lotes_producto: { created_at: 'asc' } },
        ],
        skip,
        take,
      }),
      this.prisma.stock_lote.count({ where }),
    ]);

    return {
      items,
      total,
      page: currentPage,
      limit: take,
      lastPage: Math.max(1, Math.ceil(total / take)),
    };
  }

  async dashboard(empresaId: string, sucursalId?: string, usuarioId?: string) {
    const alcance = await alcanceSucursalUsuario(this.prisma, usuarioId, empresaId);
    const sucursalOperativa = await this.resolveSucursalOperativa(
      empresaId,
      { sucursalId: sucursalId || null },
      undefined,
      alcance?.sucursalIds ?? null,
    );
    if (!sucursalOperativa) {
      throw new NotFoundException('No se encontró sucursal activa para consultar dashboard de lotes');
    }
    await this.ensureEnabledForEmpresa(empresaId, sucursalOperativa);

    const config = await this.getEmpresaConfig(empresaId, sucursalOperativa);
    const alertDays = Number(config?.dias_alerta_vencimiento || 30);

    const today = new Date();
    const alertDate = new Date(today);
    alertDate.setDate(alertDate.getDate() + alertDays);

    const baseWhere: Prisma.stock_loteWhereInput = {
      lotes_producto: { empresa_id: empresaId },
      depositos: {
        sucursal_id: sucursalOperativa,
      },
    };

    const [totalLotes, lotesConStock, vencidos, porVencer, agotados, sumatoria] = await this.prisma.$transaction([
      this.prisma.lotes_producto.count({ where: { empresa_id: empresaId } }),
      this.prisma.stock_lote.count({ where: { ...baseWhere, cantidad_disponible: { gt: 0 } } }),
      this.prisma.stock_lote.count({
        where: {
          ...baseWhere,
          cantidad_disponible: { gt: 0 },
          lotes_producto: {
            empresa_id: empresaId,
            fecha_vencimiento: { lt: today },
          },
        },
      }),
      this.prisma.stock_lote.count({
        where: {
          ...baseWhere,
          cantidad_disponible: { gt: 0 },
          lotes_producto: {
            empresa_id: empresaId,
            fecha_vencimiento: { gte: today, lte: alertDate },
          },
        },
      }),
      this.prisma.stock_lote.count({ where: { ...baseWhere, cantidad_disponible: { lte: 0 } } }),
      this.prisma.stock_lote.aggregate({
        where: baseWhere,
        _sum: { cantidad_disponible: true },
      }),
    ]);

    return {
      total_lotes: totalLotes,
      lotes_con_stock: lotesConStock,
      lotes_vencidos: vencidos,
      lotes_por_vencer: porVencer,
      lotes_agotados: agotados,
      cantidad_total_disponible: this.toNumber(sumatoria._sum.cantidad_disponible),
      dias_alerta_vencimiento: alertDays,
    };
  }

  async findByProducto(empresaId: string, productoId: string, depositoId?: string, sucursalId?: string) {
    const sucursalOperativa = await this.resolveSucursalOperativa(
      empresaId,
      { sucursalId: sucursalId || null, depositoId: depositoId || null },
    );
    if (!sucursalOperativa) {
      throw new NotFoundException('No se encontró sucursal activa para consultar lotes del producto');
    }
    await this.ensureEnabledForEmpresa(empresaId, sucursalOperativa);

    const producto = await this.prisma.productos.findFirst({
      where: { id: productoId, empresa_id: empresaId },
      select: { id: true },
    });

    if (!producto) throw new NotFoundException('Producto no encontrado para la empresa');

    const where: Prisma.stock_loteWhereInput = {
      lotes_producto: {
        empresa_id: empresaId,
        producto_id: productoId,
      },
      depositos: {
        sucursal_id: sucursalOperativa,
      },
    };

    if (depositoId) where.deposito_id = depositoId;

    const items = await this.prisma.stock_lote.findMany({
      where,
      include: {
        depositos: {
          select: { id: true, descripcion: true, codigo: true },
        },
        lotes_producto: {
          select: {
            id: true,
            numero_lote: true,
            fecha_fabricacion: true,
            fecha_vencimiento: true,
            cantidad_inicial: true,
            cantidad_disponible: true,
            costo_unitario: true,
            compra_id: true,
            compra_det_id: true,
          },
        },
      },
      orderBy: [
        { lotes_producto: { fecha_vencimiento: 'asc' } },
        { lotes_producto: { created_at: 'asc' } },
      ],
    });

    return {
      producto_id: productoId,
      items,
      total: items.length,
    };
  }

  private toNumber(value: Prisma.Decimal | number | string | null | undefined): number {
    const n = Number(value ?? 0);
    return Number.isFinite(n) ? n : 0;
  }

  private parseNumeric(value?: string | null): number | null {
    if (value === undefined || value === null || String(value).trim() === '') return null;
    const n = Number(value);
    if (!Number.isFinite(n)) return null;
    return n;
  }

  private parseDateBoundary(value?: string, endOfDay = false): Date | null {
    if (!value) return null;
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) return null;

    if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
      if (endOfDay) date.setHours(23, 59, 59, 999);
      else date.setHours(0, 0, 0, 0);
    }

    return date;
  }

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