import {
  BadRequestException,
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import * as bcrypt from 'bcrypt';
import * as fs from 'fs';
import * as moment from 'moment';
import * as nodemailer from 'nodemailer';
import * as path from 'path';
import { AppUsersStep } from 'src/app_users_steps/entities/app_users_step.entity';
import { ChangePasswordDto } from 'src/auth/dto/change-password.dto';
import { mailConfig } from 'src/common/Email/mail-config';
import { emailVerify } from 'src/common/Email/verify-email-template';
import {
  generateRandomString,
  getCompleteUrl,
  isEmpty,
} from 'src/common/helper';
import isValidUuidV4 from 'src/common/uuid validator/uuid-validate';
import { Group } from 'src/groups/entities/group.entity';
import { GroupsRepository } from 'src/groups/groups.repository';
import { lan } from 'src/lan';
import { Between, EntityManager, Repository } from 'typeorm';
import { AppUserRepository } from './app_users.repository';
import { CreateAppUserDto } from './dto/create-app_user.dto';
import { UpdateAppUserDto } from './dto/update-app_user.dto';
import { AppUser } from './entities/app_user.entity';
import { ProvincesRepository } from 'src/provinces/provinces.repository';

@Injectable()
export class AppUsersService {
  constructor(
    @InjectRepository(AppUser)
    private readonly userEntity: Repository<AppUser>,
    private readonly userRepository: AppUserRepository,
    private readonly groupRepository: GroupsRepository,
    @InjectRepository(AppUsersStep)
    private readonly userStepsEntity: Repository<AppUsersStep>,
    private readonly provinceRepository: ProvincesRepository,
  ) {}

  async create(createAppUserDto: CreateAppUserDto, manager: EntityManager) {
    if (!isEmpty(createAppUserDto.email)) {
      const data = await this.userRepository.findByEmail(
        createAppUserDto.email,
      );

      if (!isEmpty(data)) {
        throw new BadRequestException(lan('common.email_already_exists'));
      }
    } else {
      delete createAppUserDto.email;
    }

    if (!isEmpty(createAppUserDto.province_id)) {
      const province = await this.provinceRepository.findById(
        createAppUserDto.province_id,
      );

      if (isEmpty(province)) {
        throw new BadRequestException(lan('province_not_found'));
      }
    }

    if (!isEmpty(createAppUserDto.social_id)) {
      const data = await this.userRepository.findOneBySocialId(
        createAppUserDto.social_id,
      );

      if (!isEmpty(data)) {
        throw new BadRequestException(lan('common.social_id_already_exists'));
      }
    } else {
      delete createAppUserDto.social_id;
      delete createAppUserDto.social_type;
    }

    if (isEmpty(createAppUserDto.step_goal)) {
      const goalValue = await this.userRepository.getUsersGoals();
      const latestGoal = goalValue.goal;
      createAppUserDto.step_goal = isEmpty(latestGoal) ? '5000' : latestGoal;
    }

    if (!isEmpty(createAppUserDto.password)) {
      createAppUserDto.password = await bcrypt.hash(
        createAppUserDto.password,
        10,
      );
    }

    if (
      !isEmpty(createAppUserDto.email) &&
      isEmpty(createAppUserDto.social_id)
    ) {
      const randomString = generateRandomString(200);
      const encodedString = Buffer.from(randomString).toString('base64');
      const timestamp = moment().format();
      const stringWithTimestamp = encodedString + timestamp.toString();
      const access_token = Buffer.from(stringWithTimestamp).toString('base64');
      const access_token_expiry = moment.utc().add(60, 'minutes').format();
      createAppUserDto.verification_token = access_token;
      createAppUserDto.verification_token_expiry = access_token_expiry;

      const verifyEmailLink = `${process.env.FRONTEND_DOMAIN}email-verification/${access_token}`;
      const content = emailVerify(verifyEmailLink);
      const transporter = nodemailer.createTransport(mailConfig);

      const mailOptions = {
        from: process.env.EMAIL_FROM,
        to: createAppUserDto.email,
        subject: 'Email Verification',
        html: content,
      };

      await transporter.sendMail(mailOptions);
    } else {
      createAppUserDto.is_email_verified = true;
    }

    if (createAppUserDto.device_id) {
      const existingUser =
        await this.userRepository.findUserByDeviceIdForSkipLogin(
          createAppUserDto.device_id,
        );

      if (!isEmpty(existingUser)) {
        Object.assign(existingUser, createAppUserDto);
        await this.userEntity.save(existingUser);

        return existingUser;
      } else {
        const createAppUser = new AppUser();
        Object.assign(createAppUser, createAppUserDto);
        const userObj = await manager.save(createAppUser);

        if (!isEmpty(userObj.profile_picture)) {
          userObj.profile_picture = getCompleteUrl(userObj.profile_picture);
        }

        return userObj;
      }
    }
  }

  async findAll(take: number, skip: number, search: string) {
    return await this.userRepository.findAll(take, skip, search);
  }

  async update(id: string, updateAppUserDto: UpdateAppUserDto) {
    const updateUser = await this.userRepository.findUserDetailsUpdate(id);

    if (isEmpty(updateUser)) {
      throw new NotFoundException(lan('login.user_not_found'));
    }

    if (!isEmpty(updateUser?.email) && !isEmpty(updateAppUserDto?.email)) {
      if (updateUser.email === updateAppUserDto.email) {
        Object.assign(updateUser, updateAppUserDto);
        await this.userEntity.update(id, updateUser);

        const userObj = await this.userRepository.findUserDetails(id);

        return {
          message: lan('profile.updated_success'),
          data: userObj,
        };
      } else {
        const userExists = await this.userRepository.findByEmail(
          updateAppUserDto.email,
        );

        if (!userExists) {
          // Generate a new token for the user
          const randomString = generateRandomString(200);
          const encodedString = Buffer.from(randomString).toString('base64');
          const timestamp = moment().format();
          const stringWithTimestamp = encodedString + timestamp.toString();
          const access_token =
            Buffer.from(stringWithTimestamp).toString('base64');

          // Set the token's expiration time to be the current time plus desired time
          const access_token_expiry = moment.utc().add(60, 'minutes').format();
          // Save the token and expiration time in the database
          updateUser.verification_token = access_token;
          updateUser.verification_token_expiry = access_token_expiry;
          updateUser.is_email_verified = false;
          await this.userEntity.save(updateUser);

          const verifyEmailLink = `${process.env.FRONTEND_DOMAIN}email-verification/${access_token}`;
          const content = emailVerify(verifyEmailLink);
          const transporter = nodemailer.createTransport(mailConfig);

          const mailOptions = {
            from: process.env.EMAIL_FROM,
            to: updateAppUserDto.email,
            subject: 'Email Verification',
            html: content,
          };

          await transporter.sendMail(mailOptions);

          if (updateUser.email) {
            updateUser.email = updateAppUserDto.email;
          }

          if (updateUser.profile_picture) {
            updateUser.profile_picture = updateAppUserDto.profile_picture;
          }

          if (updateUser.name) {
            updateUser.name = updateAppUserDto.name;
          }

          await this.userEntity.update(id, updateAppUserDto);

          const userObj = await this.userRepository.findUserDetails(id);

          return {
            message: lan('registration.verification_email'),
            data: userObj,
          };
        } else {
          throw new UnauthorizedException(lan('common.email_already_exists'));
        }
      }
      // if the user is having social id then to update him (active/inactive) from admin side
    } else {
      Object.assign(updateUser, updateAppUserDto);
      await this.userEntity.update(id, updateUser);

      const userObj = await this.userRepository.findUserDetails(id);

      return {
        message: lan('profile.updated_success'),
        data: userObj,
      };
    }
  }

  async forgotPassword(
    email: string,
  ): Promise<{ userDetail: Partial<AppUser> | null; token: string | null }> {
    const userDetail = await this.userRepository.findByEmail(email);

    if (userDetail) {
      // Generate a new token for the user
      const randomString = generateRandomString(200);
      const encodedString = Buffer.from(randomString).toString('base64');
      const timestamp = moment().format();
      const stringWithTimestamp = encodedString + timestamp.toString();
      const access_token = Buffer.from(stringWithTimestamp).toString('base64');

      // Set the token's expiration time to be the current time plus desired time
      const access_token_expiry = moment.utc().add(60, 'minutes').format();
      // Save the token and expiration time in the database
      userDetail.password_reset_token = access_token;
      userDetail.password_reset_token_expiry = access_token_expiry;

      await this.userEntity.save(userDetail);

      return { userDetail, token: access_token };
    } else {
      return { userDetail: null, token: null };
    }
  }

  async resetPassword(email: string, token: string, newPassword: string) {
    const validateUser = await this.userRepository.findByEmail(email);

    if (isEmpty(validateUser)) {
      throw new Error(lan('login.user_not_found'));
    }

    const passwordResetTokenExpirationTime = moment(
      validateUser.password_reset_token_expiry,
    )
      .utc(true)
      .valueOf();
    const currentTime = moment().utc().valueOf();

    if (isEmpty(token)) {
      throw new Error(lan('middleware.token_not_found'));
    }

    if (token !== validateUser.password_reset_token) {
      throw new Error(lan('middleware.invalid_token'));
    }

    if (currentTime >= passwordResetTokenExpirationTime) {
      throw new Error(lan('middleware.token_expire'));
    }

    if (validateUser.password_reset_token === token) {
      // Hash the new password
      const hashedNewPassword = await bcrypt.hash(newPassword, 10);

      // Update the password in the users table
      const user = await this.userRepository.findByEmail(email);
      user.password_reset_token = null;
      user.password_reset_token_expiry = null;
      user.password = hashedNewPassword;
      await this.userEntity.save(user);
    }
  }

  async verifyUserEmail(token: string) {
    const validateUser = await this.userRepository.findByVerificationToken(
      token,
    );

    if (!isEmpty(validateUser)) {
      const verificationTokenExpirationTime = moment(
        validateUser.verification_token_expiry,
      )
        .utc(true)
        .valueOf();
      const currentTime = moment().utc().valueOf();

      if (isEmpty(token)) {
        throw new NotFoundException(lan('middleware.token_not_found'));
      } else if (token !== validateUser.verification_token) {
        throw new UnauthorizedException(lan('middleware.invalid_token'));
      } else if (currentTime >= verificationTokenExpirationTime) {
        throw new BadRequestException(lan('middleware.token_expire'));
      } else if (validateUser.verification_token === token) {
        validateUser.is_email_verified = true;
        validateUser.verification_token = null;
        validateUser.verification_token_expiry = null;
        await this.userEntity.save(validateUser);
      }
    } else {
      throw new NotFoundException(lan('middleware.invalid_token'));
    }
  }

  async verifyUserEmailByAdmin(email: string) {
    const userDetail = await this.userRepository.findByEmail(email);

    if (!isEmpty(userDetail)) {
      // Generate a new token for the user
      const randomString = generateRandomString(200);
      const encodedString = Buffer.from(randomString).toString('base64');
      const timestamp = moment().format();
      const stringWithTimestamp = encodedString + timestamp.toString();
      const access_token = Buffer.from(stringWithTimestamp).toString('base64');

      // Set the token's expiration time to be the current time plus desired time
      const access_token_expiry = moment.utc().add(60, 'minutes').format();
      // Save the token and expiration time in the database
      userDetail.verification_token = access_token;
      userDetail.verification_token_expiry = access_token_expiry;
      await this.userEntity.save(userDetail);

      const verifyEmailLink = `${process.env.FRONTEND_DOMAIN}email-verification/${access_token}`;
      const content = emailVerify(verifyEmailLink);
      const transporter = nodemailer.createTransport(mailConfig);

      const mailOptions = {
        from: process.env.EMAIL_FROM,
        to: email,
        subject: 'Email Verification',
        html: content,
      };

      await transporter.sendMail(mailOptions);
    }
  }

  async login(
    createAppUserDto: CreateAppUserDto,
    manager: EntityManager,
    deviceId: string,
    fcmToken: string,
  ) {
    const { email, password, social_id } = createAppUserDto;

    if (isEmpty(email) && isEmpty(social_id)) {
      throw new BadRequestException(lan('login.email_or_social_id_required'));
    }

    if (!isEmpty(social_id)) {
      const user = await this.userRepository.findOneBySocialId(social_id);

      if (isEmpty(user)) {
        const randomString = generateRandomString(200);
        const encodedString = Buffer.from(randomString).toString('base64');
        const timestamp = moment().format();
        const stringWithTimestamp = encodedString + timestamp.toString();
        const access_token =
          Buffer.from(stringWithTimestamp).toString('base64');

        const accessTokenExpireToMinutes =
          Number(process.env.ACCESS_TOKEN_EXPIRE || 24) * 60 * 30;

        // Set the token's expiration time to be the current time plus desired time
        const access_token_expiry = moment
          .utc()
          .add(accessTokenExpireToMinutes, 'minutes')
          .format();

        if (isEmpty(createAppUserDto.step_goal)) {
          const goalValue = await this.userRepository.getUsersGoals();
          const latestGoal = goalValue.goal;
          createAppUserDto.step_goal = isEmpty(latestGoal)
            ? '5000'
            : latestGoal;
        }

        createAppUserDto.is_email_verified = true;

        if (deviceId) {
          const existingUser =
            await this.userRepository.findUserByDeviceIdForSkipLogin(deviceId);

          if (!isEmpty(existingUser) && isEmpty(existingUser.social_id)) {
            createAppUserDto.access_token = access_token;
            createAppUserDto.access_token_expiry = access_token_expiry;
            createAppUserDto.device_id = deviceId;
            createAppUserDto.fcm_token = fcmToken;
            Object.assign(existingUser, createAppUserDto);
            await this.userEntity.save(existingUser);
            const data = await this.userRepository.findOneBySocialId(social_id);

            return data;
          } else {
            const createAppUser = new AppUser();
            createAppUserDto.access_token = access_token;
            createAppUserDto.access_token_expiry = access_token_expiry;
            createAppUserDto.device_id = deviceId;
            createAppUserDto.fcm_token = fcmToken;
            Object.assign(createAppUser, createAppUserDto);
            await manager.save(createAppUser);

            return createAppUser;
          }
        }
      } else {
        if (user.status === 0) {
          throw new BadRequestException(lan('login.account_inactive'));
        }

        const randomString = generateRandomString(200);
        const encodedString = Buffer.from(randomString).toString('base64');
        const timestamp = moment().format();
        const stringWithTimestamp = encodedString + timestamp.toString();
        const access_token =
          Buffer.from(stringWithTimestamp).toString('base64');

        const accessTokenExpireToMinutes =
          Number(process.env.ACCESS_TOKEN_EXPIRE || 24) * 60 * 30;

        // Set the token's expiration time to be the current time plus desired time
        const access_token_expiry = moment
          .utc()
          .add(accessTokenExpireToMinutes, 'minutes')
          .format();

        // Save the token and expiration time in the database
        user.access_token = access_token;
        user.access_token_expiry = access_token_expiry;
        user.device_id = deviceId;
        user.fcm_token = fcmToken;

        await this.userEntity.save(user);
        const data = await this.userRepository.findOneBySocialId(social_id);

        return data;
      }
    }

    if (isEmpty(password)) {
      throw new BadRequestException(lan('login.password_required'));
    }

    const user = await this.userRepository.findByEmail(email);

    if (isEmpty(user)) {
      throw new NotFoundException(lan('login.user_not_found'));
    }

    if (user.status === 0) {
      throw new BadRequestException(lan('login.account_inactive'));
    }

    const isPasswordValid = await bcrypt.compare(password, user.password);

    if (!isPasswordValid) {
      throw new BadRequestException(lan('login.invalid_password'));
    }

    if (user.is_email_verified === false) {
      throw new UnauthorizedException(lan('login.email_not_verified'));
    }

    const randomString = generateRandomString(200);
    const encodedString = Buffer.from(randomString).toString('base64');
    const timestamp = moment().format();
    const stringWithTimestamp = encodedString + timestamp.toString();
    const access_token = Buffer.from(stringWithTimestamp).toString('base64');

    const accessTokenExpireToMinutes =
      Number(process.env.ACCESS_TOKEN_EXPIRE || 24) * 60 * 30;

    // Set the token's expiration time to be the current time plus desired time
    const access_token_expiry = moment
      .utc()
      .add(accessTokenExpireToMinutes, 'minutes')
      .format();

    // Save the token and expiration time in the database
    user.access_token = access_token;
    user.access_token_expiry = access_token_expiry;
    user.device_id = deviceId;
    user.fcm_token = fcmToken;

    await this.userEntity.save(user);
    const data = await this.userRepository.findByEmail(email);

    return data;
  }

  async findUserStepsForSingleSteps(userId: string, fromDate?: string) {
    let whereCondition: any = { app_user_id: userId };

    if (!isEmpty(fromDate)) {
      // Convert fromDate to UTC and include time up to the end of the day
      const fromDateUTC = moment.utc(fromDate).startOf('day').toDate();

      // Create a condition to find records from fromDateUTC to current date
      whereCondition.created_at = Between(fromDateUTC, new Date());
    }

    const data = await this.userStepsEntity.find({
      where: whereCondition,
    });

    if (!isEmpty(data)) {
      const groupedData = data.reduce((acc, curr) => {
        const date = moment(curr.created_at).utc().format('YYYY-MM-DD'); // Format date to UTC

        // If this date is not yet in the accumulator, add it with the current steps
        if (!acc[date]) {
          acc[date] = Number(curr.steps);
        } else {
          // If this date is already in the accumulator, add the current steps to the existing steps
          acc[date] += Number(curr.steps);
        }

        return acc;
      }, {});

      const formattedData = Object.entries(groupedData).map(
        ([date, steps]) => ({
          date: new Date(date),
          steps,
        }),
      );
      const steps = formattedData[0].steps;

      return steps;
    } else {
      return 0;
    }
  }

  async changePassword(changePassDto: ChangePasswordDto) {
    const { email, password, new_password, confirm_password } = changePassDto;

    const user = await this.userRepository.findByEmail(email);

    if (isEmpty(user)) {
      throw new NotFoundException(lan('login.user_not_found'));
    }

    if (isEmpty(user.password)) {
      throw new BadRequestException(lan('login.password_not_found'));
    }

    const isPasswordValid = await bcrypt.compare(password, user.password);

    if (!isPasswordValid) {
      throw new UnauthorizedException(
        lan('change.password.invalid_old_password'),
      );
    }

    if (new_password !== confirm_password) {
      throw new BadRequestException(
        lan('change.password.password_do_not_match'),
      );
    }

    const hashedNewPassword = await bcrypt.hash(new_password, 10);
    user.password = hashedNewPassword;

    await this.userEntity.save(user);
    const data = await this.userRepository.findByEmail(email);

    return data;
  }

  async logout(id: string): Promise<void> {
    if (!isValidUuidV4(id)) {
      throw new UnauthorizedException(lan('login.user_not_found'));
    }

    const user = await this.userRepository.findUserId(id);

    if (isEmpty(user)) {
      throw new UnauthorizedException(lan('login.user_not_found'));
    }
  }

  async findUser(id: string) {
    return await this.userRepository.findOne(id);
  }

  async skipLogin(createAppUserDto: CreateAppUserDto, fcmToken: any) {
    const deviceId = createAppUserDto.device_id;

    const goalValue = await this.userRepository.getUsersGoals();
    const latestGoal = goalValue.goal;
    createAppUserDto.step_goal = isEmpty(latestGoal) ? '5000' : latestGoal;

    const randomString = generateRandomString(200);
    const encodedString = Buffer.from(randomString).toString('base64');
    const timestamp = moment().format();
    const stringWithTimestamp = encodedString + timestamp.toString();
    const access_token = Buffer.from(stringWithTimestamp).toString('base64');

    const accessTokenExpireToMinutes =
      Number(process.env.ACCESS_TOKEN_EXPIRE || 24) * 60 * 30;

    // Set the token's expiration time to be the current time plus desired time
    const access_token_expiry = moment
      .utc()
      .add(accessTokenExpireToMinutes, 'minutes')
      .format();

    if (!isEmpty(deviceId)) {
      const userExists =
        await this.userRepository.findUserByDeviceIdForSkipLogin(deviceId);

      if (userExists) {
        userExists.access_token = access_token;
        userExists.access_token_expiry = access_token_expiry;
        userExists.fcm_token = fcmToken;
        Object.assign(userExists, createAppUserDto);
        await this.userEntity.save(userExists);
        const data = await this.userRepository.findOne(userExists.id);

        return data;
      } else {
        // Save the token and expiration time in the database
        createAppUserDto.access_token = access_token;
        createAppUserDto.access_token_expiry = access_token_expiry;
        createAppUserDto.fcm_token = fcmToken;
        const createAppUser = new AppUser();
        Object.assign(createAppUser, createAppUserDto);
        await this.userEntity.save(createAppUser);
        const returnData = await this.userRepository.findOne(createAppUser.id);

        return returnData;
      }
    }
  }

  async associateUserWithGroup(userId: string, groupId: string): Promise<void> {
    if (!isValidUuidV4(userId)) {
      throw new BadRequestException(lan('common.invalid_uuid_format'));
    }

    const user = await this.userRepository.findUserId(userId);

    if (isEmpty(user)) {
      throw new NotFoundException(lan('login.user_not_found'));
    }

    if (!isValidUuidV4(groupId)) {
      throw new BadRequestException(lan('common.invalid_uuid_format'));
    }

    const groupObj = await this.groupRepository.findOneById(groupId);

    if (isEmpty(groupObj)) {
      throw new NotFoundException(lan('group.invalid_group_id'));
    }

    const existingGroup = user.user_groups.find(
      (group) => group.id === groupId,
    );

    if (existingGroup) {
      throw new BadRequestException(lan('group.user_exists'));
    }

    const group = new Group();
    group.id = groupId;
    user.user_groups.push(group);

    await this.userEntity.save(user);
  }

  async getUserGroups(userId: string, search: string): Promise<any> {
    const user = await this.userRepository.findUserGroups(userId, search);

    if (isEmpty(user)) {
      throw new NotFoundException(lan('user.not_associated_with_group'));
    }

    return user;
  }

  async getExploreGroups(userId: string, search: string): Promise<any> {
    const data = await this.userRepository.findExploreGroups(userId, search);

    if (isEmpty(data)) {
      throw new NotFoundException(lan('common.not_found'));
    }

    return data;
  }

  async updateUserGoals(goal: string): Promise<any> {
    const users = await this.userRepository.findAllUsers();

    if (isEmpty(users)) {
      throw new NotFoundException(lan('common.not_found'));
    }

    const updatedUsers = await Promise.all(
      users.map(async (user) => {
        user.step_goal = goal;
        return await this.userEntity.save(user);
      }),
    );

    return updatedUsers;
  }

  async getStepGoals() {
    return await this.userRepository.getUsersGoals();
  }

  async removeUserFromGroup(userId: string, groupId: string): Promise<void> {
    if (!isValidUuidV4(userId)) {
      throw new BadRequestException(lan('common.invalid_uuid_format'));
    }

    const user = await this.userRepository.findUserId(userId);

    if (isEmpty(user)) {
      throw new NotFoundException(lan('login.user_not_found'));
    }

    if (!isValidUuidV4(groupId)) {
      throw new BadRequestException(lan('common.invalid_uuid_format'));
    }

    const groupObj = await this.groupRepository.findOneById(groupId);

    if (isEmpty(groupObj)) {
      throw new NotFoundException(lan('group.invalid_group_id'));
    }

    user.user_groups = user.user_groups.filter((g) => g.id !== groupId);
    await this.userEntity.save(user);
  }

  async deleteUser(id: string) {
    if (!isValidUuidV4(id)) {
      throw new BadRequestException(lan('common.invalid_uuid_format'));
    }

    const user = await this.userRepository.findUserId(id);

    if (isEmpty(user)) {
      throw new NotFoundException(lan('common.data_not_found'));
    }

    if (user.profile_picture) {
      const filePath = path.join(`public/${user.profile_picture}`);
      if (fs.existsSync(filePath)) {
        fs.unlinkSync(filePath);
      }
    }

    return await this.userEntity.delete(id);
  }

  async updateStepTrackingPermission(
    userId: string,
    isStepTrackingEnabled: boolean,
  ) {
    const user = await this.userRepository.findUserDetails(userId);

    if (isEmpty(user)) {
      throw new NotFoundException(lan('login.user_not_found'));
    }
    user.is_step_tracking_enabled = isStepTrackingEnabled;
    return this.userEntity.update(userId, user);
  }
}
