import { Response, NextFunction } from 'express';
import { User, Users } from '../schema/users.schema'; // Adjust path as needed
import { AuthenticatedUserRequest } from '../../../libs/middlewares/types';
import mongoose from 'mongoose';
import { UserDisplayPicture } from '../schema/userDp.schema';
import sharp from 'sharp';
import {
  fetchConfigJwtGenerator,
  jwtGeneratorForNewAccountPassword,
  jwtGeneratorForValidateEmailLink,
  mailJwtGenerator,
} from '../../../libs/utils/createJwt';
import {
  BadRequestError,
  InternalServerError,
  LargePayloadError,
  NotFoundError,
  UnauthorizedError,
} from '../../../libs/errors/http.errors';
import { inject, injectable } from 'inversify';
import { MailService } from '../services/mail.service';
import {
  EntitiesEventProducer,
  Event,
  EventType,
  UserAddedEvent,
  UserDeletedEvent,
  UserUpdatedEvent,
  SyncAction,
} from '../services/entity_events.service';
import { Logger } from '../../../libs/services/logger.service';
import { AppConfig } from '../../tokens_manager/config/config';
import { UserGroups } from '../schema/userGroup.schema';
import type {
  GraphUserListResponse,
  UserGroupSummary,
} from '../types/user_management.types';
import { safeParsePagination } from '../../../utils/safe-integer';
import { buildPaginationMetadata } from '../../enterprise_search/utils/utils';
import { AuthService } from '../services/auth.service';
import { Org } from '../schema/org.schema';
import { UserCredentials } from '../../auth/schema/userCredentials.schema';
import { AICommandOptions } from '../../../libs/commands/ai_service/ai.service.command';
import { AIServiceCommand } from '../../../libs/commands/ai_service/ai.service.command';
import { HttpMethod } from '../../../libs/enums/http-methods.enum';
import { HTTP_STATUS } from '../../../libs/enums/http-status.enum';
import { validateNoFormatSpecifiers, validateNoXSS } from '../../../utils/xss-sanitization';
import {
  OAuthApp,
  OAuthAppStatus,
} from '../../oauth_provider/schema/oauth.app.schema';
import { resolveOAuthTokenService } from '../../../libs/services/oauth-token-service.provider';

@injectable()
export class UserController {
  constructor(
    @inject('AppConfig') private config: AppConfig,
    @inject('MailService') private mailService: MailService,
    @inject('AuthService') private authService: AuthService,
    @inject('Logger') private logger: Logger,
    @inject('EntitiesEventProducer')
    private eventService: EntitiesEventProducer,
  ) { }

  async getAllUsers(
    req: AuthenticatedUserRequest,
    res: Response,
  ): Promise<void> {
    const { blocked } = req.query;
    if (blocked === 'true') {

      const blockedUsers = await UserCredentials.aggregate([
        {
          $match: {
            orgId: req.user?.orgId,
            isBlocked: true,
            isDeleted: false,
          },
        },
        {
          $lookup: {
            from: 'users',
            let: { credUserId: '$userId' },
            pipeline: [
              {
                $match: {
                  $expr: {
                    $eq: ['$_id', { $toObjectId: '$$credUserId' }],
                  },
                },
              },
            ],
            as: 'userProfile',
          },
        },
        { $unwind: '$userProfile' },
        {
          $project: {
            _id: '$userProfile._id',
            email: '$userProfile.email',
            orgId: '$userProfile.orgId',
            fullName: '$userProfile.fullName',
            hasLoggedIn: '$userProfile.hasLoggedIn',
            slug: '$userProfile.slug',
            createdAt: '$userProfile.createdAt',
            updatedAt: '$userProfile.updatedAt',
          },
        },
      ]);

      res.status(200).json(blockedUsers);
      return;
    }

    // ── Paginated + filtered path (when page param is present) ──
    const { page: pageParam, limit: limitParam, search, hasLoggedIn, isBlocked, groupIds } = req.query;

    if (pageParam) {
      const orgId = req.user?.orgId;
      const orgIdObj = new mongoose.Types.ObjectId(orgId);
      const { page, limit, skip } = safeParsePagination(
        pageParam as string,
        limitParam as string,
        1, 25, 100,
      );

      // Build MongoDB filter
      const filter: Record<string, any> = { orgId: orgIdObj, isDeleted: { $ne: true } };

      if (search) {
        const searchRegex = { $regex: String(search), $options: 'i' };
        filter.$or = [{ fullName: searchRegex }, { email: searchRegex }];
      }

      // Status filters: hasLoggedIn and isBlocked
      // When both are present, combine with $or so users matching either condition are returned.
      const hasLoggedInFilter = hasLoggedIn !== undefined && hasLoggedIn !== '';
      const isBlockedFilter = isBlocked !== undefined && isBlocked !== '';

      if (isBlockedFilter) {
        // Resolve blocked user IDs once and apply blocked/non-blocked constraint.
        const blockedCreds = await UserCredentials.find({
          orgId, isBlocked: true, isDeleted: false,
        }).select('userId').lean().exec();
        const blockedIds = blockedCreds
          .filter((c) => c.userId)
          .map((c) => new mongoose.Types.ObjectId(c.userId!));

        if (String(isBlocked) === 'true') {
          const statusConditions: Record<string, any>[] = [];
          if (hasLoggedInFilter) {
            statusConditions.push({ hasLoggedIn: String(hasLoggedIn) === 'true' });
          }
          // Always include the blocked constraint when isBlocked=true; an empty
          // $in correctly matches nothing so "Blocked" with zero blocked users
          // returns an empty list rather than the entire org.
          statusConditions.push({ _id: { $in: blockedIds } });
          // Merge with any existing $or (search) using $and
          if (filter.$or) {
            filter.$and = [{ $or: filter.$or }, { $or: statusConditions }];
            delete filter.$or;
          } else {
            filter.$or = statusConditions;
          }
        } else {
          if (hasLoggedInFilter) {
            filter.hasLoggedIn = String(hasLoggedIn) === 'true';
          }
          if (blockedIds.length > 0) {
            filter._id = { ...filter._id, $nin: blockedIds };
          }
        }
      } else if (hasLoggedInFilter) {
        filter.hasLoggedIn = String(hasLoggedIn) === 'true';
      }

      // groupIds filter: restrict to users belonging to any of the specified groups
      if (groupIds) {
        const gids = String(groupIds).split(',').filter(Boolean);
        if (gids.length > 0) {
          const groups = await UserGroups.find({
            _id: { $in: gids.map((id) => new mongoose.Types.ObjectId(id)) },
            isDeleted: false,
          }).select('users').lean().exec();
          const userIdsInGroups = groups.flatMap((g) =>
            g.users.map((u: any) => new mongoose.Types.ObjectId(u.toString()))
          );
          // If we already have $in from isBlocked, intersect
          if (filter._id?.$in) {
            const existing = new Set(filter._id.$in.map((id: any) => id.toString()));
            filter._id.$in = userIdsInGroups.filter((id) => existing.has(id.toString()));
          } else {
            filter._id = { ...filter._id, $in: userIdsInGroups };
          }
        }
      }

      const [mongoUsers, totalCount] = await Promise.all([
        Users.find(filter).sort({ fullName: 1 }).skip(skip).limit(limit).lean().exec(),
        Users.countDocuments(filter),
      ]);

      const userIds = mongoUsers.map((u) => u._id.toString());

      // Enrich with profile pictures, groups, and blocked status
      const [dpDocs, groupDocs, credDocs] = userIds.length > 0
        ? await Promise.all([
          UserDisplayPicture.find({
            orgId, userId: { $in: userIds }, pic: { $ne: null },
          }).lean().exec(),
          UserGroups.find({
            orgId: orgIdObj, isDeleted: false,
            users: { $in: userIds.map((id) => new mongoose.Types.ObjectId(id)) },
          }).select('_id name type users').lean().exec(),
          UserCredentials.find({
            orgId, userId: { $in: userIds }, isBlocked: true, isDeleted: false,
          }).select('userId').lean().exec(),
        ])
        : [[], [], []];

      const dpMap = new Map<string, string>();
      for (const dp of dpDocs) {
        if (dp.userId && dp.pic) {
          const mime = dp.mimeType || 'image/jpeg';
          dpMap.set(dp.userId.toString(), `data:${mime};base64,${dp.pic}`);
        }
      }

      const blockedUserIds = new Set(credDocs.map((c) => c.userId?.toString()));

      // Build per-user group data
      const userGroupsMap = new Map<string, { _id: string; name: string; type: string }[]>();
      for (const g of groupDocs) {
        for (const uid of g.users) {
          const uidStr = uid.toString();
          if (!userGroupsMap.has(uidStr)) userGroupsMap.set(uidStr, []);
          userGroupsMap.get(uidStr)!.push({ _id: g._id.toString(), name: g.name, type: g.type });
        }
      }

      const enrichedUsers = mongoUsers.map((u) => {
        const uid = u._id.toString();
        const timestamps = u as typeof u & { createdAt?: Date; updatedAt?: Date };
        const groups = userGroupsMap.get(uid) ?? [];
        return {
          id: uid,
          userId: uid,
          orgId: u.orgId?.toString(),
          name: u.fullName,
          email: u.email,
          isActive: !blockedUserIds.has(uid) && (u.hasLoggedIn ?? false),
          hasLoggedIn: u.hasLoggedIn ?? false,
          isBlocked: blockedUserIds.has(uid),
          createdAtTimestamp: timestamps.createdAt ? new Date(timestamps.createdAt).getTime() : undefined,
          updatedAtTimestamp: timestamps.updatedAt ? new Date(timestamps.updatedAt).getTime() : undefined,
          profilePicture: dpMap.get(uid),
          role: groups.some((g) => g.type === 'admin') ? 'Admin' : 'Member',
          groupCount: groups.filter((g) => g.type !== 'everyone').length,
          userGroups: groups,
        };
      });

      res.status(200).json({
        users: enrichedUsers,
        pagination: buildPaginationMetadata(totalCount, page, limit),
      });
      return;
    }

    // ── Legacy path (no pagination — returns all users as array) ──
    const users = await Users.find({
      orgId: req.user?.orgId,
      isDeleted: false,
    })
      .select('-email')
      .lean()
      .exec();
    res.json(users);
  }

  async getAllUsersWithGroups(
    req: AuthenticatedUserRequest,
    res: Response,
  ): Promise<void> {
    const orgId = req.user?.orgId;
    const orgIdObj = new mongoose.Types.ObjectId(orgId);

    const users = await Users.aggregate([
      {
        $match: {
          orgId: orgIdObj, // Only include users from the same org
          isDeleted: false, // Exclude deleted users
        },
      },
      {
        $lookup: {
          from: 'userGroups', // Collection name for user groups
          localField: '_id', // Field in appusers collection
          foreignField: 'users', // Field in appuserGroups collection (array of user IDs)
          as: 'groups', // Resulting array of groups for each user
        },
      },
      {
        $addFields: {
          // Filter groups array to keep only non-deleted groups from same org
          groups: {
            $filter: {
              input: '$groups',
              as: 'group',
              cond: {
                $and: [
                  { $eq: ['$$group.orgId', orgIdObj] },
                  { $ne: ['$$group.isDeleted', true] },
                ],
              },
            },
          },
        },
      },
      {
        $project: {
          _id: 1,
          userId: 1,
          orgId: 1,
          fullName: 1,
          hasLoggedIn: 1,
          groups: {
            $map: {
              input: '$groups',
              as: 'group',
              in: {
                name: '$$group.name',
                type: '$$group.type',
              },
            },
          },
        },
      },
    ]);

    res.status(200).json(users);
  }

  async getUserById(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ) {
    const userId = req.params.id;
    const orgId = req.user?.orgId;
    try {
      // Check if email should be included based on environment variable
      const hideEmail = process.env.HIDE_EMAIL === 'true';

      const user = await Users.findOne({
        _id: userId,
        orgId,
        isDeleted: false,
      })
        .lean()
        .exec();

      if (!user) {
        throw new NotFoundError('User not found');
      }

      if (hideEmail) {
        delete (user as any)?.email;
      }

      res.json(user);
    } catch (error) {
      next(error);
    }
  }

  async unblockUser(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction
  ): Promise<void> {
    try {
      const userId = req.params.id;
      const orgId = req.user?.orgId;

      if (!userId) {
        throw new BadRequestError(
          'userId must be provided',
        );
      }

      if (!orgId) {
        throw new BadRequestError(
          'orgId must be provided',
        );
      }

      const credential = await UserCredentials.findOneAndUpdate(
        {
          userId,
          orgId,
          isBlocked: true,
          isDeleted: false,
        },
        { $set: { isBlocked: false, wrongCredentialCount: 0, blockExpiresAt: null } },
        { new: true }
      );

      if (!credential) {
        throw new BadRequestError(
          'User not found or not blocked',
        );

      }

      res.status(200).json({
        message: "User unblocked successfully",
      });
    } catch (error) {
      next(error);
    }
  }




  async getUserEmailByUserId(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ) {
    const userId = req.params.id;
    const orgId = req.user?.orgId;
    try {
      const user = await Users.findOne({
        _id: userId,
        orgId,
        isDeleted: false,
      })
        .select('email')
        .lean()
        .exec();

      if (!user) {
        throw new NotFoundError('User not found');
      }

      res.json({ email: user.email });
    } catch (error) {
      next(error);
    }
  }

  async getUsersByIds(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    try {
      const { userIds }: { userIds: string[] } = req.body;

      // Validate if userIds is an array and not empty
      if (!userIds || !Array.isArray(userIds) || userIds.length === 0) {
        throw new BadRequestError(
          'userIds must be provided as a non-empty array',
        );
      }

      // Ensure that userIds are valid MongoDB ObjectIds
      const userObjectIds = userIds.map(
        (id) => new mongoose.mongo.ObjectId(id),
      );

      // Fetch the users using the provided list of user IDs
      const users = await Users.find({
        orgId: req.user?.orgId, // Assuming orgId is in decodedToken
        isDeleted: false,
        _id: { $in: userObjectIds },
      });

      res.status(200).json(users);
    } catch (error) {
      next(error);
    }
  }

  async checkUserExistsByEmail(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    try {
      const { email } = req.body;

      const users = await Users.find({
        email: email,
        isDeleted: false,
      });

      res.json(users);
      return;
    } catch (error) {
      next(error);
    }
  }

  async createUser(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    try {
      const newUser = new Users({
        ...req.body,
        orgId: req.user?.orgId,
      });

      await UserGroups.updateOne(
        { orgId: newUser.orgId, type: 'everyone' }, // Find the everyone group in the same org
        { $addToSet: { users: newUser._id } }, // Add user to the group if not already present
      );

      await this.eventService.start();
      const event: Event = {
        eventType: EventType.NewUserEvent,
        timestamp: Date.now(),
        payload: {
          orgId: newUser.orgId.toString(),
          userId: newUser._id,
          fullName: newUser.fullName,
          email: newUser.email,
          syncAction: SyncAction.Immediate,
        } as UserAddedEvent,
      };
      await this.eventService.publishEvent(event);
      await this.eventService.stop();
      await newUser.save();
      this.logger.debug('user created');
      res.status(201).json(newUser);
    } catch (error) {
      next(error);
    }
  }

  /**
   * Just-In-Time user provisioning from SAML assertion
   * Creates user, adds to everyone group, and publishes creation event
   */
  async provisionSamlUser(
    email: string,
    samlUser: any,
    orgId: string,
    logger: Logger,
  ) {
    logger.info('Auto-provisioning user from SAML', { email, orgId });

    const userDetails = this.extractSamlUserDetails(samlUser, email);
    const newUser = new Users({
      email,
      ...userDetails,
      orgId,
      hasLoggedIn: false,
      isDeleted: false,
    });

    await newUser.save();

    // Add to everyone group
    await UserGroups.updateOne(
      { orgId, type: 'everyone', isDeleted: false },
      { $addToSet: { users: newUser._id } },
    );

    // Publish user creation event
    try {
      await this.eventService.start();
      await this.eventService.publishEvent({
        eventType: EventType.NewUserEvent,
        timestamp: Date.now(),
        payload: {
          orgId: orgId.toString(),
          userId: newUser._id,
          fullName: newUser.fullName,
          email: newUser.email,
          syncAction: SyncAction.Immediate,
        } as UserAddedEvent,
      });
    } catch (eventError) {
      logger.error('Failed to publish user creation event', {
        error: eventError,
        userId: newUser._id,
      });
    } finally {
      await this.eventService.stop();
    }

    logger.info('User auto-provisioned successfully', {
      userId: newUser._id,
      email,
    });

    return newUser.toObject();
  }

  /**
   * Generic Just-In-Time user provisioning for OAuth providers (Google, Microsoft, Azure AD, OAuth)
   * Creates user, adds to everyone group, and publishes creation event
   */
  async provisionJitUser(
    email: string,
    userDetails: { firstName?: string; lastName?: string; fullName: string },
    orgId: string,
    provider: 'google' | 'microsoft' | 'azureAd' | 'oauth',
    logger: Logger,
  ) {
    logger.info(`Auto-provisioning user from ${provider}`, { email, orgId });
    const user = await Users.findOne({
      email,
      orgId,
      isDeleted: true,
    });
    if (user) {
      throw new BadRequestError('User account deleted by admin. Please contact your admin to restore your account.');
    }

    const newUser = new Users({
      email,
      ...userDetails,
      orgId,
      hasLoggedIn: false,
      isDeleted: false,
    });

    await newUser.save();

    // Add to everyone group
    await UserGroups.updateOne(
      { orgId, type: 'everyone', isDeleted: false },
      { $addToSet: { users: newUser._id } },
    );

    // Publish user creation event
    try {
      await this.eventService.start();
      await this.eventService.publishEvent({
        eventType: EventType.NewUserEvent,
        timestamp: Date.now(),
        payload: {
          orgId: orgId.toString(),
          userId: newUser._id,
          fullName: newUser.fullName,
          email: newUser.email,
          syncAction: SyncAction.Immediate,
        } as UserAddedEvent,
      });
    } catch (eventError) {
      logger.error('Failed to publish user creation event', {
        error: eventError,
        userId: newUser._id,
      });
    } finally {
      await this.eventService.stop();
    }

    logger.info(`User auto-provisioned successfully via ${provider}`, {
      userId: newUser._id,
      email,
    });

    return newUser.toObject();
  }

  /**
   * Extract user details from Google ID token payload
   */
  extractGoogleUserDetails(payload: any, email: string) {
    const firstName = payload?.given_name;
    const lastName = payload?.family_name;
    const displayName = payload?.name;

    const fullName =
      displayName ||
      [firstName, lastName].filter(Boolean).join(' ') ||
      email.split('@')[0];

    return {
      firstName: firstName || undefined,
      lastName: lastName || undefined,
      fullName,
    };
  }

  /**
   * Extract user details from Microsoft/Azure AD decoded token
   */
  extractMicrosoftUserDetails(decodedToken: any, email: string) {
    const firstName = decodedToken?.given_name;
    const lastName = decodedToken?.family_name;
    const displayName = decodedToken?.name;

    const fullName =
      displayName ||
      [firstName, lastName].filter(Boolean).join(' ') ||
      email.split('@')[0];

    return {
      firstName: firstName || undefined,
      lastName: lastName || undefined,
      fullName,
    };
  }

  /**
   * Extract user details from OAuth userInfo response
   */
  extractOAuthUserDetails(userInfo: any, email: string) {
    // Common OAuth/OIDC claims
    const firstName =
      userInfo?.given_name ||
      userInfo?.first_name ||
      userInfo?.firstName;
    const lastName =
      userInfo?.family_name ||
      userInfo?.last_name ||
      userInfo?.lastName;
    const displayName =
      userInfo?.name ||
      userInfo?.displayName ||
      userInfo?.preferred_username;

    const fullName =
      displayName ||
      [firstName, lastName].filter(Boolean).join(' ') ||
      email.split('@')[0];

    return {
      firstName: firstName || undefined,
      lastName: lastName || undefined,
      fullName,
    };
  }

  async updateUser(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    try {
      if (!req.user) {
        throw new UnauthorizedError('Unauthorized to update the user');
      }
      let emailChangeRequested = 'notNeeded';


      // Define whitelist of allowed fields that can be updated
      const ALLOWED_UPDATE_FIELDS = [
        'firstName',
        'lastName',
        'fullName',
        'middleName',
        'email',
        'designation',
        'mobile',
        'address',
        'dataCollectionConsent',
        'hasLoggedIn',
      ] as const;

      // List of sensitive system fields that must never be updated via API
      const RESTRICTED_FIELDS = [
        '_id',
        'orgId',
        'slug',
        '__v',
      ];

      // Check for restricted fields in request body
      const restrictedFieldsFound = RESTRICTED_FIELDS.filter(
        (field) => field in req.body,
      );
      if (restrictedFieldsFound.length > 0) {
        throw new BadRequestError(
          `Cannot update restricted fields: ${restrictedFieldsFound.join(', ')}`,
        );
      }

      // Extract only allowed fields from request body
      const updateFields: Partial<Record<typeof ALLOWED_UPDATE_FIELDS[number], any>> = {};
      for (const field of ALLOWED_UPDATE_FIELDS) {
        if (field in req.body && req.body[field] !== undefined) {
          updateFields[field] = req.body[field];
        }
      }

      // If no valid fields to update, return error
      if (Object.keys(updateFields).length === 0) {
        throw new BadRequestError('No valid fields provided for update');
      }

      const { id } = req.params;
      const user = await Users.findOne({
        orgId: req.user.orgId,
        _id: id,
        isDeleted: false,
      });

      if (!user) {
        throw new NotFoundError('User not found');
      }

      // Apply updates only for whitelisted fields
      // Separate email from other fields since it requires special handling (uniqueness check)
      const { email, ...otherUpdateFields } = updateFields;

      // Apply all other whitelisted fields
      Object.assign(user, otherUpdateFields);

      // Handle email update separately due to uniqueness check
      if (email !== undefined) {
        // Only update email if it's different from the current email
        const currentEmail = user.email?.toLowerCase().trim();
        const newEmail = email?.toLowerCase().trim();

        if (currentEmail !== newEmail) {
          // Email is being changed - validate uniqueness
          const existingUser = await Users.findOne({
            email: email,
            _id: { $ne: id },
            orgId: req.user.orgId,
            isDeleted: false,
          });
          if (existingUser) {
            throw new BadRequestError('Email already exists for another user');
          }

          const emailSentResponse = await this.emailChange(
            email,
            newEmail,
            user,
          )

          if (emailSentResponse.statusCode !== 200) {
            emailChangeRequested = 'failed';
          } else {
            emailChangeRequested = 'sent';
          }
        }
      }

      await user.save();

      await this.eventService.start();

      const event: Event = {
        eventType: EventType.UpdateUserEvent,
        timestamp: Date.now(),
        payload: {
          orgId: user.orgId.toString(),
          userId: user._id,
          fullName: user.fullName,
          ...(user.firstName && { firstName: user.firstName }),
          ...(user.lastName && { lastName: user.lastName }),
          ...(user.designation && { designation: user.designation }),
          email: user.email,
        } as UserUpdatedEvent,
      };

      await this.eventService.publishEvent(event);
      await this.eventService.stop();
      // Save the updated user
      res.json({
        ...user.toObject(),
        meta: {
          emailChangeMailStatus: emailChangeRequested,
        },
      });
    } catch (error) {
      next(error);
    }
  }
  async updateFullName(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    try {
      if (!req.user) {
        throw new UnauthorizedError('Unauthorized to update the user');
      }

      const { id } = req.params;
      const user = await Users.findOne({
        orgId: req.user.orgId,
        _id: id,
        isDeleted: false,
      });

      if (!user) {
        throw new NotFoundError('User not found');
      }

      user.fullName = req.body.fullName;
      await user.save();

      await this.eventService.start();
      const event: Event = {
        eventType: EventType.UpdateUserEvent,
        timestamp: Date.now(),
        payload: {
          orgId: user.orgId.toString(),
          userId: user._id,
          fullName: user.fullName,
          ...(user.firstName && { firstName: user.firstName }),
          ...(user.lastName && { lastName: user.lastName }),
          ...(user.designation && { designation: user.designation }),
          email: user.email,
        } as UserUpdatedEvent,
      };

      await this.eventService.publishEvent(event);
      await this.eventService.stop();
      res.json(user.toObject());
    } catch (error) {
      next(error);
    }
  }

  async updateFirstName(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    try {
      if (!req.user) {
        throw new UnauthorizedError('Unauthorized to update the user');
      }

      const { id } = req.params;
      const user = await Users.findOne({
        orgId: req.user.orgId,
        _id: id,
        isDeleted: false,
      });

      if (!user) {
        throw new NotFoundError('User not found');
      }

      user.firstName = req.body.firstName;
      await user.save();

      await this.eventService.start();
      const event: Event = {
        eventType: EventType.UpdateUserEvent,
        timestamp: Date.now(),
        payload: {
          orgId: user.orgId.toString(),
          userId: user._id,
          fullName: user.fullName,
          ...(user.firstName && { firstName: user.firstName }),
          ...(user.lastName && { lastName: user.lastName }),
          ...(user.designation && { designation: user.designation }),
          email: user.email,
        } as UserUpdatedEvent,
      };

      await this.eventService.publishEvent(event);
      await this.eventService.stop();
      res.json(user.toObject());
    } catch (error) {
      next(error);
    }
  }

  async updateLastName(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    try {
      if (!req.user) {
        throw new UnauthorizedError('Unauthorized to update the user');
      }

      const { id } = req.params;
      const user = await Users.findOne({
        orgId: req.user.orgId,
        _id: id,
        isDeleted: false,
      });

      if (!user) {
        throw new NotFoundError('User not found');
      }

      user.lastName = req.body.lastName;
      await user.save();

      await this.eventService.start();
      const event: Event = {
        eventType: EventType.UpdateUserEvent,
        timestamp: Date.now(),
        payload: {
          orgId: user.orgId.toString(),
          userId: user._id,
          fullName: user.fullName,
          ...(user.firstName && { firstName: user.firstName }),
          ...(user.lastName && { lastName: user.lastName }),
          ...(user.designation && { designation: user.designation }),
          email: user.email,
        } as UserUpdatedEvent,
      };

      await this.eventService.publishEvent(event);
      await this.eventService.stop();
      res.json(user.toObject());
    } catch (error) {
      next(error);
    }
  }

  async updateDesignation(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    try {
      if (!req.user) {
        throw new UnauthorizedError('Unauthorized to update the user');
      }

      const { id } = req.params;
      const user = await Users.findOne({
        orgId: req.user.orgId,
        _id: id,
        isDeleted: false,
      });

      if (!user) {
        throw new NotFoundError('User not found');
      }

      user.designation = req.body.designation;
      await user.save();

      await this.eventService.start();
      const event: Event = {
        eventType: EventType.UpdateUserEvent,
        timestamp: Date.now(),
        payload: {
          orgId: user.orgId.toString(),
          userId: user._id,
          fullName: user.fullName,
          ...(user.firstName && { firstName: user.firstName }),
          ...(user.lastName && { lastName: user.lastName }),
          ...(user.designation && { designation: user.designation }),
          email: user.email,
        } as UserUpdatedEvent,
      };

      await this.eventService.publishEvent(event);
      await this.eventService.stop();
      res.json(user.toObject());
    } catch (error) {
      next(error);
    }
  }

  async updateEmail(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    try {
      if (!req.user) {
        throw new UnauthorizedError('Unauthorized to update the user');
      }

      const { id } = req.params;
      const user = await Users.findOne({
        orgId: req.user.orgId,
        _id: id,
        isDeleted: false,
      });

      if (!user) {
        throw new NotFoundError('User not found');
      }

      user.email = req.body.email;
      await user.save();

      await this.eventService.start();
      const event: Event = {
        eventType: EventType.UpdateUserEvent,
        timestamp: Date.now(),
        payload: {
          orgId: user.orgId.toString(),
          userId: user._id,
          fullName: user.fullName,
          ...(user.firstName && { firstName: user.firstName }),
          ...(user.lastName && { lastName: user.lastName }),
          ...(user.designation && { designation: user.designation }),
          email: user.email,
        } as UserUpdatedEvent,
      };

      await this.eventService.publishEvent(event);
      await this.eventService.stop();
      res.json(user.toObject());
    } catch (error) {
      next(error);
    }
  }

  /**
   * Soft-delete all OAuth apps owned by a user and revoke their tokens (when OAuth is initialized).
   */
  private async softDeleteOAuthAppsForUser(
    orgId: unknown,
    createdByUserId: unknown,
    actorUser: Record<string, unknown>,
  ): Promise<void> {
    const orgOid =
      orgId instanceof mongoose.Types.ObjectId
        ? orgId
        : new mongoose.Types.ObjectId(String(orgId));
    const creatorOid =
      createdByUserId instanceof mongoose.Types.ObjectId
        ? createdByUserId
        : new mongoose.Types.ObjectId(String(createdByUserId));

    const apps = await OAuthApp.find({
      orgId: orgOid,
      createdBy: creatorOid,
      isDeleted: false,
    })
      .select('clientId')
      .lean()
      .exec();

    const oauthTokenService = resolveOAuthTokenService();
    if (oauthTokenService && apps.length > 0) {
      for (const app of apps) {
        if (!app?.clientId) {
          continue;
        }
        try {
          await oauthTokenService.revokeAllTokensForApp(app.clientId);
        } catch (err) {
          this.logger.error(
            'Failed to revoke OAuth tokens when deleting user',
            { clientId: app.clientId, err },
          );
        }
      }
    }

    const actorIdRaw = actorUser.userId ?? actorUser._id;
    const deletedBy =
      actorIdRaw != null
        ? new mongoose.Types.ObjectId(String(actorIdRaw))
        : creatorOid;

    await OAuthApp.updateMany(
      { orgId: orgOid, createdBy: creatorOid, isDeleted: false },
      {
        $set: {
          isDeleted: true,
          status: OAuthAppStatus.REVOKED,
          deletedBy,
        },
      },
    );
  }

  async deleteUser(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    try {
      if (!req.user) {
        throw new UnauthorizedError('Unauthorized to delete the user');
      }
      const { id } = req.params;

      const user = await Users.findOne({
        orgId: req.user.orgId,
        _id: id,
      });

      if (!user) {
        throw new NotFoundError('User not found');
      }

      const userId = user?._id;
      const orgId = user?.orgId;
      if (!userId || !orgId) {
        throw new NotFoundError('Account not found');
      }

      const groups = await UserGroups.find({
        orgId,
        users: { $in: [userId] },
        isDeleted: false,
      }).select('type');

      const isAdmin = groups.find(
        (userGroup: any) => userGroup.type === 'admin',
      );

      if (isAdmin) {
        throw new BadRequestError('User cannot be deleted. Please remove the user from the admin group first.');
      }

      await UserGroups.updateMany(
        { orgId, users: userId },
        { $pull: { users: userId } },
      );

      await this.softDeleteOAuthAppsForUser(orgId, userId, req.user);

      user.isDeleted = true;
      user.hasLoggedIn = false;
      user.deletedBy = req.user._id;

      await UserCredentials.updateOne(
        { userId },
        { $unset: { hashedPassword: '' } },
      );

      await user.save();

      await this.eventService.start();
      const event: Event = {
        eventType: EventType.DeleteUserEvent,
        timestamp: Date.now(),
        payload: {
          orgId: user.orgId.toString(),
          userId: user._id,
          email: user.email,
        } as UserDeletedEvent,
      };
      await this.eventService.publishEvent(event);
      await this.eventService.stop();

      res.json({ message: 'User deleted successfully' });
    } catch (error) {
      next(error);
    }
  }

  async updateUserDisplayPicture(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    try {
      const dpFile = req.body.fileBuffer;
      const orgId = req.user?.orgId;
      const userId = req.user?.userId;

      if (!dpFile) {
        throw new BadRequestError('DP File is required');
      }
      let quality = 100;
      let compressedImageBuffer = await sharp(dpFile.buffer)
        .jpeg({ quality })
        .toBuffer();
      while (compressedImageBuffer.length > 100 * 1024 && quality > 10) {
        quality -= 10;
        compressedImageBuffer = await sharp(dpFile.buffer)
          .jpeg({ quality })
          .toBuffer();
      }

      if (compressedImageBuffer.length > 100 * 1024) {
        throw new LargePayloadError('File too large , limit:1MB');
      }
      const compressedPic = compressedImageBuffer.toString('base64');
      const compressedPicMimeType = 'image/jpeg';

      await UserDisplayPicture.findOneAndUpdate(
        {
          orgId,
          userId,
        },
        {
          orgId,
          userId,
          pic: compressedPic,
          mimeType: compressedPicMimeType,
        },
        { new: true, upsert: true },
      );
      res.setHeader('Content-Type', compressedPicMimeType);
      res.status(201).send(compressedImageBuffer);
      return;
    } catch (error) {
      next(error);
    }
  }

  async getUserDisplayPicture(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    try {
      const orgId = req.user?.orgId;
      const userId = req.user?.userId;

      const userDp = await UserDisplayPicture.findOne({ orgId, userId })
        .lean()
        .exec();
      if (!userDp || !userDp.pic) {
        res.status(200).json({ errorMessage: 'User pic not found' });
        return;
      }

      const userDisplayBuffer = Buffer.from(userDp.pic, 'base64');
      if (userDp.mimeType) {
        res.setHeader('Content-Type', userDp.mimeType);
      }
      res.status(200).send(userDisplayBuffer);
      return;
    } catch (error) {
      next(error);
    }
  }

  async removeUserDisplayPicture(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    try {
      const orgId = req.user?.orgId;
      const userId = req.user?.userId;

      const userDp = await UserDisplayPicture.findOne({
        orgId,
        userId,
      }).exec();

      if (!userDp) {
        res
          .status(200)
          .json({ errorMessage: 'User display picture not found' });
        return;
      }

      userDp.pic = null;
      userDp.mimeType = null;

      await userDp.save();

      res.status(200).send(userDp);
    } catch (error) {
      next(error);
    }
  }

  async resendInvite(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    try {
      const { id } = req.params;
      if (!id) {
        throw new BadRequestError('Id is required');
      }

      if (!req.user) {
        throw new NotFoundError('User not found');
      }
      const org = await Org.findOne({ _id: req.user.orgId, isDeleted: false });
      const user = await Users.findOne({ _id: id, isDeleted: false });
      if (!user) {
        throw new UnauthorizedError('Error getting the user');
      }
      if (user?.hasLoggedIn) {
        throw new BadRequestError('User has already accepted the invite');
      }

      const email = user?.email;
      const userId = req.user?.userId;
      const orgId = req.user?.orgId;
      const authToken = fetchConfigJwtGenerator(
        userId,
        orgId,
        this.config.scopedJwtSecret,
      );
      let result = await this.authService.passwordMethodEnabled(authToken);

      if (result.statusCode !== 200) {
        throw new InternalServerError('Error fetching auth methods');
      }
      if (result.data?.isPasswordAuthEnabled) {
        const { passwordResetToken, mailAuthToken } =
          jwtGeneratorForNewAccountPassword(
            email,
            id,
            orgId,
            this.config.scopedJwtSecret,
          );

        result = await this.mailService.sendMail({
          emailTemplateType: 'appuserInvite',
          initiator: {
            jwtAuthToken: mailAuthToken,
          },
          usersMails: [email],
          subject: `You are invited to join ${org?.registeredName} `,
          templateData: {
            invitee: user?.fullName,
            orgName: org?.shortName || org?.registeredName,
            link: `${this.config.frontendUrl}/reset-password#token=${passwordResetToken}`,
          },
        });
        if (result.statusCode !== 200) {
          throw new InternalServerError('Error sending invite');
        }
      } else {
        result = await this.mailService.sendMail({
          emailTemplateType: 'appuserInvite',
          initiator: {
            jwtAuthToken: mailJwtGenerator(email, this.config.scopedJwtSecret),
          },
          usersMails: [email],
          subject: `You are invited to join ${org?.registeredName} `,
          templateData: {
            invitee: user?.fullName,
            orgName: org?.shortName || org?.registeredName,
            link: `${this.config.frontendUrl}/sign-in`,
          },
        });
        if (result.statusCode !== 200) {
          throw new InternalServerError('Error sending invite');
        }
      }

      res.status(200).json({ message: 'Invite sent successfully' });
      return;
    } catch (error) {
      next(error);
    }
  }

  async addManyUsers(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    try {
      const { emails } = req.body;
      const { groupIds } = req.body;

      if (!req.user) {
        throw new NotFoundError('User not found');
      }
      if (!emails) {
        throw new BadRequestError('emails are required');
      }

      const orgId = req.user?.orgId;
      const org = await Org.findOne({ _id: req.user.orgId, isDeleted: false });
      // Check if emails array is provided
      if (!emails || !Array.isArray(emails)) {
        throw new BadRequestError('Please provide an array of email addresses');
      }

      // Email validation regex
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

      // Validate all emails
      const invalidEmails = emails.filter((email) => !emailRegex.test(email));
      if (invalidEmails.length > 0) {
        throw new BadRequestError('Invalid emails are found');
      }

      // Find all users (both active and deleted) with the provided emails
      const existingUsers = await Users.find({
        email: { $in: emails },
      });
      // Separate active and deleted users
      const activeUsers = existingUsers.filter((user) => !user.isDeleted);
      const deletedUsers = existingUsers.filter((user) => user.isDeleted);

      const activeEmails = activeUsers.map((user) => user.email);
      const deletedEmails = deletedUsers.map((user) => user.email);
      const pendingUsers = activeUsers.filter((user) => !user.hasLoggedIn);
      const pendingUserIds = pendingUsers
        .map((user) => user._id)
        .filter((userId): userId is mongoose.Types.ObjectId => Boolean(userId));

      const blockedPendingCredentialDocs = pendingUserIds.length > 0
        ? await UserCredentials.find({
          orgId: req.user?.orgId,
          userId: { $in: pendingUserIds.map((userId) => userId.toString()) },
          isBlocked: true,
          isDeleted: false,
        })
          .select('userId')
          .lean()
          .exec()
        : [];

      const blockedPendingUserIds = new Set(
        blockedPendingCredentialDocs
          .map((doc) => doc.userId?.toString())
          .filter((userId): userId is string => Boolean(userId)),
      );
      const pendingUsersToReinvite = pendingUsers.filter(
        (user) => user._id && !blockedPendingUserIds.has(user._id.toString()),
      );

      // Restore deleted accounts
      let restoredUsers: User[] = [];
      if (deletedUsers.length > 0) {
        await Users.updateMany(
          {
            email: { $in: deletedEmails },
            isDeleted: true,
            orgId: req.user?.orgId,
          },
          {
            $set: {
              isDeleted: false,
            },
          },
        );

        // Fetch the restored users for response
        restoredUsers = await Users.find({
          email: { $in: deletedEmails },
        });
      }
      for (let i = 0; i < existingUsers.length; ++i) {
        const userId = existingUsers[i]?._id;

        await UserGroups.updateMany(
          { _id: { $in: groupIds }, orgId },
          { $addToSet: { users: userId } },
          { new: true },
        );

        await UserGroups.updateOne(
          { orgId: req.user?.orgId, type: 'everyone' }, // Find the everyone group in the same org
          { $addToSet: { users: userId } }, // Add user to the group if not already present
        );
      }

      // Filter emails that need new accounts
      // (excluding both active and restored accounts)
      const emailsForNewAccounts = emails.filter(
        (email) =>
          !activeEmails.includes(email) && !deletedEmails.includes(email),
      );

      // Create new users for remaining emails
      let newUsers: User[] = [];
      if (emailsForNewAccounts.length > 0) {
        newUsers = await Users.create(
          emailsForNewAccounts.map((email) => ({
            email,
            isDeleted: false,
            hasLoggedIn: false,
            orgId: req.user?.orgId,
          })),
        );
      }
      // If nothing was done, return 409
      if (
        newUsers.length === 0 &&
        restoredUsers.length === 0 &&
        pendingUsersToReinvite.length === 0
      ) {
        res.status(200).json({
          errorMessage: 'All provided emails already have active accounts',
        });
        return;
      }
      let errorSendingMail = false;
      let errorCode = 500;

      await this.eventService.start();
      const newUserByEmail = new Map(
        newUsers.map((user) => [user.email, user]),
      );
      const pendingUserByEmail = new Map(
        pendingUsersToReinvite.map((user) => [user.email, user]),
      );
      const emailsForPendingAccounts = pendingUsersToReinvite
        .map((user) => user.email)
        .filter((email): email is string => Boolean(email));
      const emailsForInvites = [...emailsForNewAccounts, ...emailsForPendingAccounts];

      for (let i = 0; i < emailsForInvites.length; ++i) {
        const email = emailsForInvites[i];
        const pendingUser = pendingUserByEmail.get(email);
        const userId = pendingUser?._id || newUserByEmail.get(email)?._id;
        if (!userId) {
          throw new InternalServerError(
            'User ID missing while inviting restored user. Please ensure user restoration was successful.',
          );
        }
        if (!pendingUser) {
          await UserGroups.updateMany(
            { _id: { $in: groupIds }, orgId },
            { $addToSet: { users: userId } },
            { new: true },
          );

          await UserGroups.updateOne(
            { orgId: req.user?.orgId, type: 'everyone' }, // Find the everyone group in the same org
            { $addToSet: { users: userId } }, // Add user to the group if not already present
          );

          const event: Event = {
            eventType: EventType.NewUserEvent,
            timestamp: Date.now(),
            payload: {
              orgId: req.user?.orgId.toString(),
              userId: userId,
              email: email,
              syncAction: SyncAction.Immediate,
            } as UserAddedEvent,
          };
          await this.eventService.publishEvent(event);
        }

        const authToken = fetchConfigJwtGenerator(
          userId.toString(),
          req.user?.orgId,
          this.config.scopedJwtSecret,
        );
        let result = await this.authService.passwordMethodEnabled(authToken);

        if (result.statusCode !== 200) {
          throw new InternalServerError('Error fetching auth methods');
        }

        if (result.data?.isPasswordAuthEnabled) {
          const { passwordResetToken, mailAuthToken } =
            jwtGeneratorForNewAccountPassword(
              email,
              userId.toString(),
              orgId,
              this.config.scopedJwtSecret,
            );

          result = await this.mailService.sendMail({
            emailTemplateType: 'appuserInvite',
            initiator: {
              jwtAuthToken: mailAuthToken,
            },
            usersMails: [email],
            subject: `You are invited to join ${org?.registeredName} `,
            templateData: {
              invitee: req.user?.fullName,
              orgName: org?.shortName || org?.registeredName,
              link: `${this.config.frontendUrl}/reset-password#token=${passwordResetToken}`,
            },
          });
          if (result.statusCode !== 200) {
            errorSendingMail = true;
            errorCode = result.statusCode;
            continue;
          }
        } else {
          result = await this.mailService.sendMail({
            emailTemplateType: 'appuserInvite',
            initiator: {
              jwtAuthToken: mailJwtGenerator(
                email,
                this.config.scopedJwtSecret,
              ),
            },
            usersMails: [email],
            subject: `You are invited to join ${org?.registeredName} `,
            templateData: {
              invitee: req.user?.fullName,
              orgName: org?.shortName || org?.registeredName,
              link: `${this.config.frontendUrl}/sign-in`,
            },
          });
          if (result.statusCode !== 200) {
            errorSendingMail = true;
            errorCode = result.statusCode;
            continue;
          }
        }
      }

      const emailsForRestoredAccounts = restoredUsers.map((user) => user.email);

      for (let i = 0; i < emailsForRestoredAccounts.length; ++i) {
        const email = emailsForRestoredAccounts[i];
        const userId = restoredUsers[i]?._id;

        if (!email) {
          continue;
        }
        if (!userId) {
          throw new InternalServerError(
            'User ID missing while inviting restored user. Please ensure user restoration was successful.',
          );
        }
        const event: Event = {
          eventType: EventType.NewUserEvent,
          timestamp: Date.now(),
          payload: {
            orgId: req.user?.orgId.toString(),
            userId: userId,
            email: email,
            syncAction: SyncAction.Immediate,
          } as UserAddedEvent,
        };
        await this.eventService.publishEvent(event);

        const authToken = fetchConfigJwtGenerator(
          userId.toString(),
          req.user?.orgId,
          this.config.scopedJwtSecret,
        );
        let result = await this.authService.passwordMethodEnabled(authToken);

        if (result.statusCode !== 200) {
          throw new InternalServerError('Error fetching auth methods');
        }

        if (result.data?.isPasswordAuthEnabled) {
          const { passwordResetToken, mailAuthToken } =
            jwtGeneratorForNewAccountPassword(
              email,
              userId.toString(),
              orgId,
              this.config.scopedJwtSecret,
            );

          result = await this.mailService.sendMail({
            emailTemplateType: 'appuserInvite',
            initiator: {
              jwtAuthToken: mailAuthToken,
            },
            usersMails: [email],
            subject: `You are invited to re-join ${org?.registeredName} `,
            templateData: {
              invitee: req.user?.fullName,
              orgName: org?.shortName || org?.registeredName,
              link: `${this.config.frontendUrl}/reset-password#token=${passwordResetToken}`,
            },
          });
          if (result.statusCode !== 200) {
            errorSendingMail = true;
            errorCode = result.statusCode;
            continue;
          }
        } else {
          result = await this.mailService.sendMail({
            emailTemplateType: 'appuserInvite',
            initiator: {
              jwtAuthToken: mailJwtGenerator(
                email,
                this.config.scopedJwtSecret,
              ),
            },
            usersMails: [email],
            subject: `You are invited to re-join ${org?.registeredName} `,
            templateData: {
              invitee: req.user?.fullName,
              orgName: org?.shortName || org?.registeredName,
              link: `${this.config.frontendUrl}/sign-in`,
            },
          });
          if (result.statusCode !== 200) {
            errorSendingMail = true;
            errorCode = result.statusCode;
            continue;
          }
        }
      }

      await this.eventService.stop();

      if (errorSendingMail) {
        res.status(errorCode).json({
          message: 'Error sending mail invite. Please try again.',
        });
        return;
      }

      res.status(200).json({ message: 'Invite sent successfully' });
    } catch (error) {
      next(error);
    }
  }

  async listUsers(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    const requestId = req.context?.requestId;
    try {
      const orgId = req.user?.orgId;
      const userId = req.user?.userId;
      if (!orgId) {
        throw new BadRequestError('Organization ID is required');
      }
      if (!userId) {
        throw new BadRequestError('User ID is required');
      }

      const { page, limit, search } = req.query;

      // Validate search parameter for XSS and format specifiers
      if (search) {
        try {
          validateNoXSS(String(search), 'search parameter');
          validateNoFormatSpecifiers(String(search), 'search parameter');

          if (String(search).length > 1000) {
            throw new BadRequestError('Search parameter too long (max 1000 characters)');
          }
        } catch (error: any) {
          throw new BadRequestError(
            error.message || 'Search parameter contains potentially dangerous content'
          );
        }
      }

      const queryParams = new URLSearchParams();
      if (page) queryParams.append('page', String(page));
      if (limit) queryParams.append('limit', String(limit));
      if (search) queryParams.append('search', String(search));
      const queryString = queryParams.toString();

      const aiCommandOptions: AICommandOptions = {
        uri: `${this.config.connectorBackend}/api/v1/entity/user/list?${queryString}`,
        headers: {
          ...(req.headers as Record<string, string>),
          'Content-Type': 'application/json',
        },
        method: HttpMethod.GET,
      };
      const aiCommand = new AIServiceCommand<GraphUserListResponse>(aiCommandOptions);
      const aiResponse = await aiCommand.execute();
      if (aiResponse && aiResponse.statusCode !== 200) {
        throw new BadRequestError('Failed to get users');
      }
      const data = aiResponse.data;
      const usersArray = data?.users ?? [];

      const userMongoIds = usersArray.map((u) => u.userId).filter(Boolean);

      if (userMongoIds.length > 0) {
        const orgIdObj = new mongoose.Types.ObjectId(orgId);

        // Parallel: fetch DPs, mongo user docs (hasLoggedIn), and group memberships
        const [dpDocs, mongoUsers, groupDocs] = await Promise.all([
          UserDisplayPicture.find({
            orgId,
            userId: { $in: userMongoIds },
            pic: { $ne: null },
          }).lean().exec(),
          Users.find({
            _id: { $in: userMongoIds },
            orgId: orgIdObj,
          }).select('_id hasLoggedIn fullName').lean().exec(),
          UserGroups.find({
            orgId: orgIdObj,
            isDeleted: false,
            users: { $in: userMongoIds.map((id: string) => new mongoose.Types.ObjectId(id)) },
          }).select('_id name type users').lean().exec(),
        ]);

        // Build lookup maps
        const dpMap = new Map<string, string>();
        for (const dp of dpDocs) {
          if (dp.userId && dp.pic) {
            const mime = dp.mimeType || 'image/jpeg';
            dpMap.set(dp.userId.toString(), `data:${mime};base64,${dp.pic}`);
          }
        }

        const mongoUserMap = new Map<string, { hasLoggedIn?: boolean; fullName?: string }>();
        for (const mu of mongoUsers) {
          mongoUserMap.set(mu._id.toString(), { hasLoggedIn: mu.hasLoggedIn, fullName: mu.fullName });
        }

        // Build per-user groups
        const userGroupsMap = new Map<string, UserGroupSummary[]>();
        for (const g of groupDocs) {
          for (const uid of g.users) {
            const uidStr = uid.toString();
            if (!userGroupsMap.has(uidStr)) {
              userGroupsMap.set(uidStr, []);
            }
            userGroupsMap.get(uidStr)!.push({
              _id: g._id.toString(),
              name: g.name,
              type: g.type,
            });
          }
        }

        // Enrich each user
        for (const user of usersArray) {
          const uid = user.userId;
          if (!uid) continue;
          if (dpMap.has(uid)) user.profilePicture = dpMap.get(uid);
          const mu = mongoUserMap.get(uid);
          if (mu) {
            user.hasLoggedIn = mu.hasLoggedIn ?? true;
            if (!user.name && mu.fullName) user.name = mu.fullName;
          }
          const groups = userGroupsMap.get(uid) ?? [];
          user.userGroups = groups;
          user.groupCount = groups.filter((g) => g.type !== 'everyone').length;
          user.role = groups.some((g) => g.type === 'admin') ? 'Admin' : 'Member';
        }
      }

      res.status(HTTP_STATUS.OK).json(data);
    } catch (error: any) {
      this.logger.error('Error getting users', {
        requestId,
        message: 'Error getting users',
        error: error.message,
      });
      next(error);
    }
  }

  async getUserTeams(
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    const requestId = req.context?.requestId;
    try {
      const orgId = req.user?.orgId;
      const userId = req.user?.userId;
      if (!orgId) {
        throw new BadRequestError('Organization ID is required');
      }
      if (!userId) {
        throw new BadRequestError('User ID is required');
      }
      const { page, limit, search } = req.query;
      let queryString = '';
      if (page) {
        queryString += `&page=${page}`;
      }
      if (limit) {
        queryString += `&limit=${limit}`;
      }
      if (search) {
        queryString += `&search=${search}`;
      }
      const aiCommandOptions: AICommandOptions = {
        uri: `${this.config.connectorBackend}/api/v1/entity/user/teams?${queryString}`,
        headers: {
          ...(req.headers as Record<string, string>),
          'Content-Type': 'application/json',
        },
        method: HttpMethod.GET,
      };
      const aiCommand = new AIServiceCommand(aiCommandOptions);
      const aiResponse = await aiCommand.execute();
      if (aiResponse && aiResponse.statusCode !== 200) {
        throw new BadRequestError('Failed to get user teams');
      }
      const userTeams = aiResponse.data;
      res.status(HTTP_STATUS.OK).json(userTeams);
    } catch (error: any) {
      this.logger.error('Error getting user teams', {
        requestId,
        message: 'Error getting user teams',
        error: error.message,
      });
      next(error);
    }
  }

  /**
   * Extract user details from SAML assertion with fallbacks for different IdP formats
   */
  private extractSamlUserDetails(samlUser: any, email: string) {
    const SAML_CLAIM_GIVENNAME =
      'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname';
    const SAML_OID_GIVENNAME = 'urn:oid:2.5.4.42';
    const SAML_CLAIM_SURNAME =
      'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname';
    const SAML_OID_SURNAME = 'urn:oid:2.5.4.4';
    const SAML_CLAIM_NAME =
      'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name';
    const SAML_OID_DISPLAYNAME = 'urn:oid:2.16.840.1.113730.3.1.241';

    // Try multiple SAML attribute names for first name
    const firstName =
      samlUser.firstName ||
      samlUser.givenName ||
      samlUser[SAML_CLAIM_GIVENNAME] ||
      samlUser[SAML_OID_GIVENNAME];

    // Try multiple SAML attribute names for last name
    const lastName =
      samlUser.lastName ||
      samlUser.surname ||
      samlUser.sn ||
      samlUser[SAML_CLAIM_SURNAME] ||
      samlUser[SAML_OID_SURNAME];

    // Try multiple SAML attribute names for display name
    const displayName =
      samlUser.displayName ||
      samlUser.name ||
      samlUser.fullName ||
      samlUser[SAML_CLAIM_NAME] ||
      samlUser[SAML_OID_DISPLAYNAME];

    // Construct full name with fallbacks
    const fullName =
      displayName ||
      [firstName, lastName].filter(Boolean).join(' ') ||
      email.split('@')[0];

    return {
      firstName: firstName || undefined,
      lastName: lastName || undefined,
      fullName,
    };
  }

  async emailChange(
    email: string,
    newEmail: string,
    user: Record<string, any>,
    // authToken: string,
  ) {
    try {
      if (!email) {
        throw new BadRequestError('Email is required');
      }
      const result = await this.sendValidateEmailIdEmail(user, newEmail);

      if (result.statusCode !== 200) {
        return {
          statusCode: 400,
          data: 'Failed to send email',
        };
      }
      return {
        statusCode: 200,
        data: 'Email change verification mail sent',
      };
      // res.status(200).send({ data: 'email change verification mail sent' });
      // return;
    } catch (error) {
      return {
        statusCode: 400,
        data: 'Failed to send email',
      };
    }
  };


  async sendValidateEmailIdEmail(user: Record<string, any>, newEmail: string) {
    try {
      const { validateEmailToken, mailAuthToken } =
        jwtGeneratorForValidateEmailLink(
          user.email,
          newEmail,
          user._id,
          user.orgId,
          this.config.scopedJwtSecret,
        );

      const validateEmailLink = `${this.config.frontendUrl}/reset-email#token=${validateEmailToken}`;
      const org = await Org.findOne({ _id: user.orgId, isDeleted: false });
      const emailSentResponse = await this.mailService.sendMail({
        emailTemplateType: 'resetEmail',
        initiator: { jwtAuthToken: mailAuthToken },
        usersMails: [newEmail],
        subject: 'PipesHub | Verify your email !',
        templateData: {
          orgName: org?.shortName || org?.registeredName,
          name: user.fullName,
          link: validateEmailLink,
        },
      });


      if (emailSentResponse.statusCode !== 200) {
        return {
          statusCode: 400,
          data: 'Failed to send email',
        };
      }

      return {
        statusCode: 200,
        data: 'mail sent',
      };
    } catch (error) {
      return {
        statusCode: 400,
        data: 'Failed to send email',
      };
    }
  }
}

