import { decidirVencimiento } from './vencimiento';
import { costoMensualDeSuscripcion } from './costo-mensual';
import { BadRequestException, ConflictException, Injectable, NotFoundException, Logger, Optional } from '@nestjs/common';
import { formatDateResponse, formatDateTimeResponse } from 'src/utils/utilidades';
import { AuditService } from '../audit/audit.service';
import { PrismaService } from '../prisma/prisma.service';
import {
  DIAS_MINIMOS_PRIMER_CICLO,
  calcularPrimerCobro,
  calcularProximoCobro,
} from '../utils/ciclo-suscripcion';
import { AiConfigService } from '../ai-dashboard/ai-config/ai-config.service';
import { EstadoComprobantePago, EstadoSuscripcion, tipoModuloSuscripcion } from './dto/enum-sucripcion';

@Injectable()
export class SuscripcionesService {
  private readonly logger = new Logger(SuscripcionesService.name);
  constructor(
    private prisma: PrismaService,
    private readonly auditService: AuditService,
    // Opcional: SuscripcionesService se provee en varios módulos; solo el contexto que
    // importa AiConfigModule tendrá este servicio. Sin él, se omite la cuota IA.
    @Optional() private readonly aiConfig?: AiConfigService,
  ) {}

  /** Aplica la cuota de tokens IA del plan a la empresa (no lanza). */
  private async aplicarCuotaIaDelPlan(empresaId: string, planId: string) {
    if (!this.aiConfig) return;
    try {
      const plan = await this.prisma.planes.findUnique({ where: { id: planId }, select: { tokens_ia_mensual: true } });
      await this.aiConfig.aplicarCuotaDesdePlan(empresaId, plan?.tokens_ia_mensual ?? 0);
    } catch (err) {
      this.logger.error(`aplicarCuotaIaDelPlan falló (empresa ${empresaId}): ${err instanceof Error ? err.message : 'unknown'}`);
    }
  }

  /**
   * Verifica si `candidatoId` es ancestro (o igual) de `empresaId` en la jerarquía.
   * Sube por parent_id hasta la raíz (máx 20 niveles para evitar bucles).
   */
  private async esAncestroOIgual(empresaId: string, candidatoId: string): Promise<boolean> {
    if (empresaId === candidatoId) return true;
    let currentId: string | null = empresaId;
    let depth = 0;
    while (currentId && depth < 20) {
      const row = await this.prisma.empresas.findUnique({
        where: { id: currentId },
        select: { parent_id: true },
      });
      if (!row) break;
      if (row.parent_id === candidatoId) return true;
      currentId = row.parent_id;
      depth++;
    }
    return false;
  }

  private addDays(date: Date, d: number) {
    const copy = new Date(date);
    copy.setDate(copy.getDate() + d);
    return copy;
  }

  /**
   * Calcula el primer vencimiento sin escribir nada, para que el alta de
   * empresa pueda mostrarlo antes de guardar.
   */
  previewPrimerCobro(fechaInicio: string, diaCobro?: string | number | null) {
    const inicio = fechaInicio ? new Date(`${String(fechaInicio).slice(0, 10)}T00:00:00.000Z`) : new Date();
    if (Number.isNaN(inicio.getTime())) {
      throw new BadRequestException('Fecha de inicio inválida');
    }

    const dia = diaCobro == null || diaCobro === '' ? null : Number(diaCobro);
    if (dia != null && (!Number.isInteger(dia) || dia < 1 || dia > 28)) {
      throw new BadRequestException('El día de cobro debe ser un entero entre 1 y 28');
    }

    const primerCobro = calcularPrimerCobro(inicio, dia);
    return {
      fecha_inicio: inicio.toISOString(),
      primer_cobro: primerCobro.toISOString(),
      dias_primer_periodo: Math.round((primerCobro.getTime() - inicio.getTime()) / 86_400_000),
      dias_minimos: DIAS_MINIMOS_PRIMER_CICLO,
    };
  }

  async create(empresaId: string, planId: string) {
    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
    });
    const plan = await this.prisma.planes.findUnique({
      where: { id: planId },
      include: {
        plan_modulos: true,
        plan_submodulos_addon: { where: { active: true } },
      },
    });

    if (!empresa) throw new NotFoundException('Empresa no encontrada');
    if (!plan) throw new NotFoundException('Plan no encontrado');
    if (plan.plan_modulos.length === 0) throw new NotFoundException('Plan no tiene modulos');

    const { periodo_prueba, precio, plan_modulos, plan_submodulos_addon } = plan;

    const now = new Date();
    const days = Number(periodo_prueba ?? 0);
    const fechaFin = this.addDays(now, days);

    const costoMensual = Number(precio ?? 0);

    // Creamos la suscripcion
    const suscripcion = await this.prisma.suscripciones.create({
      data: {
        empresa_id: empresaId,
        plan_id: planId,
        fecha_inicio: now,
        fecha_fin: fechaFin,
        estado: EstadoSuscripcion.Activo,
        fecha_prox_cobro: fechaFin,
        descripcion: 'Suscripcion plan Inicial',
        costo_mensual: costoMensual,
        // El acordado y el cobrado arrancan iguales. Sin escribirlo, un cambio
        // de módulos posterior no tendría de dónde sacar el precio pactado.
        precio_venta: costoMensual,
      },
    });
    // Asignamos los modulos del plan a la suscripcion
    await this.prisma.$transaction([
      ...(plan_modulos ?? []).map((item) =>
        this.prisma.suscripcion_modulos.create({
          data: {
            suscripcion_id: suscripcion.id,
            modulo_id: item.modulo_id,
            tipo: tipoModuloSuscripcion.Incluido,
            costo_adicional: 0,
          },
        }),
      ),
      ...(plan_submodulos_addon ?? []).map((addon) =>
        this.prisma.suscripcion_submodulos.create({
          data: {
            suscripcion_id: suscripcion.id,
            submodulo_id: addon.submodulo_id,
            precio: addon.precio ?? 0,
            active: true,
          },
        }),
      ),
    ]);

    // Aplicar la cuota de tokens IA que incluye el plan (post-commit, no bloquea).
    await this.aplicarCuotaIaDelPlan(empresaId, planId);

    return suscripcion;
  }

  async changePlan(
    empresaId: string,
    planId: string,
    fechaInicio?: Date,
    motivo?: string,
    userId?: string,
    fechaFinOverride?: Date,
    costoMensualOverride?: number,
    diaCobro?: number,
  ) {
    const now = new Date();
    const start = fechaInicio ?? now;

    const { suscripcion, planAnteriorId, planNuevoNombre } = await this.prisma.$transaction(async (tx) => {
      const empresa = await tx.empresas.findUnique({
        where: { id: empresaId },
      });
      if (!empresa) throw new NotFoundException('Empresa no encontrada');

      const plan = await tx.planes.findUnique({
        where: { id: planId },
        include: {
          plan_modulos: true,
          plan_submodulos_addon: { where: { active: true } },
        },
      });

      if (!plan) throw new Error('Plan no encontrado');
      if (!plan.active) throw new Error('Plan inactivo');
      if (plan.plan_modulos.length === 0) throw new ConflictException('Plan no tiene modulos');

      const activeSub = await tx.suscripciones.findFirst({
        where: {
          empresa_id: empresaId,
          estado: EstadoSuscripcion.Activo,
        },
        orderBy: {
          created_at: 'desc',
        },
      });

      if (activeSub?.plan_id === planId) throw new ConflictException('La empresa ya tiene este plan activo');

      const planAnteriorId = activeSub?.plan_id ?? null;

      if (activeSub) {
        await tx.suscripciones.update({
          where: { id: activeSub.id },
          data: {
            estado: EstadoSuscripcion.Cancelado,
            fecha_cancelacion: now,
            fecha_fin: now,
            updated_at: now,
          },
        });
      }

      const days = Number(plan.periodo_prueba ?? 0);
      const fechaFin = fechaFinOverride ?? this.addDays(start, days);
      const costoMensual = costoMensualOverride ?? Number(plan.precio ?? 0);

      const suscripcion = await tx.suscripciones.create({
        data: {
          empresa_id: empresaId,
          plan_id: planId,
          fecha_inicio: start,
          fecha_fin: fechaFin,
          estado: EstadoSuscripcion.Activo,
          fecha_prox_cobro: fechaFin,
          descripcion: (motivo ?? `Cambio de plan a ${plan.descripcion ?? 'nuevo plan'}`).slice(0, 60),
          costo_mensual: costoMensual,
          // El precio del plan nuevo (o el override negociado) pasa a ser el
          // acordado de esta suscripción: si no, un cambio de módulos posterior
          // tomaría como base el precio pactado para el plan anterior.
          precio_venta: costoMensual,
          ...(diaCobro != null && { dia_cobro: diaCobro }),
        },
      });

      await Promise.all(
        (plan.plan_modulos ?? []).map((item) =>
          tx.suscripcion_modulos.create({
            data: {
              suscripcion_id: suscripcion.id,
              modulo_id: item.modulo_id,
              tipo: tipoModuloSuscripcion.Incluido,
              costo_adicional: 0,
            },
          }),
        ),
      );

      await Promise.all(
        (plan.plan_submodulos_addon ?? []).map((addon) =>
          tx.suscripcion_submodulos.create({
            data: {
              suscripcion_id: suscripcion.id,
              submodulo_id: addon.submodulo_id,
              precio: addon.precio ?? 0,
              active: true,
            },
          }),
        ),
      );

      return { suscripcion, planAnteriorId, planNuevoNombre: plan.descripcion ?? planId };
    });

    await this.auditService.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'CAMBIO_PLAN',
      entity_type: 'suscripcion',
      entity_id: suscripcion.id,
      old_value: planAnteriorId ? { plan_id: planAnteriorId } : undefined,
      new_value: { plan_id: planId, plan_nombre: planNuevoNombre },
      descripcion: motivo
        ? `Cambio de plan a "${planNuevoNombre}" — Motivo: ${motivo}`
        : `Cambio de plan a "${planNuevoNombre}"`,
    });

    // Actualizar la cuota de tokens IA según el nuevo plan (no bloquea).
    await this.aplicarCuotaIaDelPlan(empresaId, planId);

    return suscripcion;
  }

  async createComprobantePagoRecibido(
    suscripcionId: string,
    empresaId: string,
    documento: Buffer,
    mimeType: string,
    observacion?: string,
    monto?: number,
  ) {
    const suscripcion = await this.prisma.suscripciones.findUnique({
      where: { id: suscripcionId },
      select: { id: true, empresa_id: true },
    });

    if (!suscripcion) throw new Error('Suscripción no encontrada');
    if (suscripcion.empresa_id !== empresaId) throw new Error('No tienes permisos para esta suscripción');

    return this.prisma.comprobante_pago_recibidos.create({
      data: {
        empresa_id: empresaId,
        suscripcion_id: suscripcionId,
        observacion: observacion ?? null,
        monto: monto ?? null,
        mime_type: mimeType,
        documento: new Uint8Array(documento),
        estado: 'Pendiente',
        created_at: new Date(),
        updated_at: new Date(),
      },
    });
  }

  // ============================================================
  // Gestión de suscripciones para Holdings y Resellers
  // ============================================================

  /**
   * Obtener suscripciones de todos los subsidiarios de una empresa (holding o reseller).
   * Incluye datos de la empresa, plan, y estado.
   */
  async getSuscripcionesSubsidiarios(
    empresaId: string,
    filtros?: {
      estado?: string;
      busqueda?: string;
      tipo?: string;
      page?: number;
      limit?: number;
    },
  ) {
    // Verificar que la empresa existe y es holding o reseller
    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { id: true, type: true, razon_social: true },
    });

    if (!empresa) throw new NotFoundException('Empresa no encontrada');
    if (empresa.type !== 'holding' && empresa.type !== 'reseller') {
      throw new BadRequestException('Solo holdings y resellers pueden gestionar suscripciones');
    }

    const page = Math.max(1, filtros?.page || 1);
    const limit = Math.min(filtros?.limit || 20, 100);
    const skip = (page - 1) * limit;

    // Obtener subsidiarios directos
    const whereSubsidiarios: any = { parent_id: empresaId };
    if (filtros?.tipo) {
      whereSubsidiarios.type = filtros.tipo;
    }
    if (filtros?.busqueda) {
      whereSubsidiarios.OR = [
        { razon_social: { contains: filtros.busqueda, mode: 'insensitive' } },
        { ruc: { contains: filtros.busqueda } },
      ];
    }

    // Contar total para paginación
    const totalCount = await this.prisma.empresas.count({
      where: whereSubsidiarios,
    });

    const subsidiarios = await this.prisma.empresas.findMany({
      where: whereSubsidiarios,
      skip,
      take: limit,
      select: {
        id: true,
        ruc: true,
        razon_social: true,
        type: true,
        active: true,
        canal_venta: true,
        created_at: true,
        suscripciones: {
          select: {
            id: true,
            plan_id: true,
            estado: true,
            fecha_inicio: true,
            fecha_fin: true,
            fecha_prox_cobro: true,
            costo_mensual: true,
            descripcion: true,
            vendida_por_empresa_id: true,
            comision_aplicada: true,
            precio_venta: true,
            created_at: true,
            planes: {
              select: {
                id: true,
                codigo: true,
                descripcion: true,
                precio: true,
                tipo_ciclo: true,
                max_user: true,
                max_producto: true,
                max_documentos: true,
              },
            },
          },
          orderBy: { created_at: 'desc' },
        },
        // Contar subsidiarios del reseller (si el hijo es reseller)
        other_empresas: {
          where: { active: true },
          select: { id: true },
        },
      },
      orderBy: { razon_social: 'asc' },
    });

    // Filtrar por estado de suscripción si se especificó
    let resultado = subsidiarios.map((sub) => {
      // Buscar la suscripción más relevante: primero Activo, luego Pausado, luego la más reciente
      const found =
        sub.suscripciones.find((s) => s.estado === 'Activo') ||
        sub.suscripciones.find((s) => s.estado === 'Pausado') ||
        sub.suscripciones[0] ||
        null;
      return {
        id: sub.id,
        ruc: sub.ruc,
        razon_social: sub.razon_social,
        type: sub.type,
        active: sub.active,
        canal_venta: sub.canal_venta,
        created_at: formatDateResponse(sub.created_at),
        suscripcion_activa: found
          ? {
              ...found,
              created_at: formatDateResponse(found.created_at),
              fecha_inicio: formatDateResponse(found.fecha_inicio),
              fecha_fin: formatDateResponse(found.fecha_fin),
              fecha_prox_cobro: formatDateResponse(found.fecha_prox_cobro),
            }
          : null,
        total_suscripciones: sub.suscripciones.length,
        total_subsidiarios_propios: sub.other_empresas.length,
      };
    });

    if (filtros?.estado) {
      if (filtros.estado === 'sin_suscripcion') {
        resultado = resultado.filter((r) => !r.suscripcion_activa);
      } else {
        resultado = resultado.filter((r) => r.suscripcion_activa?.estado === filtros.estado);
      }
    }

    const totalPages = Math.ceil(totalCount / limit);

    return {
      empresa_gestora: {
        id: empresa.id,
        type: empresa.type,
        razon_social: empresa.razon_social,
      },
      subsidiarios: resultado,
      pagination: {
        page,
        limit,
        total: totalCount,
        totalPages,
        hasNext: page < totalPages,
        hasPrev: page > 1,
      },
      resumen: {
        total: resultado.length,
        con_suscripcion_activa: resultado.filter((r) => r.suscripcion_activa?.estado === 'Activo').length,
        sin_suscripcion: resultado.filter((r) => !r.suscripcion_activa).length,
        pausadas: resultado.filter((r) => r.suscripcion_activa?.estado === 'Pausado').length,
      },
    };
  }

  /**
   * Obtener planes disponibles para asignar a subsidiarios.
   */
  async getPlanesDisponibles() {
    return this.prisma.planes.findMany({
      where: { active: true },
      select: {
        id: true,
        codigo: true,
        descripcion: true,
        precio: true,
        tipo_ciclo: true,
        max_user: true,
        max_producto: true,
        max_documentos: true,
        periodo_prueba: true,
        plan_modulos: {
          select: {
            modulo_id: true,
            modulos: {
              select: {
                id: true,
                descripcion: true,
              },
            },
          },
        },
      },
      orderBy: { precio: 'asc' },
    });
  }

  /**
   * Crear suscripción para un subsidiario desde holding/reseller.
   * Registra quién vendió y la comisión aplicada.
   */
  async crearSuscripcionParaSubsidiario(
    parentEmpresaId: string,
    subsidiarioId: string,
    planId: string,
    opciones?: {
      precio_venta?: number;
      comision_porcentaje?: number;
      descripcion?: string;
      dias_vigencia?: number;
      fecha_inicio?: Date;
      dia_cobro?: number;
      notas_comerciales?: string;
    },
  ) {
    // Validar jerarquía: el parent debe ser padre directo del subsidiario
    const subsidiario = await this.prisma.empresas.findUnique({
      where: { id: subsidiarioId },
      select: { id: true, parent_id: true, razon_social: true, type: true },
    });

    if (!subsidiario) throw new NotFoundException('Empresa subsidiaria no encontrada');
    if (!(await this.esAncestroOIgual(subsidiarioId, parentEmpresaId))) {
      throw new BadRequestException('No tienes permisos para gestionar esta empresa');
    }

    // Verificar que no tenga suscripción activa
    const suscripcionActiva = await this.prisma.suscripciones.findFirst({
      where: { empresa_id: subsidiarioId, estado: EstadoSuscripcion.Activo },
    });

    if (suscripcionActiva) {
      throw new BadRequestException(
        `${subsidiario.razon_social} ya tiene una suscripción activa. Cancelala o cambiá el plan primero.`,
      );
    }

    // Obtener plan
    const plan = await this.prisma.planes.findUnique({
      where: { id: planId },
      include: { plan_modulos: true },
    });

    if (!plan) throw new NotFoundException('Plan no encontrado');
    if (!plan.active) throw new BadRequestException('El plan no está activo');
    if (plan.plan_modulos.length === 0) throw new BadRequestException('El plan no tiene módulos asignados');

    const now = new Date();
    const fechaInicio = opciones?.fecha_inicio ?? now;
    const costoMensual = opciones?.precio_venta ?? Number(plan.precio ?? 0);
    const comision = opciones?.comision_porcentaje ?? 0;

    const fechaProxCobro = calcularPrimerCobro(fechaInicio, opciones?.dia_cobro);
    const fechaFin = fechaProxCobro; // el período termina cuando vence el próximo cobro

    // Crear suscripción con datos de venta
    const suscripcion = await this.prisma.suscripciones.create({
      data: {
        empresa_id: subsidiarioId,
        plan_id: planId,
        fecha_inicio: fechaInicio,
        fecha_fin: fechaFin,
        estado: EstadoSuscripcion.Activo,
        fecha_prox_cobro: fechaProxCobro,
        descripcion: (opciones?.descripcion ?? `Suscripción ${plan.descripcion}`).slice(0, 60),
        costo_mensual: costoMensual,
        vendida_por_empresa_id: parentEmpresaId,
        comision_aplicada: comision,
        precio_venta: costoMensual,
        precio_costo: Number(plan.precio ?? 0),
        dia_cobro: opciones?.dia_cobro ?? null,
        notas_comerciales: opciones?.notas_comerciales ?? null,
      },
    });

    // Asignar módulos del plan
    await this.prisma.$transaction(
      plan.plan_modulos.map((item) =>
        this.prisma.suscripcion_modulos.create({
          data: {
            suscripcion_id: suscripcion.id,
            modulo_id: item.modulo_id,
            tipo: tipoModuloSuscripcion.Incluido,
            costo_adicional: 0,
          },
        }),
      ),
    );

    return this.prisma.suscripciones.findUnique({
      where: { id: suscripcion.id },
      include: {
        planes: { select: { id: true, descripcion: true, precio: true } },
        empresas: { select: { id: true, ruc: true, razon_social: true } },
      },
    });
  }

  /**
   * Editar una suscripción existente (precio, descripción, fechas).
   * Solo el vendedor o holding puede editar.
   */
  async editarSuscripcion(
    suscripcionId: string,
    parentEmpresaId: string,
    datos: {
      precio_venta?: number;
      descripcion?: string;
      fecha_fin?: Date;
      comision_aplicada?: number;
      fecha_inicio?: Date;
      dia_cobro?: number | null;
      notas_comerciales?: string;
    },
  ) {
    const suscripcion = await this.prisma.suscripciones.findUnique({
      where: { id: suscripcionId },
      include: {
        empresas: { select: { id: true, parent_id: true, razon_social: true } },
      },
    });

    if (!suscripcion) throw new NotFoundException('Suscripción no encontrada');
    if (!(await this.esAncestroOIgual(suscripcion.empresas?.id ?? '', parentEmpresaId))) {
      throw new BadRequestException('No tienes permisos para editar esta suscripción');
    }

    const updateData: any = { updated_at: new Date() };
    if (datos.precio_venta !== undefined) {
      updateData.precio_venta = datos.precio_venta;
      updateData.costo_mensual = datos.precio_venta;
    }
    if (datos.descripcion !== undefined) updateData.descripcion = datos.descripcion.slice(0, 60);
    if (datos.fecha_fin !== undefined) updateData.fecha_fin = datos.fecha_fin;
    if (datos.comision_aplicada !== undefined) updateData.comision_aplicada = datos.comision_aplicada;
    if (datos.fecha_inicio !== undefined) updateData.fecha_inicio = datos.fecha_inicio;
    if (datos.dia_cobro !== undefined) updateData.dia_cobro = datos.dia_cobro;
    if (datos.notas_comerciales !== undefined) updateData.notas_comerciales = datos.notas_comerciales;

    return this.prisma.suscripciones.update({
      where: { id: suscripcionId },
      data: updateData,
      include: {
        planes: { select: { id: true, descripcion: true, precio: true } },
        empresas: { select: { id: true, ruc: true, razon_social: true } },
      },
    });
  }

  /**
   * Cambiar estado de una suscripción (Activar, Pausar, Cancelar).
   * Solo el padre directo puede cambiar el estado.
   */
  async cambiarEstadoSuscripcion(suscripcionId: string, parentEmpresaId: string, nuevoEstado: string) {
    const estadosValidos = [EstadoSuscripcion.Activo, EstadoSuscripcion.Pausado, EstadoSuscripcion.Cancelado];
    if (!estadosValidos.includes(nuevoEstado as EstadoSuscripcion)) {
      throw new BadRequestException(`Estado inválido. Valores permitidos: ${estadosValidos.join(', ')}`);
    }

    const suscripcion = await this.prisma.suscripciones.findUnique({
      where: { id: suscripcionId },
      include: {
        empresas: { select: { id: true, parent_id: true, razon_social: true } },
      },
    });

    if (!suscripcion) throw new NotFoundException('Suscripción no encontrada');
    if (!(await this.esAncestroOIgual(suscripcion.empresas?.id ?? '', parentEmpresaId))) {
      throw new BadRequestException('No tienes permisos para modificar esta suscripción');
    }

    const updateData: any = {
      estado: nuevoEstado,
      updated_at: new Date(),
    };

    if (nuevoEstado === 'Cancelado') {
      updateData.fecha_cancelacion = new Date();
      updateData.fecha_fin = new Date();
    }

    return this.prisma.suscripciones.update({
      where: { id: suscripcionId },
      data: updateData,
      include: {
        planes: { select: { id: true, descripcion: true, precio: true } },
        empresas: { select: { id: true, ruc: true, razon_social: true } },
      },
    });
  }

  // ============================================================
  // Endpoints del Subsidiario
  // ============================================================

  /**
   * Obtener la suscripción actual de una empresa (para el subsidiario).
   */
  async getMiSuscripcion(empresaId: string) {
    const suscripcion = await this.prisma.suscripciones.findFirst({
      where: {
        empresa_id: empresaId,
        estado: {
          in: [
            EstadoSuscripcion.Activo,
            EstadoSuscripcion.EnGracia,
            EstadoSuscripcion.Expirado,
            EstadoSuscripcion.Pausado,
          ],
        },
      },
      orderBy: { created_at: 'desc' },
      include: {
        planes: true,
        suscripcion_modulos: {
          include: {
            modulos: { select: { id: true, codigo: true, descripcion: true } },
          },
        },
      },
    });

    if (!suscripcion) {
      return {
        tiene_suscripcion: false,
        suscripcion: null,
        vencida: true,
        en_gracia: false,
        dias_restantes: 0,
        bloqueada: true,
      };
    }

    const now = new Date();
    const fechaFin = suscripcion.fecha_fin ? new Date(suscripcion.fecha_fin) : null;
    const diasGracia = Number(suscripcion.planes?.dias_gracia ?? 0);
    const vencida = fechaFin ? now > fechaFin : false;
    const estadoStr = String(suscripcion.estado ?? '');

    let enGracia = false;
    let bloqueada = false;
    let diasRestantes = 0;

    if (fechaFin) {
      if (!vencida) {
        diasRestantes = Math.ceil((fechaFin.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
      } else if (diasGracia > 0) {
        const fechaLimiteGracia = this.addDays(fechaFin, diasGracia);
        if (now <= fechaLimiteGracia) {
          enGracia = true;
          diasRestantes = Math.ceil((fechaLimiteGracia.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
        } else {
          bloqueada = true;
        }
      } else {
        bloqueada = true;
      }
    }

    if (estadoStr === 'Expirado' || estadoStr === 'Cancelado') {
      bloqueada = true;
      enGracia = false;
    }
    if (estadoStr === 'Pausado') {
      bloqueada = true;
    }

    return {
      tiene_suscripcion: true,
      suscripcion: {
        id: suscripcion.id,
        estado: suscripcion.estado,
        plan: suscripcion.planes,
        modulos: suscripcion.suscripcion_modulos.map((m) => m.modulos),
        fecha_inicio: suscripcion.fecha_inicio ? suscripcion.fecha_inicio.toISOString().split('T')[0] : null,
        fecha_fin: suscripcion.fecha_fin ? suscripcion.fecha_fin.toISOString().split('T')[0] : null,
        fecha_prox_cobro: suscripcion.fecha_prox_cobro
          ? suscripcion.fecha_prox_cobro.toISOString().split('T')[0]
          : null,
        costo_mensual: suscripcion.costo_mensual,
        descripcion: suscripcion.descripcion,
      },
      vencida,
      en_gracia: enGracia,
      dias_restantes: diasRestantes,
      bloqueada,
    };
  }

  /**
   * Obtener historial de comprobantes de pago (últimos N).
   */
  async getHistorialPagos(empresaId: string, limit = 6) {
    const take = Math.min(limit, 50);
    const comprobantes = await this.prisma.comprobante_pago_recibidos.findMany({
      where: { empresa_id: empresaId },
      orderBy: { created_at: 'desc' },
      take,
      select: {
        id: true,
        observacion: true,
        estado: true,
        monto: true,
        mime_type: true,
        motivo_rechazo: true,
        created_at: true,
        fecha_revision: true,
        suscripciones: {
          select: {
            id: true,
            descripcion: true,
            planes: { select: { descripcion: true } },
          },
        },
      },
    });

    return comprobantes.map((c) => ({
      ...c,
      created_at: formatDateTimeResponse(c.created_at),
      fecha_revision: formatDateTimeResponse(c.fecha_revision),
    }));
  }

  /**
   * Obtener el documento (archivo) de un comprobante de pago.
   */
  async getDocumentoComprobante(comprobanteId: string, empresaId: string, esHolding = false) {
    const comprobante = await this.prisma.comprobante_pago_recibidos.findUnique({
      where: { id: comprobanteId },
      select: {
        id: true,
        documento: true,
        mime_type: true,
        empresa_id: true,
      },
    });

    if (!comprobante) throw new NotFoundException('Comprobante no encontrado');
    if (!esHolding && comprobante.empresa_id !== empresaId) {
      throw new BadRequestException('No tienes permisos para ver este comprobante');
    }
    if (!comprobante.documento) {
      throw new NotFoundException('El comprobante no tiene documento adjunto');
    }

    return {
      buffer: Buffer.from(comprobante.documento),
      mimeType: comprobante.mime_type || 'application/octet-stream',
    };
  }

  // ============================================================
  // Endpoints del Holding (validación de comprobantes)
  // ============================================================

  /**
   * Obtener comprobantes de pago pendientes de validación para el holding.
   */
  async getComprobantesPendientesHolding(
    holdingEmpresaId: string,
    filtros?: {
      estado?: string;
      limit?: number;
      page?: number;
      busqueda?: string;
    },
  ) {
    const holding = await this.prisma.empresas.findUnique({
      where: { id: holdingEmpresaId },
      select: { id: true, type: true },
    });
    if (!holding || (holding.type !== 'holding' && holding.type !== 'reseller')) {
      throw new BadRequestException('Solo holdings o resellers pueden ver comprobantes');
    }

    // Buscar subsidiarios, filtrar por busqueda si se proporcionó
    const subWhere: any = { parent_id: holdingEmpresaId };
    if (filtros?.busqueda) {
      subWhere.OR = [
        { razon_social: { contains: filtros.busqueda, mode: 'insensitive' } },
        { ruc: { contains: filtros.busqueda } },
      ];
    }

    const subsidiarios = await this.prisma.empresas.findMany({
      where: subWhere,
      select: { id: true },
    });
    const subIds = subsidiarios.map((s) => s.id);
    // Incluir también el holding/reseller para ver sus propios comprobantes
    if (!filtros?.busqueda) {
      subIds.push(holdingEmpresaId);
    }
    if (subIds.length === 0) return { data: [], total: 0, page: 1, totalPages: 0 };

    const take = Math.min(filtros?.limit ?? 20, 200);
    const page = Math.max(filtros?.page ?? 1, 1);
    const skip = (page - 1) * take;
    const estadoFiltro = filtros?.estado || EstadoComprobantePago.Pendiente;

    const where = { empresa_id: { in: subIds }, estado: estadoFiltro };

    const [total, comprobantes] = await Promise.all([
      this.prisma.comprobante_pago_recibidos.count({ where }),
      this.prisma.comprobante_pago_recibidos.findMany({
        where,
        orderBy: { created_at: 'desc' },
        skip,
        take,
        select: {
          id: true,
          observacion: true,
          estado: true,
          monto: true,
          mime_type: true,
          motivo_rechazo: true,
          created_at: true,
          fecha_revision: true,
          empresas: { select: { id: true, ruc: true, razon_social: true } },
          suscripciones: {
            select: {
              id: true,
              descripcion: true,
              estado: true,
              fecha_fin: true,
              costo_mensual: true,
              planes: {
                select: {
                  id: true,
                  descripcion: true,
                  precio: true,
                  tipo_ciclo: true,
                },
              },
            },
          },
        },
      }),
    ]);

    const data = comprobantes.map((c) => ({
      ...c,
      created_at: formatDateTimeResponse(c.created_at),
      fecha_revision: formatDateTimeResponse(c.fecha_revision),
      suscripciones: c.suscripciones
        ? {
            ...c.suscripciones,
            fecha_fin: formatDateResponse(c.suscripciones.fecha_fin, true),
          }
        : null,
    }));

    return { data, total, page, totalPages: Math.ceil(total / take) };
  }

  /**
   * Aprobar un comprobante de pago y renovar la suscripción automáticamente.
   */
  async validarComprobante(comprobanteId: string, revisadoPor: string, holdingEmpresaId: string) {
    const comprobante = await this.prisma.comprobante_pago_recibidos.findUnique({
      where: { id: comprobanteId },
      include: {
        suscripciones: {
          include: {
            planes: true,
            empresas: {
              select: { id: true, parent_id: true, razon_social: true },
            },
          },
        },
      },
    });

    if (!comprobante) throw new NotFoundException('Comprobante no encontrado');
    if (String(comprobante.estado ?? '') !== 'Pendiente') {
      throw new BadRequestException('Este comprobante ya fue procesado');
    }
    if (!(await this.esAncestroOIgual(comprobante.suscripciones?.empresas?.id ?? '', holdingEmpresaId))) {
      throw new BadRequestException('No tienes permisos para validar este comprobante');
    }

    const now = new Date();
    await this.prisma.comprobante_pago_recibidos.update({
      where: { id: comprobanteId },
      data: {
        estado: EstadoComprobantePago.Aprobado,
        revisado_por: revisadoPor,
        fecha_revision: now,
        updated_at: now,
      },
    });

    let suscripcionRenovada = null;
    if (comprobante.suscripciones) {
      suscripcionRenovada = await this.renovarSuscripcion(comprobante.suscripciones.id);
    }

    this.logger.log(
      `Comprobante ${comprobanteId} aprobado. Suscripción renovada: ${suscripcionRenovada?.id ?? 'N/A'}`,
      'SuscripcionesService',
    );

    await this.auditService.log({
      empresa_id: holdingEmpresaId,
      user_id: revisadoPor,
      action: 'APPROVE',
      entity_type: 'comprobante_pago',
      entity_id: comprobanteId,
      descripcion: `Comprobante aprobado para ${comprobante.suscripciones?.empresas?.razon_social ?? 'empresa'}`,
    });

    return {
      comprobante_id: comprobanteId,
      suscripcion_renovada: suscripcionRenovada,
    };
  }

  /**
   * Rechazar un comprobante de pago con motivo.
   */
  async rechazarComprobante(comprobanteId: string, revisadoPor: string, holdingEmpresaId: string, motivo: string) {
    const comprobante = await this.prisma.comprobante_pago_recibidos.findUnique({
      where: { id: comprobanteId },
      include: {
        suscripciones: {
          include: {
            empresas: {
              select: { id: true, parent_id: true, razon_social: true },
            },
          },
        },
      },
    });

    if (!comprobante) throw new NotFoundException('Comprobante no encontrado');
    if (String(comprobante.estado ?? '') !== 'Pendiente') {
      throw new BadRequestException('Este comprobante ya fue procesado');
    }
    if (!(await this.esAncestroOIgual(comprobante.suscripciones?.empresas?.id ?? '', holdingEmpresaId))) {
      throw new BadRequestException('No tienes permisos para rechazar este comprobante');
    }

    const now = new Date();
    await this.prisma.comprobante_pago_recibidos.update({
      where: { id: comprobanteId },
      data: {
        estado: EstadoComprobantePago.Rechazado,
        revisado_por: revisadoPor,
        fecha_revision: now,
        motivo_rechazo: motivo,
        updated_at: now,
      },
    });

    this.logger.log(`Comprobante ${comprobanteId} rechazado. Motivo: ${motivo}`, 'SuscripcionesService');

    await this.auditService.log({
      empresa_id: holdingEmpresaId,
      user_id: revisadoPor,
      action: 'REJECT',
      entity_type: 'comprobante_pago',
      entity_id: comprobanteId,
      descripcion: `Comprobante rechazado: ${motivo}`,
    });

    return { success: true };
  }

  /**
   * Registrar un pago manual desde el holding (sin comprobante del subsidiario).
   */
  async registrarPagoManual(
    holdingEmpresaId: string,
    suscripcionId: string,
    registradoPor: string,
    datos: { monto?: number; observacion?: string },
  ) {
    const suscripcion = await this.prisma.suscripciones.findUnique({
      where: { id: suscripcionId },
      include: {
        empresas: {
          select: { id: true, parent_id: true, razon_social: true },
        },
      },
    });

    if (!suscripcion) throw new NotFoundException('Suscripción no encontrada');
    if (!(await this.esAncestroOIgual(suscripcion.empresas?.id ?? '', holdingEmpresaId))) {
      throw new BadRequestException('No tienes permisos para esta suscripción');
    }

    const now = new Date();
    const comprobante = await this.prisma.comprobante_pago_recibidos.create({
      data: {
        empresa_id: suscripcion.empresa_id,
        suscripcion_id: suscripcionId,
        observacion: datos.observacion ?? 'Pago registrado manualmente por el holding',
        monto: datos.monto ?? null,
        estado: EstadoComprobantePago.Aprobado,
        revisado_por: registradoPor,
        registrado_por: registradoPor,
        fecha_revision: now,
        created_at: now,
        updated_at: now,
      },
    });

    const suscripcionRenovada = await this.renovarSuscripcion(suscripcionId);

    this.logger.log(
      `Pago manual registrado para suscripción ${suscripcionId} de ${suscripcion.empresas?.razon_social}`,
      'SuscripcionesService',
    );

    await this.auditService.log({
      empresa_id: holdingEmpresaId,
      user_id: registradoPor,
      action: 'CREATE',
      entity_type: 'pago_manual',
      entity_id: comprobante.id,
      descripcion: `Pago manual para ${suscripcion.empresas?.razon_social}. Monto: ${datos.monto ?? 'N/A'}`,
    });

    return { comprobante, suscripcion_renovada: suscripcionRenovada };
  }

  // ============================================================
  // Renovación y Cron
  // ============================================================

  /**
   * Renovar una suscripción: extiende la fecha_fin según el ciclo del plan.
   */
  async renovarSuscripcion(suscripcionId: string) {
    const suscripcion = await this.prisma.suscripciones.findUnique({
      where: { id: suscripcionId },
      include: { planes: true },
    });
    if (!suscripcion) throw new NotFoundException('Suscripción no encontrada');

    const now = new Date();

    // Base para el siguiente ciclo: la fecha_prox_cobro actual (o ahora si ya venció)
    const baseDate =
      suscripcion.fecha_prox_cobro && new Date(suscripcion.fecha_prox_cobro) > now
        ? new Date(suscripcion.fecha_prox_cobro)
        : now;

    // Renovación: un mes, sin el piso del primer ciclo.
    const nuevaFechaProxCobro = calcularProximoCobro(baseDate, suscripcion.dia_cobro ?? null);
    const nuevaFechaFin = nuevaFechaProxCobro;

    return this.prisma.suscripciones.update({
      where: { id: suscripcionId },
      data: {
        estado: EstadoSuscripcion.Activo,
        fecha_fin: nuevaFechaFin,
        fecha_prox_cobro: nuevaFechaProxCobro,
        updated_at: now,
      },
      include: {
        planes: {
          select: {
            id: true,
            descripcion: true,
            precio: true,
            tipo_ciclo: true,
          },
        },
        empresas: { select: { id: true, ruc: true, razon_social: true } },
      },
    });
  }

  /**
   * Cron: Verificar vencimientos de suscripciones.
   */
  async verificarVencimientos() {
    const now = new Date();
    this.logger.log('Iniciando verificación de vencimientos de suscripciones', 'SuscripcionesCron');

    const suscripcionesVencidas = await this.prisma.suscripciones.findMany({
      where: {
        estado: {
          in: [EstadoSuscripcion.Activo, EstadoSuscripcion.EnGracia],
        },
        fecha_fin: { lt: now },
      },
      include: {
        planes: { select: { dias_gracia: true, descripcion: true, periodo_prueba: true } },
        empresas: { select: { id: true, razon_social: true } },
      },
    });

    let pasadasAGracia = 0;
    let pasadasAExpirado = 0;
    let renovadasGratuitas = 0;

    for (const sub of suscripcionesVencidas) {
      const decision = decidirVencimiento({
        costoMensual: sub.costo_mensual,
        periodoPrueba: sub.planes?.periodo_prueba,
        diasGracia: sub.planes?.dias_gracia,
        estado: String(sub.estado ?? ''),
        fechaFin: new Date(sub.fecha_fin),
        ahora: now,
      });

      if (decision.accion === 'renovar') {
        // Cuenta gratuita: no hay cobro que esperar, así que no vence. Se le
        // empuja el ciclo y sigue activa.
        await this.prisma.suscripciones.update({
          where: { id: sub.id },
          data: {
            estado: EstadoSuscripcion.Activo,
            fecha_fin: decision.nuevaFechaFin,
            fecha_prox_cobro: decision.nuevaFechaFin,
            updated_at: now,
          },
        });
        renovadasGratuitas++;
        this.logger.log(
          `Suscripción ${sub.id} (${sub.empresas?.razon_social}) es gratuita: renovada en vez de expirar`,
          'SuscripcionesCron',
        );
      } else if (decision.accion === 'gracia') {
        await this.prisma.suscripciones.update({
          where: { id: sub.id },
          data: { estado: EstadoSuscripcion.EnGracia, updated_at: now },
        });
        pasadasAGracia++;
        this.logger.warn(
          `Suscripción ${sub.id} (${sub.empresas?.razon_social}) movida a período de gracia`,
          'SuscripcionesCron',
        );
      } else {
        await this.prisma.suscripciones.update({
          where: { id: sub.id },
          data: { estado: EstadoSuscripcion.Expirado, updated_at: now },
        });
        pasadasAExpirado++;
        this.logger.warn(`Suscripción ${sub.id} (${sub.empresas?.razon_social}) EXPIRADA`, 'SuscripcionesCron');
      }
    }

    const resumen = {
      fecha_ejecucion: now.toISOString(),
      total_evaluadas: suscripcionesVencidas.length,
      pasadas_a_gracia: pasadasAGracia,
      pasadas_a_expirado: pasadasAExpirado,
      renovadas_gratuitas: renovadasGratuitas,
      sin_cambios:
        suscripcionesVencidas.length - pasadasAGracia - pasadasAExpirado - renovadasGratuitas,
    };

    this.logger.log(`Verificación completada: ${JSON.stringify(resumen)}`, 'SuscripcionesCron');

    await this.auditService.log({
      action: 'CRON_EXECUTION',
      entity_type: 'verificacion_vencimientos',
      descripcion: `Evaluadas: ${resumen.total_evaluadas}, Gracia: ${resumen.pasadas_a_gracia}, Expiradas: ${resumen.pasadas_a_expirado}`,
      new_value: resumen as unknown as Record<string, unknown>,
    });

    return resumen;
  }

  /**
   * Verificar si una empresa tiene suscripción activa (para el guard de bloqueo).
   */
  async verificarEstadoSuscripcion(empresaId: string) {
    const suscripcion = await this.prisma.suscripciones.findFirst({
      where: {
        empresa_id: empresaId,
        estado: {
          in: [EstadoSuscripcion.Activo, EstadoSuscripcion.EnGracia],
        },
      },
      orderBy: { created_at: 'desc' },
      select: {
        id: true,
        estado: true,
        fecha_fin: true,
        planes: { select: { dias_gracia: true } },
      },
    });

    if (!suscripcion) {
      return { activa: false, bloqueada: true, razon: 'sin_suscripcion' };
    }

    const now = new Date();
    const fechaFin = suscripcion.fecha_fin ? new Date(suscripcion.fecha_fin) : null;
    const diasGracia = Number(suscripcion.planes?.dias_gracia ?? 0);
    const estadoSub = String(suscripcion.estado ?? '');

    if (estadoSub === 'Activo') {
      if (fechaFin && now > fechaFin) {
        if (diasGracia > 0 && now <= this.addDays(fechaFin, diasGracia)) {
          return { activa: true, bloqueada: false, razon: 'en_gracia' };
        }
        return {
          activa: false,
          bloqueada: true,
          razon: 'suscripcion_vencida',
        };
      }
      return { activa: true, bloqueada: false, razon: null };
    }

    if (estadoSub === 'EnGracia') {
      if (fechaFin) {
        const fechaLimiteGracia = this.addDays(fechaFin, diasGracia);
        if (now <= fechaLimiteGracia) {
          return { activa: true, bloqueada: false, razon: 'en_gracia' };
        }
      }
      return { activa: false, bloqueada: true, razon: 'gracia_expirada' };
    }

    return { activa: false, bloqueada: true, razon: 'estado_no_activo' };
  }

  // ============================================================
  // Gestión de módulos adicionales por suscripción
  // ============================================================

  /**
   * Obtener módulos de una empresa buscando su suscripción activa.
   * Conveniente cuando no se tiene el suscripcionId.
   */
  async getModulosByEmpresa(subsidiarioId: string, parentEmpresaId: string) {
    // Verificar jerarquía
    const empresa = await this.prisma.empresas.findUnique({
      where: { id: subsidiarioId },
      select: { id: true, parent_id: true, razon_social: true },
    });
    if (!empresa) throw new NotFoundException('Empresa no encontrada');
    if (!(await this.esAncestroOIgual(subsidiarioId, parentEmpresaId))) {
      throw new BadRequestException('No tienes permisos para ver los módulos de esta empresa');
    }

    // Buscar suscripción activa
    const suscripcion = await this.prisma.suscripciones.findFirst({
      where: {
        empresa_id: subsidiarioId,
        estado: { in: ['Activo', 'EnGracia'] },
      },
      orderBy: { created_at: 'desc' },
      select: { id: true },
    });

    if (!suscripcion) {
      // Retornar todos los módulos sin ninguno activo
      const todosLosModulos = await this.prisma.modulos.findMany({
        where: { active: true },
        select: { id: true, codigo: true, descripcion: true, precio: true },
        orderBy: { codigo: 'asc' },
      });
      return {
        suscripcion: null,
        costos: null,
        empresa: { id: empresa.id, razon_social: empresa.razon_social },
        modulos_actuales: [],
        todos_los_modulos: todosLosModulos.map((m) => ({
          ...m,
          precio: Number(m.precio ?? 0),
          en_suscripcion: false,
          tipo: null,
          costo_adicional: 0,
        })),
      };
    }

    return this.getModulosSuscripcion(suscripcion.id, parentEmpresaId);
  }

  /**
   * Gestionar módulos adicionales por empresa (busca suscripción activa).
   */
  async gestionarModulosByEmpresa(
    subsidiarioId: string,
    parentEmpresaId: string,
    modulosAdicionalesIds: string[],
    userId?: string,
  ) {
    const empresa = await this.prisma.empresas.findUnique({
      where: { id: subsidiarioId },
      select: { id: true, parent_id: true },
    });
    if (!empresa) throw new NotFoundException('Empresa no encontrada');
    if (!(await this.esAncestroOIgual(subsidiarioId, parentEmpresaId))) {
      throw new BadRequestException('No tienes permisos para esta empresa');
    }

    const suscripcion = await this.prisma.suscripciones.findFirst({
      where: {
        empresa_id: subsidiarioId,
        estado: { in: ['Activo', 'EnGracia'] },
      },
      orderBy: { created_at: 'desc' },
      select: { id: true },
    });

    if (!suscripcion) {
      throw new BadRequestException('La empresa no tiene suscripción activa. Asigná un plan primero.');
    }

    return this.gestionarModulosAdicionales(suscripcion.id, parentEmpresaId, modulosAdicionalesIds, userId);
  }

  /**
   * Obtener módulos de una suscripción (incluidos del plan + adicionales).
   * También retorna todos los módulos del sistema para saber cuáles se pueden agregar.
   */
  async getModulosSuscripcion(suscripcionId: string, parentEmpresaId: string) {
    const suscripcion = await this.prisma.suscripciones.findUnique({
      where: { id: suscripcionId },
      include: {
        empresas: { select: { id: true, parent_id: true, razon_social: true } },
        planes: { select: { id: true, descripcion: true, precio: true } },
        suscripcion_modulos: {
          include: {
            modulos: {
              select: {
                id: true,
                codigo: true,
                descripcion: true,
                precio: true,
              },
            },
          },
        },
      },
    });

    if (!suscripcion) throw new NotFoundException('Suscripción no encontrada');
    if (!(await this.esAncestroOIgual(suscripcion.empresas?.id ?? '', parentEmpresaId))) {
      throw new BadRequestException('No tienes permisos para ver los módulos de esta suscripción');
    }

    // Obtener todos los módulos activos del sistema (con precio)
    const todosLosModulos = await this.prisma.modulos.findMany({
      where: { active: true },
      select: { id: true, codigo: true, descripcion: true, precio: true },
      orderBy: { codigo: 'asc' },
    });

    // Mapear módulos actuales de la suscripción
    const modulosSuscripcion = suscripcion.suscripcion_modulos.map((sm) => ({
      id: sm.id,
      modulo_id: sm.modulo_id,
      codigo: sm.modulos?.codigo,
      descripcion: sm.modulos?.descripcion,
      precio: Number(sm.modulos?.precio ?? 0),
      tipo: sm.tipo, // 'Incluido' | 'Adicional'
      activo: sm.activo,
      costo_adicional: Number(sm.costo_adicional ?? 0),
    }));

    const moduloIdsEnSuscripcion = modulosSuscripcion.filter((m) => m.activo !== false).map((m) => m.modulo_id);

    // Calcular resumen de costos
    const costoBasePlan = Number(suscripcion.planes?.precio ?? 0);
    const costoAdicionalesActual = modulosSuscripcion
      .filter((m) => String(m.tipo) === String(tipoModuloSuscripcion.Adicional) && m.activo !== false)
      .reduce((sum, m) => sum + m.costo_adicional, 0);

    return {
      suscripcion: {
        id: suscripcion.id,
        empresa: suscripcion.empresas,
        plan: suscripcion.planes,
        costo_mensual: Number(suscripcion.costo_mensual ?? 0),
      },
      costos: {
        costo_base_plan: costoBasePlan,
        costo_adicionales: costoAdicionalesActual,
        costo_total: Number(suscripcion.costo_mensual ?? 0),
      },
      modulos_actuales: modulosSuscripcion,
      todos_los_modulos: todosLosModulos.map((m) => ({
        ...m,
        precio: Number(m.precio ?? 0),
        en_suscripcion: moduloIdsEnSuscripcion.includes(m.id),
        tipo: modulosSuscripcion.find((sm) => sm.modulo_id === m.id && sm.activo !== false)?.tipo ?? null,
        costo_adicional: Number(
          modulosSuscripcion.find((sm) => sm.modulo_id === m.id && sm.activo !== false)?.costo_adicional ?? 0,
        ),
      })),
    };
  }

  /**
   * Agregar o quitar módulos adicionales de una suscripción.
   * Los módulos "Incluido" (del plan) no se pueden quitar.
   * @param modulosAdicionalesIds - IDs de módulos que deben quedar como adicionales
   */
  async gestionarModulosAdicionales(
    suscripcionId: string,
    parentEmpresaId: string,
    modulosAdicionalesIds: string[],
    userId?: string,
  ) {
    const suscripcion = await this.prisma.suscripciones.findUnique({
      where: { id: suscripcionId },
      include: {
        empresas: { select: { id: true, parent_id: true, razon_social: true } },
        planes: { select: { id: true, precio: true } },
        suscripcion_modulos: {
          include: {
            modulos: {
              select: {
                id: true,
                codigo: true,
                descripcion: true,
                precio: true,
              },
            },
          },
        },
      },
    });

    if (!suscripcion) throw new NotFoundException('Suscripción no encontrada');
    if (!(await this.esAncestroOIgual(suscripcion.empresas?.id ?? '', parentEmpresaId))) {
      throw new BadRequestException('No tienes permisos para gestionar los módulos de esta suscripción');
    }

    // Separar módulos incluidos (del plan, intocables) y adicionales actuales
    const modulosIncluidos = suscripcion.suscripcion_modulos.filter((sm) => sm.tipo === tipoModuloSuscripcion.Incluido);
    const modulosAdicionalesActuales = suscripcion.suscripcion_modulos.filter(
      (sm) => sm.tipo === tipoModuloSuscripcion.Adicional,
    );

    // IDs de módulos que ya están incluidos en el plan (no se tocan)
    const idsIncluidos = modulosIncluidos.map((m) => m.modulo_id);

    // Filtrar: no permitir agregar como adicional algo que ya está incluido
    const idsAdicionalesDeseados = modulosAdicionalesIds.filter((id) => !idsIncluidos.includes(id));

    // Módulos adicionales a AGREGAR (están en deseados pero no en actuales)
    const idsAdicionalesActuales = modulosAdicionalesActuales.map((m) => m.modulo_id);
    const aAgregar = idsAdicionalesDeseados.filter((id) => !idsAdicionalesActuales.includes(id));

    // Módulos adicionales a QUITAR (están en actuales pero no en deseados)
    const aQuitar = modulosAdicionalesActuales.filter((m) => !idsAdicionalesDeseados.includes(m.modulo_id));

    // Obtener precios de los módulos que se van a agregar
    const modulosConPrecio =
      aAgregar.length > 0
        ? await this.prisma.modulos.findMany({
            where: { id: { in: aAgregar } },
            select: { id: true, precio: true },
          })
        : [];
    const preciosPorModulo = new Map(modulosConPrecio.map((m) => [m.id, Number(m.precio ?? 0)]));

    const now = new Date();
    const operaciones: any[] = [];

    // Crear nuevos módulos adicionales con su costo real
    for (const moduloId of aAgregar) {
      const costoModulo = preciosPorModulo.get(moduloId) ?? 0;
      operaciones.push(
        this.prisma.suscripcion_modulos.create({
          data: {
            suscripcion_id: suscripcionId,
            modulo_id: moduloId,
            tipo: tipoModuloSuscripcion.Adicional,
            activo: true,
            costo_adicional: costoModulo,
            created_at: now,
            updated_at: now,
          },
        }),
      );
    }

    // Eliminar módulos adicionales que ya no se desean
    for (const modulo of aQuitar) {
      operaciones.push(
        this.prisma.suscripcion_modulos.delete({
          where: { id: modulo.id },
        }),
      );
    }

    // Recalcular costo mensual: precio ACORDADO + adicionales que quedan.
    // Antes partía del precio de lista del plan, así que tocar los módulos de una
    // cuenta con descuento le borraba el descuento y la pasaba a tarifa plena.

    // Costos de los adicionales que se mantienen (no se quitan)
    const idsAQuitar = new Set(aQuitar.map((m) => m.id));
    const costoAdicionalesQueSeQuedan = modulosAdicionalesActuales
      .filter((m) => !idsAQuitar.has(m.id))
      .reduce((sum, m) => sum + Number(m.costo_adicional ?? 0), 0);

    // Costos de los nuevos módulos que se agregan
    const costoNuevosAdicionales = aAgregar.reduce((sum, id) => sum + (preciosPorModulo.get(id) ?? 0), 0);

    const nuevoCostoMensual = costoMensualDeSuscripcion({
      precioVenta: suscripcion.precio_venta,
      precioPlan: suscripcion.planes?.precio,
      adicionales: costoAdicionalesQueSeQuedan + costoNuevosAdicionales,
    });

    // Actualizar costo mensual de la suscripción
    operaciones.push(
      this.prisma.suscripciones.update({
        where: { id: suscripcionId },
        data: {
          costo_mensual: nuevoCostoMensual,
          updated_at: now,
        },
      }),
    );

    if (operaciones.length > 0) {
      await this.prisma.$transaction(operaciones);
    }

    // Registrar en historial
    const cambios: string[] = [];
    if (aAgregar.length > 0) cambios.push(`+${aAgregar.length} módulos`);
    if (aQuitar.length > 0) cambios.push(`-${aQuitar.length} módulos`);
    const costoAnterior = Number(suscripcion.costo_mensual ?? 0);
    if (costoAnterior !== nuevoCostoMensual) {
      cambios.push(`costo: ${costoAnterior.toLocaleString()} → ${nuevoCostoMensual.toLocaleString()}`);
    }

    if (cambios.length > 0) {
      this.logger.log(
        `Módulos adicionales actualizados para suscripción ${suscripcionId} (${suscripcion.empresas?.razon_social}): ${cambios.join(', ')}`,
        'SuscripcionesService',
      );

      await this.auditService.log({
        empresa_id: parentEmpresaId,
        user_id: userId,
        action: 'UPDATE',
        entity_type: 'suscripcion_modulos',
        entity_id: suscripcionId,
        descripcion: `Módulos adicionales actualizados para ${suscripcion.empresas?.razon_social}: ${cambios.join(', ')}`,
      });
    }

    // Retornar estado actualizado
    return this.getModulosSuscripcion(suscripcionId, parentEmpresaId);
  }

  /**
   * Lista los addons disponibles según el plan de la suscripción + estado actual
   * de cada uno (activo/inactivo + precio efectivo) en `suscripcion_submodulos`.
   * Solo accesible por holding/vendedor ancestro de la empresa de la suscripción.
   */
  async getAddonsSuscripcionHolding(suscripcionId: string, parentEmpresaId: string) {
    const suscripcion = await this.prisma.suscripciones.findUnique({
      where: { id: suscripcionId },
      include: {
        empresas: { select: { id: true, razon_social: true, parent_id: true } },
        planes: {
          select: {
            id: true,
            descripcion: true,
            plan_submodulos_addon: {
              where: { active: true },
              select: {
                submodulo_id: true,
                precio: true,
                submodulos: {
                  select: {
                    id: true,
                    codigo: true,
                    descripcion: true,
                    icono: true,
                    precio_addon: true,
                    modulos: { select: { codigo: true, descripcion: true } },
                  },
                },
              },
            },
          },
        },
      },
    });

    if (!suscripcion) throw new NotFoundException('Suscripción no encontrada');
    if (!(await this.esAncestroOIgual(suscripcion.empresas?.id ?? '', parentEmpresaId))) {
      throw new BadRequestException('No tienes permisos para ver esta suscripción');
    }

    const overrides = await this.prisma.suscripcion_submodulos.findMany({
      where: { suscripcion_id: suscripcionId },
      select: { submodulo_id: true, active: true, precio: true },
    });
    const overrideMap = new Map(overrides.map((o) => [o.submodulo_id, o]));

    const addons = (suscripcion.planes?.plan_submodulos_addon ?? []).map((a) => {
      const ov = overrideMap.get(a.submodulo_id);
      const precioPlan = a.precio != null ? Number(a.precio) : null;
      const precioDefault = a.submodulos.precio_addon != null ? Number(a.submodulos.precio_addon) : null;
      const precioOverride = ov?.precio != null ? Number(ov.precio) : null;
      return {
        submodulo_id: a.submodulo_id,
        codigo: a.submodulos.codigo,
        descripcion: a.submodulos.descripcion,
        icono: a.submodulos.icono,
        modulo_codigo: a.submodulos.modulos?.codigo ?? null,
        modulo_descripcion: a.submodulos.modulos?.descripcion ?? null,
        precio_default: precioDefault,
        precio_plan: precioPlan,
        precio_override: precioOverride,
        precio_efectivo: precioOverride ?? precioPlan ?? precioDefault,
        active: ov?.active ?? false,
      };
    });

    return {
      suscripcion_id: suscripcionId,
      empresa: {
        id: suscripcion.empresas?.id,
        razon_social: suscripcion.empresas?.razon_social,
      },
      plan: {
        id: suscripcion.planes?.id,
        descripcion: suscripcion.planes?.descripcion,
      },
      addons,
    };
  }

  /**
   * Sincroniza los addons (suscripcion_submodulos) de una suscripción a partir del body del holding.
   * Cada item: { submodulo_id, active, precio? }.
   * - Si `active=true`: upsert con `precio` provisto (o null para usar default del plan).
   * - Si `active=false`: marca la fila existente con `active=false` (preserva historial).
   */
  async updateAddonsSuscripcionHolding(
    suscripcionId: string,
    parentEmpresaId: string,
    body: { addons: Array<{ submodulo_id: string; active: boolean; precio?: number | null }> },
    userId?: string,
  ) {
    const suscripcion = await this.prisma.suscripciones.findUnique({
      where: { id: suscripcionId },
      include: {
        empresas: { select: { id: true, razon_social: true, parent_id: true } },
      },
    });
    if (!suscripcion) throw new NotFoundException('Suscripción no encontrada');
    if (!(await this.esAncestroOIgual(suscripcion.empresas?.id ?? '', parentEmpresaId))) {
      throw new BadRequestException('No tienes permisos para editar esta suscripción');
    }

    const items = body?.addons ?? [];
    if (!Array.isArray(items)) throw new BadRequestException('Body inválido: se espera { addons: [] }');

    const planAddons = await this.prisma.plan_submodulos_addon.findMany({
      where: { plan_id: suscripcion.plan_id ?? undefined, active: true },
      select: { submodulo_id: true },
    });
    const planAddonIds = new Set(planAddons.map((a) => a.submodulo_id));

    await this.prisma.$transaction(async (tx) => {
      for (const item of items) {
        if (!planAddonIds.has(item.submodulo_id)) {
          throw new BadRequestException(`Submódulo ${item.submodulo_id} no es addon del plan contratado`);
        }
        const precio = item.precio != null ? item.precio : 0;
        const existing = await tx.suscripcion_submodulos.findFirst({
          where: { suscripcion_id: suscripcionId, submodulo_id: item.submodulo_id },
          select: { id: true },
        });
        if (existing) {
          await tx.suscripcion_submodulos.update({
            where: { id: existing.id },
            data: { active: item.active, precio },
          });
        } else if (item.active) {
          await tx.suscripcion_submodulos.create({
            data: {
              suscripcion_id: suscripcionId,
              submodulo_id: item.submodulo_id,
              active: true,
              precio,
            },
          });
        }
      }
    });

    await this.auditService.log({
      empresa_id: suscripcion.empresas?.id ?? '',
      user_id: userId,
      action: 'UPDATE',
      entity_type: 'suscripcion_submodulos',
      entity_id: suscripcionId,
      new_value: { addons: items },
      descripcion: `Addons actualizados para ${suscripcion.empresas?.razon_social ?? suscripcionId} (${items.length} cambios)`,
    });

    return this.getAddonsSuscripcionHolding(suscripcionId, parentEmpresaId);
  }

  /**
   * Lista los submódulos activos para una empresa, combinando:
   * - Submódulos no-addon de los módulos raíz incluidos en la suscripción.
   * - Submódulos addon contratados explícitamente en `suscripcion_submodulos`.
   *
   * Devuelve estructura plana lista para consumir por frontend/guards.
   */
  async getSubmodulosActivos(empresaId: string) {
    // Holdings/resellers bypasean el plan (no tienen suscripción propia):
    // devolvemos todos los submódulos activos como si estuvieran contratados.
    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { type: true },
    });
    if (empresa?.type === 'holding' || empresa?.type === 'reseller') {
      const todos = await this.prisma.submodulos.findMany({
        where: { active: true, modulos: { active: true } },
        select: {
          id: true,
          codigo: true,
          descripcion: true,
          icono: true,
          orden: true,
          es_addon: true,
          modulo_id: true,
          modulos: { select: { codigo: true, descripcion: true } },
        },
        orderBy: { orden: 'asc' },
      });
      return todos.map((s) => ({
        id: s.id,
        codigo: s.codigo,
        descripcion: s.descripcion,
        icono: s.icono,
        orden: s.orden,
        es_addon: s.es_addon,
        modulo_id: s.modulo_id,
        modulo_codigo: s.modulos?.codigo ?? null,
        modulo_descripcion: s.modulos?.descripcion ?? null,
        precio: null as number | null,
        origen: 'BYPASS_HOLDING' as const,
      }));
    }

    const suscripcion = await this.prisma.suscripciones.findFirst({
      where: {
        empresa_id: empresaId,
        estado: { in: [EstadoSuscripcion.Activo, EstadoSuscripcion.EnGracia] },
      },
      orderBy: { created_at: 'desc' },
      select: { id: true },
    });

    if (!suscripcion) return [];

    const [modulosRaiz, addons, exclusiones] = await Promise.all([
      this.prisma.suscripcion_modulos.findMany({
        where: {
          suscripcion_id: suscripcion.id,
          activo: true,
          modulos: { active: true },
        },
        select: {
          modulos: {
            select: {
              id: true,
              codigo: true,
              descripcion: true,
              submodulos: {
                where: { active: true, es_addon: false },
                select: {
                  id: true,
                  codigo: true,
                  descripcion: true,
                  icono: true,
                  orden: true,
                  es_addon: true,
                  modulo_id: true,
                },
              },
            },
          },
        },
      }),
      this.prisma.suscripcion_submodulos.findMany({
        where: {
          suscripcion_id: suscripcion.id,
          active: true,
          submodulos: { active: true, es_addon: true },
        },
        select: {
          precio: true,
          submodulos: {
            select: {
              id: true,
              codigo: true,
              descripcion: true,
              icono: true,
              orden: true,
              es_addon: true,
              modulo_id: true,
              modulos: { select: { codigo: true, descripcion: true } },
            },
          },
        },
      }),
      this.prisma.suscripcion_submodulos.findMany({
        where: {
          suscripcion_id: suscripcion.id,
          active: false,
          submodulos: { es_addon: false },
        },
        select: { submodulo_id: true },
      }),
    ]);

    const excluidosSet = new Set(exclusiones.map((e) => e.submodulo_id));

    const incluidos = modulosRaiz.flatMap((sm) =>
      (sm.modulos?.submodulos ?? [])
        .filter((sub) => !excluidosSet.has(sub.id))
        .map((sub) => ({
        id: sub.id,
        codigo: sub.codigo,
        descripcion: sub.descripcion,
        icono: sub.icono,
        orden: sub.orden,
        es_addon: false as const,
        modulo_id: sub.modulo_id,
        modulo_codigo: sm.modulos!.codigo,
        modulo_descripcion: sm.modulos!.descripcion,
        precio: null as number | null,
        origen: 'INCLUIDO' as const,
      })),
    );

    const adicionales = addons.map((a) => ({
      id: a.submodulos.id,
      codigo: a.submodulos.codigo,
      descripcion: a.submodulos.descripcion,
      icono: a.submodulos.icono,
      orden: a.submodulos.orden,
      es_addon: true as const,
      modulo_id: a.submodulos.modulo_id,
      modulo_codigo: a.submodulos.modulos?.codigo ?? null,
      modulo_descripcion: a.submodulos.modulos?.descripcion ?? null,
      precio: a.precio != null ? Number(a.precio) : null,
      origen: 'ADDON' as const,
    }));

    const map = new Map<string, (typeof incluidos)[number] | (typeof adicionales)[number]>();
    [...incluidos, ...adicionales].forEach((s) => map.set(s.codigo, s));
    return Array.from(map.values()).sort((a, b) => (a.orden ?? 0) - (b.orden ?? 0));
  }

  /**
   * True si la empresa tiene acceso al submódulo (vía módulo raíz incluido o addon contratado).
   */
  async hasAccessToSubmodulo(empresaId: string, submoduloCodigo: string): Promise<boolean> {
    const submodulo = await this.prisma.submodulos.findFirst({
      where: { codigo: submoduloCodigo, active: true },
      select: { id: true, modulo_id: true, es_addon: true },
    });
    if (!submodulo) return false;

    // Holdings/resellers bypasean el plan.
    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { type: true },
    });
    if (empresa?.type === 'holding' || empresa?.type === 'reseller') return true;

    const suscripcion = await this.prisma.suscripciones.findFirst({
      where: { empresa_id: empresaId, estado: EstadoSuscripcion.Activo },
      orderBy: { created_at: 'desc' },
      select: { id: true },
    });
    if (!suscripcion) return false;

    if (submodulo.es_addon) {
      const addon = await this.prisma.suscripcion_submodulos.findFirst({
        where: {
          suscripcion_id: suscripcion.id,
          submodulo_id: submodulo.id,
          active: true,
        },
        select: { id: true },
      });
      return !!addon;
    }

    const modulo = await this.prisma.suscripcion_modulos.findFirst({
      where: {
        suscripcion_id: suscripcion.id,
        modulo_id: submodulo.modulo_id,
        activo: true,
      },
      select: { id: true },
    });
    if (!modulo) return false;

    const exclusion = await this.prisma.suscripcion_submodulos.findFirst({
      where: {
        suscripcion_id: suscripcion.id,
        submodulo_id: submodulo.id,
        active: false,
      },
      select: { id: true },
    });
    return !exclusion;
  }

  /**
   * Lista submódulos no-addon de los módulos incluidos en la suscripción
   * con su estado de inclusión (active=true por default; false si está excluido).
   * Solo accesible por holding/vendedor ancestro de la empresa.
   */
  async getSubmodulosIncluidosSuscripcionHolding(
    suscripcionId: string,
    parentEmpresaId: string,
  ) {
    const suscripcion = await this.prisma.suscripciones.findUnique({
      where: { id: suscripcionId },
      include: {
        empresas: { select: { id: true, razon_social: true } },
        planes: { select: { id: true, descripcion: true } },
      },
    });
    if (!suscripcion) throw new NotFoundException('Suscripción no encontrada');
    if (!(await this.esAncestroOIgual(suscripcion.empresas?.id ?? '', parentEmpresaId))) {
      throw new BadRequestException('No tienes permisos para ver esta suscripción');
    }

    // Catálogo completo de módulos activos con sus submódulos no-addon.
    // Incluimos también módulos NO contratados aún para soportar previsualización
    // de submódulos cuando el usuario los marca como "Adicional" antes de guardar.
    const todosModulos = await this.prisma.modulos.findMany({
      where: { active: true },
      select: {
        id: true,
        codigo: true,
        descripcion: true,
        icono: true,
        orden: true,
        submodulos: {
          where: { active: true, es_addon: false },
          select: {
            id: true,
            codigo: true,
            descripcion: true,
            icono: true,
            orden: true,
          },
          orderBy: { orden: 'asc' },
        },
      },
    });

    const overrides = await this.prisma.suscripcion_submodulos.findMany({
      where: { suscripcion_id: suscripcionId, submodulos: { es_addon: false } },
      select: { submodulo_id: true, active: true },
    });
    const overrideMap = new Map(overrides.map((o) => [o.submodulo_id, o.active]));

    const modulos = todosModulos
      .map((mod) => ({
        modulo_id: mod.id,
        modulo_codigo: mod.codigo,
        modulo_descripcion: mod.descripcion,
        modulo_icono: mod.icono,
        modulo_orden: mod.orden,
        submodulos: (mod.submodulos ?? []).map((sub) => ({
          submodulo_id: sub.id,
          codigo: sub.codigo,
          descripcion: sub.descripcion,
          icono: sub.icono,
          orden: sub.orden,
          active: overrideMap.has(sub.id) ? overrideMap.get(sub.id)! : true,
        })),
      }))
      .sort((a, b) => (a.modulo_orden ?? 0) - (b.modulo_orden ?? 0));

    return {
      suscripcion_id: suscripcionId,
      empresa: {
        id: suscripcion.empresas?.id,
        razon_social: suscripcion.empresas?.razon_social,
      },
      plan: {
        id: suscripcion.planes?.id,
        descripcion: suscripcion.planes?.descripcion,
      },
      modulos,
    };
  }

  /**
   * Sincroniza la inclusión/exclusión de submódulos no-addon de una suscripción.
   * Body: { items: [{ submodulo_id, active }] }
   * - active=true: elimina el override (vuelve al default incluido por el módulo).
   * - active=false: upsert con active=false (excluido para esta empresa).
   */
  async updateSubmodulosIncluidosSuscripcionHolding(
    suscripcionId: string,
    parentEmpresaId: string,
    body: { items: Array<{ submodulo_id: string; active: boolean }> },
    userId?: string,
  ) {
    const suscripcion = await this.prisma.suscripciones.findUnique({
      where: { id: suscripcionId },
      include: { empresas: { select: { id: true, razon_social: true } } },
    });
    if (!suscripcion) throw new NotFoundException('Suscripción no encontrada');
    if (!(await this.esAncestroOIgual(suscripcion.empresas?.id ?? '', parentEmpresaId))) {
      throw new BadRequestException('No tienes permisos para editar esta suscripción');
    }

    const items = body?.items ?? [];
    if (!Array.isArray(items)) throw new BadRequestException('Body inválido: se espera { items: [] }');

    const submoduloIds = items.map((i) => i.submodulo_id);
    if (submoduloIds.length === 0) {
      return this.getSubmodulosIncluidosSuscripcionHolding(suscripcionId, parentEmpresaId);
    }

    const submodulosValidos = await this.prisma.submodulos.findMany({
      where: {
        id: { in: submoduloIds },
        es_addon: false,
        active: true,
        modulos: {
          suscripcion_modulos: {
            some: { suscripcion_id: suscripcionId, activo: true },
          },
        },
      },
      select: { id: true },
    });
    const validIds = new Set(submodulosValidos.map((s) => s.id));

    await this.prisma.$transaction(async (tx) => {
      for (const item of items) {
        if (!validIds.has(item.submodulo_id)) {
          throw new BadRequestException(
            `Submódulo ${item.submodulo_id} no pertenece a un módulo incluido en la suscripción`,
          );
        }
        const existing = await tx.suscripcion_submodulos.findFirst({
          where: { suscripcion_id: suscripcionId, submodulo_id: item.submodulo_id },
          select: { id: true },
        });
        if (item.active) {
          if (existing) {
            await tx.suscripcion_submodulos.delete({ where: { id: existing.id } });
          }
        } else {
          if (existing) {
            await tx.suscripcion_submodulos.update({
              where: { id: existing.id },
              data: { active: false, precio: 0 },
            });
          } else {
            await tx.suscripcion_submodulos.create({
              data: {
                suscripcion_id: suscripcionId,
                submodulo_id: item.submodulo_id,
                active: false,
                precio: 0,
              },
            });
          }
        }
      }
    });

    await this.auditService.log({
      empresa_id: suscripcion.empresas?.id ?? '',
      user_id: userId,
      action: 'UPDATE',
      entity_type: 'suscripcion_submodulos_incluidos',
      entity_id: suscripcionId,
      new_value: { items },
      descripcion: `Submódulos incluidos actualizados para ${suscripcion.empresas?.razon_social ?? suscripcionId} (${items.length} cambios)`,
    });

    return this.getSubmodulosIncluidosSuscripcionHolding(suscripcionId, parentEmpresaId);
  }
}
