import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from '../prisma/prisma.service';
import { CrearReglaPrecioDto } from './dto/crear-regla-precio.dto';
import { ActualizarReglaPrecioDto } from './dto/actualizar-regla-precio.dto';
import { RecalcularListaDto } from './dto/recalcular-lista.dto';
import { planAplicaAProducto } from '../common/utils/plan-cuotas-matching.util';

type ReglaPrecioRow = {
  id: string;
  nombre: string;
  prioridad: number;
  moneda: string;
  costo_desde: Prisma.Decimal;
  costo_hasta: Prisma.Decimal | null;
  metodo: string;
  valor: Prisma.Decimal;
  redondeo_activo: boolean;
  redondeo_tipo: string;
  redondeo_multiplo: Prisma.Decimal;
  lista_precios_id: string;
  alcance: string;
  categoria_id: string | null;
};

export type CalculoPrecio = {
  precioCosto: number;
  precioSugerido: number | null;
  reglaAplicadaId: string | null;
  reglaAplicadaNombre: string | null;
  desglose: string;
};

@Injectable()
export class PreciosEngineService {
  private readonly logger = new Logger(PreciosEngineService.name);

  constructor(
    private prisma: PrismaService,
    private readonly auditService: AuditService,
  ) {}

  // ============ CRUD reglas ============

  async crear(dto: CrearReglaPrecioDto, empresaId: string, userId?: string) {
    await this.validarListaPertenece(dto.listaPreciosId, empresaId);
    this.validarRangoYMetodo(dto);

    const regla = await this.prisma.reglas_precio.create({
      data: {
        empresa_id: empresaId,
        nombre: dto.nombre,
        prioridad: dto.prioridad ?? 1,
        alcance: dto.alcance ?? 'global',
        categoria_id: dto.alcance === 'categoria' ? dto.categoriaId : null,
        marca_id: dto.alcance === 'marca' ? dto.marcaId : null,
        producto_id: dto.alcance === 'producto' ? dto.productoId : null,
        moneda: dto.moneda ?? 'PYG',
        costo_desde: dto.costoDesde,
        costo_hasta: dto.costoHasta ?? null,
        metodo: dto.metodo ?? 'margen_sobre_costo',
        valor: dto.valor,
        redondeo_activo: dto.redondeoActivo ?? true,
        redondeo_tipo: dto.redondeoTipo ?? 'superior',
        redondeo_multiplo: dto.redondeoMultiplo ?? 1000,
        lista_precios_id: dto.listaPreciosId,
        fecha_inicio: dto.fechaInicio ? new Date(dto.fechaInicio) : new Date(),
        fecha_fin: dto.fechaFin ? new Date(dto.fechaFin) : null,
        usuario_creacion: userId,
      },
    });

    await this.auditService.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'CREATE',
      entity_type: 'reglas_precio',
      entity_id: regla.id,
      descripcion: `Regla de precio creada: ${regla.nombre}`,
      new_value: { ...dto },
    });

    return regla;
  }

  async listar(empresaId: string, filtros: { activo?: boolean; listaPreciosId?: string } = {}) {
    return this.prisma.reglas_precio.findMany({
      where: {
        empresa_id: empresaId,
        ...(filtros.activo !== undefined && { activo: filtros.activo }),
        ...(filtros.listaPreciosId && { lista_precios_id: filtros.listaPreciosId }),
      },
      include: {
        lista_precios: { select: { id: true, codigo: true, nombre: true, moneda: true } },
        categoria: { select: { id: true, descripcion: true } },
        marca: { select: { id: true, descripcion: true } },
        producto: { select: { id: true, descripcion: true, cod_producto: true } },
      },
      orderBy: [{ prioridad: 'desc' }, { costo_desde: 'asc' }],
    });
  }

  async obtenerPorId(id: string, empresaId: string) {
    const regla = await this.prisma.reglas_precio.findFirst({
      where: { id, empresa_id: empresaId },
      include: { lista_precios: { select: { id: true, codigo: true, nombre: true, moneda: true } } },
    });
    if (!regla) {
      throw new HttpException({ success: false, message: 'Regla de precio no encontrada' }, HttpStatus.NOT_FOUND);
    }
    return regla;
  }

  async actualizar(id: string, dto: ActualizarReglaPrecioDto, empresaId: string, userId?: string) {
    const existente = await this.obtenerPorId(id, empresaId);

    if (dto.listaPreciosId) {
      await this.validarListaPertenece(dto.listaPreciosId, empresaId);
    }
    this.validarRangoYMetodo({ ...existente, ...dto } as CrearReglaPrecioDto);

    const data: Prisma.reglas_precioUpdateInput = { updated_at: new Date() };
    if (dto.nombre !== undefined) data.nombre = dto.nombre;
    if (dto.prioridad !== undefined) data.prioridad = dto.prioridad;
    if (dto.alcance !== undefined) {
      data.alcance = dto.alcance;
      data.categoria = dto.alcance === 'categoria' && dto.categoriaId ? { connect: { id: dto.categoriaId } } : { disconnect: true };
      data.marca = dto.alcance === 'marca' && dto.marcaId ? { connect: { id: dto.marcaId } } : { disconnect: true };
      data.producto = dto.alcance === 'producto' && dto.productoId ? { connect: { id: dto.productoId } } : { disconnect: true };
    }
    if (dto.moneda !== undefined) data.moneda = dto.moneda;
    if (dto.costoDesde !== undefined) data.costo_desde = dto.costoDesde;
    if (dto.costoHasta !== undefined) data.costo_hasta = dto.costoHasta ?? null;
    if (dto.metodo !== undefined) data.metodo = dto.metodo;
    if (dto.valor !== undefined) data.valor = dto.valor;
    if (dto.redondeoActivo !== undefined) data.redondeo_activo = dto.redondeoActivo;
    if (dto.redondeoTipo !== undefined) data.redondeo_tipo = dto.redondeoTipo;
    if (dto.redondeoMultiplo !== undefined) data.redondeo_multiplo = dto.redondeoMultiplo;
    if (dto.listaPreciosId !== undefined) data.lista_precios = { connect: { id: dto.listaPreciosId } };
    if (dto.fechaInicio !== undefined) data.fecha_inicio = new Date(dto.fechaInicio);
    if (dto.fechaFin !== undefined) data.fecha_fin = dto.fechaFin ? new Date(dto.fechaFin) : null;

    const actualizado = await this.prisma.reglas_precio.update({ where: { id }, data });

    await this.auditService.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'UPDATE',
      entity_type: 'reglas_precio',
      entity_id: id,
      descripcion: `Regla de precio actualizada: ${actualizado.nombre}`,
      old_value: { ...existente },
      new_value: { ...dto },
    });

    return actualizado;
  }

  async eliminar(id: string, empresaId: string, userId?: string) {
    const existente = await this.obtenerPorId(id, empresaId);
    const desactivada = await this.prisma.reglas_precio.update({
      where: { id },
      data: { activo: false, updated_at: new Date() },
    });

    await this.auditService.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'DEACTIVATE',
      entity_type: 'reglas_precio',
      entity_id: id,
      descripcion: `Regla de precio desactivada: ${existente.nombre}`,
    });

    return desactivada;
  }

  private validarRangoYMetodo(dto: Partial<CrearReglaPrecioDto>) {
    if (dto.costoHasta !== undefined && dto.costoHasta !== null && dto.costoDesde !== undefined) {
      if (Number(dto.costoHasta) <= Number(dto.costoDesde)) {
        throw new HttpException(
          { success: false, message: 'El costo hasta debe ser mayor al costo desde' },
          HttpStatus.BAD_REQUEST,
        );
      }
    }
    if (dto.metodo === 'margen_sobre_precio' && dto.valor !== undefined && Number(dto.valor) >= 100) {
      throw new HttpException(
        { success: false, message: 'El margen sobre precio de venta debe ser menor a 100%' },
        HttpStatus.BAD_REQUEST,
      );
    }
  }

  private async validarListaPertenece(listaId: string, empresaId: string) {
    const lista = await this.prisma.lista_precios.findFirst({ where: { id: listaId, empresa_id: empresaId } });
    if (!lista) {
      throw new HttpException(
        { success: false, message: 'La lista de precios destino no existe o no pertenece a la empresa' },
        HttpStatus.BAD_REQUEST,
      );
    }
    return lista;
  }

  // ============ Cálculo ============

  /**
   * Resuelve la regla aplicable a un producto contra una lista, y calcula el
   * precio sugerido a partir de productos.precio_costo. Considera reglas de
   * alcance 'global' y 'categoria' (categoría específica o padre directo del
   * producto); ante empate desempata por especificidad y luego por prioridad.
   * `empresaId` acota producto y lista al tenant del caller (evita leer datos
   * de otra empresa adivinando un UUID).
   */
  async calcularPrecio(productoId: string, listaPreciosId: string, empresaId: string): Promise<CalculoPrecio> {
    const producto = await this.prisma.productos.findFirst({
      where: { id: productoId, empresa_id: empresaId },
      select: {
        precio_costo: true,
        categoria_id: true,
        marca_id: true,
        empresa_id: true,
        categoria: { select: { padre_id: true } },
      },
    });
    if (!producto) {
      throw new HttpException({ success: false, message: 'Producto no encontrado' }, HttpStatus.NOT_FOUND);
    }

    const precioCosto = Number(producto.precio_costo || 0);
    if (precioCosto <= 0) {
      return {
        precioCosto,
        precioSugerido: null,
        reglaAplicadaId: null,
        reglaAplicadaNombre: null,
        desglose: 'El producto no tiene costo cargado (precio_costo).',
      };
    }

    const lista = await this.prisma.lista_precios.findFirst({
      where: { id: listaPreciosId, empresa_id: empresaId },
      select: { moneda: true },
    });
    if (!lista) {
      throw new HttpException({ success: false, message: 'Lista de precios no encontrada' }, HttpStatus.NOT_FOUND);
    }

    // Categoría del producto y su padre directo (Fase 6, docs/plan-motor-precios-rentabilidad.md §11).
    // Una regla de alcance 'categoria' matchea si su categoria_id es la categoría
    // específica del producto O la categoría padre de esa categoría.
    const categoriaId = producto.categoria_id ?? null;
    const categoriaPadreId = producto.categoria?.padre_id ?? null;

    const alcanceOR: Prisma.reglas_precioWhereInput[] = [{ alcance: 'global' }];
    if (categoriaId) alcanceOR.push({ alcance: 'categoria', categoria_id: categoriaId });
    if (categoriaPadreId) alcanceOR.push({ alcance: 'categoria', categoria_id: categoriaPadreId });

    const candidatas = await this.prisma.reglas_precio.findMany({
      where: {
        empresa_id: empresaId,
        lista_precios_id: listaPreciosId,
        activo: true,
        moneda: lista.moneda,
        OR: alcanceOR,
        costo_desde: { lte: precioCosto },
        AND: [
          { OR: [{ costo_hasta: null }, { costo_hasta: { gt: precioCosto } }] },
          { OR: [{ fecha_fin: null }, { fecha_fin: { gte: new Date() } }] },
        ],
        fecha_inicio: { lte: new Date() },
      },
      orderBy: [{ prioridad: 'desc' }, { costo_desde: 'desc' }],
    });

    // Desempate por especificidad primero: categoría específica > categoría padre >
    // global. Dentro del mismo nivel, se mantiene el desempate anterior (prioridad,
    // luego costo_desde). Ver §11.2 del spec.
    const especificidad = (r: ReglaPrecioRow): number => {
      if (r.alcance === 'categoria' && categoriaId && r.categoria_id === categoriaId) return 2;
      if (r.alcance === 'categoria' && categoriaPadreId && r.categoria_id === categoriaPadreId) return 1;
      return 0;
    };
    const ordenadas = [...(candidatas as ReglaPrecioRow[])].sort((a, b) => {
      const porEspecificidad = especificidad(b) - especificidad(a);
      if (porEspecificidad !== 0) return porEspecificidad;
      const porPrioridad = b.prioridad - a.prioridad;
      if (porPrioridad !== 0) return porPrioridad;
      return Number(b.costo_desde) - Number(a.costo_desde);
    });

    const regla = ordenadas[0];
    if (!regla) {
      return {
        precioCosto,
        precioSugerido: null,
        reglaAplicadaId: null,
        reglaAplicadaNombre: null,
        desglose: `Ningún rango de costo cubre ${precioCosto} ${lista.moneda} en esta lista.`,
      };
    }

    const precioSugerido = this.aplicarRegla(precioCosto, regla);

    return {
      precioCosto,
      precioSugerido,
      reglaAplicadaId: regla.id,
      reglaAplicadaNombre: regla.nombre,
      desglose: this.describirCalculo(precioCosto, regla, precioSugerido),
    };
  }

  private aplicarRegla(precioCosto: number, regla: ReglaPrecioRow): number {
    const valor = Number(regla.valor);
    let precio: number;
    switch (regla.metodo) {
      case 'margen_sobre_precio':
        precio = precioCosto / (1 - valor / 100);
        break;
      case 'precio_fijo':
        precio = valor;
        break;
      case 'margen_sobre_costo':
      default:
        precio = precioCosto * (1 + valor / 100);
        break;
    }

    if (regla.redondeo_activo) {
      const multiplo = Number(regla.redondeo_multiplo) || 1;
      switch (regla.redondeo_tipo) {
        case 'inferior':
          precio = Math.floor(precio / multiplo) * multiplo;
          break;
        case 'normal':
          precio = Math.round(precio / multiplo) * multiplo;
          break;
        case 'superior':
        default:
          precio = Math.ceil(precio / multiplo) * multiplo;
          break;
      }
    }

    return Math.max(precio, 0);
  }

  private describirCalculo(precioCosto: number, regla: ReglaPrecioRow, resultado: number): string {
    const valor = Number(regla.valor);
    switch (regla.metodo) {
      case 'margen_sobre_precio':
        return `Regla "${regla.nombre}": costo ${precioCosto} / (1 - ${valor}%) = ${resultado} (redondeo ${regla.redondeo_tipo})`;
      case 'precio_fijo':
        return `Regla "${regla.nombre}": precio fijo = ${resultado}`;
      default:
        return `Regla "${regla.nombre}": costo ${precioCosto} × (1 + ${valor}%) = ${resultado} (redondeo ${regla.redondeo_tipo})`;
    }
  }

  // ============ Previsualización ============

  async previsualizar(reglaId: string, empresaId: string, limit = 20) {
    const regla = await this.obtenerPorId(reglaId, empresaId);
    const costoDesde = Number(regla.costo_desde);
    const costoHasta = regla.costo_hasta !== null ? Number(regla.costo_hasta) : null;

    const where: Prisma.productosWhereInput = {
      empresa_id: empresaId,
      deleted: false,
      precio_costo: {
        gte: costoDesde,
        ...(costoHasta !== null && { lt: costoHasta }),
      },
      ...(regla.alcance === 'categoria' &&
        regla.categoria_id && {
          OR: [{ categoria_id: regla.categoria_id }, { categoria: { padre_id: regla.categoria_id } }],
        }),
    };

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

    const ejemplos = muestra.map((p) => {
      const precioCosto = Number(p.precio_costo || 0);
      const precioSugerido = this.aplicarRegla(precioCosto, regla as unknown as ReglaPrecioRow);
      return {
        productoId: p.id,
        descripcion: p.descripcion,
        codigo: p.cod_producto,
        precioCosto,
        precioActual: Number(p.precio || 0),
        precioSugerido,
      };
    });

    return {
      regla: { id: regla.id, nombre: regla.nombre, costoDesde, costoHasta },
      totalProductosAfectados: total,
      ejemplos,
    };
  }

  // ============ Recálculo masivo ============

  async recalcularLista(dto: RecalcularListaDto, empresaId: string, userId?: string) {
    await this.validarListaPertenece(dto.listaPreciosId, empresaId);
    const preservarOverride = dto.preservarOverride ?? true;
    const generarCuotas = dto.generarCuotas ?? true;

    // Distinción deliberada: productoIds === undefined => todo el catálogo
    // (comportamiento documentado). productoIds === [] => selección vacía
    // explícita => no debe "caer" a todo el catálogo (evita que un array
    // vacío accidental dispare un recálculo masivo no intencional).
    const whereProducto: Prisma.productosWhereInput = {
      empresa_id: empresaId,
      deleted: false,
      precio_costo: { not: null, gt: 0 },
      ...(dto.productoIds !== undefined && { id: { in: dto.productoIds } }),
    };

    const productos = await this.prisma.productos.findMany({ where: whereProducto, select: { id: true } });

    let actualizados = 0;
    let omitidosPorOverride = 0;
    let sinReglaAplicable = 0;
    let cuotasGeneradas = 0;
    let cuotasRemovidas = 0;

    for (const producto of productos) {
      const existente = await this.prisma.lista_precios_productos.findFirst({
        where: { lista_precios_id: dto.listaPreciosId, producto_id: producto.id },
        select: { id: true, origen_precio: true },
      });

      if (preservarOverride && existente?.origen_precio === 'manual') {
        omitidosPorOverride++;
        continue;
      }

      const calculo = await this.calcularPrecio(producto.id, dto.listaPreciosId, empresaId);
      if (calculo.precioSugerido === null) {
        sinReglaAplicable++;
        continue;
      }

      if (existente) {
        await this.prisma.lista_precios_productos.update({
          where: { id: existente.id },
          data: {
            precio_base: calculo.precioSugerido,
            origen_precio: 'regla',
            regla_precio_id: calculo.reglaAplicadaId,
            fecha_actualizacion: new Date(),
          },
        });
      } else {
        await this.prisma.lista_precios_productos.create({
          data: {
            lista_precios_id: dto.listaPreciosId,
            producto_id: producto.id,
            precio_base: calculo.precioSugerido,
            origen_precio: 'regla',
            regla_precio_id: calculo.reglaAplicadaId,
          },
        });
      }
      actualizados++;

      if (generarCuotas) {
        const { generadas, removidas } = await this.generarGrillaCuotas(producto.id, dto.listaPreciosId, empresaId, userId);
        cuotasGeneradas += generadas;
        cuotasRemovidas += removidas;
      }
    }

    const resultado = {
      lista_precios_id: dto.listaPreciosId,
      total_evaluados: productos.length,
      actualizados,
      omitidos_por_override: omitidosPorOverride,
      sin_regla_aplicable: sinReglaAplicable,
      cuotas_generadas: cuotasGeneradas,
      cuotas_removidas: cuotasRemovidas,
    };

    this.logger.log(`Recálculo de precios en lista ${dto.listaPreciosId}: ${JSON.stringify(resultado)}`);
    await this.auditService.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'UPDATE',
      entity_type: 'lista_precios',
      entity_id: dto.listaPreciosId,
      descripcion: `Recálculo masivo de precios por reglas: ${actualizados} productos actualizados`,
      new_value: resultado,
    });

    return resultado;
  }

  // ============ Grilla de cuotas (auto-generación) ============

  /**
   * Combina el precio contado (de la lista) con cada plan_cuota activo de la
   * empresa cuyo alcance de categoría/rango de precio matchea al producto, para
   * poblar producto_precio_cuota — la tabla que alimenta el dropdown de
   * financiación del ecommerce (estilo "18 x Gs. X"). Además desactiva las filas
   * generadas por este motor en una corrida anterior que ya no matchean (nunca
   * las filas creadas por el CRUD manual de cuotas por producto).
   */
  async generarGrillaCuotas(
    productoId: string,
    listaPreciosId: string,
    empresaId: string,
    userId?: string,
  ): Promise<{ generadas: number; removidas: number }> {
    const precioLista = await this.prisma.lista_precios_productos.findFirst({
      where: { lista_precios_id: listaPreciosId, producto_id: productoId },
      select: { precio_base: true },
    });
    const lista = await this.prisma.lista_precios.findUnique({
      where: { id: listaPreciosId },
      select: { moneda: true },
    });
    if (!precioLista || !lista) return { generadas: 0, removidas: 0 };

    const precioContado = Number(precioLista.precio_base);
    if (precioContado <= 0) return { generadas: 0, removidas: 0 };

    // Categoría del producto y su padre directo (mismo criterio que calcularPrecio,
    // Fase 6 — docs/plan-motor-precios-rentabilidad.md §11).
    const producto = await this.prisma.productos.findUnique({
      where: { id: productoId },
      select: { categoria_id: true, categoria: { select: { padre_id: true } } },
    });
    const categoriaId = producto?.categoria_id ?? null;
    const categoriaPadreId = producto?.categoria?.padre_id ?? null;

    const planesActivos = await this.prisma.planes_cuotas.findMany({
      where: { empresa_id: empresaId, activo: true, cantidad_cuotas: { not: null } },
      select: {
        id: true,
        nombre: true,
        cantidad_cuotas: true,
        tasa_interes: true,
        tipo_interes: true,
        categoria_id: true,
        precio_producto_desde: true,
        precio_producto_hasta: true,
      },
    });

    // Filtro de inclusión (no hay "ganador único" como en reglas_precio): un plan
    // sin categoria_id/rango seteados sigue aplicando a cualquier producto.
    const planes = planesActivos.filter((plan) =>
      planAplicaAProducto(plan, { categoriaId, categoriaPadreId }, precioContado),
    );

    const nombresVigentes = new Set<string>();
    let generadas = 0;
    for (const plan of planes) {
      const cantCuotas = plan.cantidad_cuotas || 1;
      const tasa = Number(plan.tasa_interes || 0);
      // Interés simple sobre el total (Fase 1: mismo criterio usado en planes-cuotas).
      const montoTotal = precioContado * (1 + tasa / 100);
      const montoCuota = Math.ceil(montoTotal / cantCuotas / 100) * 100; // redondeo a 100 hacia arriba

      const nombre = `${cantCuotas} cuota${cantCuotas > 1 ? 's' : ''}`;
      nombresVigentes.add(nombre);
      const existente = await this.prisma.producto_precio_cuota.findFirst({
        where: { producto_id: productoId, nombre, empresa_id: empresaId },
      });

      const data = {
        empresa_id: empresaId,
        producto_id: productoId,
        nombre,
        cant_cuotas: cantCuotas,
        monto_cuota: montoCuota,
        monto_total: montoCuota * cantCuotas,
        moneda: lista.moneda,
        usuario: userId,
        activo: true,
      };

      if (existente) {
        await this.prisma.producto_precio_cuota.update({ where: { id: existente.id }, data: { ...data, updated_at: new Date() } });
      } else {
        await this.prisma.producto_precio_cuota.create({ data });
      }
      generadas++;
    }

    // Filas de una generación anterior cuyo plan ya no matchea (p. ej. alguien le
    // asignó categoría/rango a un plan después de que este producto ya tuviera su
    // fila). Se desactivan, nunca se borran: facturas/solicitudes de crédito
    // históricas referencian estas filas por FK. Ver riesgo en §11.6 del spec.
    //
    // Importante: el barrido SOLO puede tocar filas cuyo nombre coincide con el
    // patrón que este motor genera ("N cuota"/"N cuotas"). Existe un CRUD manual
    // separado (producto-precio-cuotas.service.ts) donde el usuario elige un
    // nombre libre (ej. "3 cuotas sin interés", "PRECIO CONTADO") — sin este
    // guard, el barrido las desactivaría igual, sacándolas silenciosamente de la
    // tienda pública (store-public.service.ts filtra por activo=true).
    const NOMBRE_GENERADO_POR_MOTOR = /^\d+ cuotas?$/;
    const filasExistentes = await this.prisma.producto_precio_cuota.findMany({
      where: { producto_id: productoId, empresa_id: empresaId, activo: true },
      select: { id: true, nombre: true },
    });
    const idsAObsoletar = filasExistentes
      .filter((f) => NOMBRE_GENERADO_POR_MOTOR.test(f.nombre) && !nombresVigentes.has(f.nombre))
      .map((f) => f.id);

    let removidas = 0;
    if (idsAObsoletar.length > 0) {
      const { count } = await this.prisma.producto_precio_cuota.updateMany({
        where: { id: { in: idsAObsoletar } },
        data: { activo: false, updated_at: new Date() },
      });
      removidas = count;
    }

    return { generadas, removidas };
  }
}
