import {
  BadRequestException,
  ConflictException,
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EntityManager, Repository } from 'typeorm';
import { UserEntity } from './entities/user.entity';
import { UserRepository } from './user.repository';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import * as bcrypt from 'bcrypt';
import { Logs } from 'src/api-logs/entities/logs.entity';
import { saveLogs } from 'src/api-logs/api-error-log';
import { UserLoginDto } from './dto/user-login.dto';
import { isEmpty } from 'src/common/helper';
import { commonMessage } from 'src/common/messages';
import { lan } from 'src/lan';
import { ChangePasswordDto } from './dto/change-password.dto';
import isValidUuidV4 from 'src/common/uuid validator/uuid-validate';
import * as moment from 'moment';
import { CourseRepository } from 'src/courses/courses.repository';
import { JobRepository } from 'src/job/job.repository';
import { CityRepository } from 'src/cities/cities.repository';
import * as ExcelJS from 'exceljs';
import { welcomeMailContent } from 'src/common/mail/welcome-mail-template';
import * as nodemailer from 'nodemailer';
import { mailConfig } from 'src/common/mail/mail-config';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(UserEntity)
    private readonly user: Repository<UserEntity>,
    private readonly userRepository: UserRepository,
    private readonly courseRepository: CourseRepository,
    private readonly jobRepository: JobRepository,
    private readonly cityRepository: CityRepository,
    @InjectRepository(Logs)
    private readonly logsRepository: Repository<Logs>,
  ) {}

  generateRandomString(length: number): string {
    let result = '';
    const characters =
      'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+=-[]{}|;:,.<>?';
    for (let i = 0; i < length; i++) {
      result += characters.charAt(
        Math.floor(Math.random() * characters.length),
      );
    }

    return result;
  }

  async processExcelFile(
    filePath: string,
    manager: EntityManager,
  ): Promise<void> {
    const workbook = new ExcelJS.Workbook();
    await workbook.xlsx.readFile(filePath);

    const worksheet = workbook.getWorksheet(1);
    const dataRows = [];
    const columnName = [
      'NAME',
      'SURNAME',
      'NATIVE PLACE',
      'EDUCATION',
      'BATCH1',
      'BATCH2',
      'DATE OF BIRTH',
      'MOBILE',
      'PRESENT CITY',
      'EMAIL ADDRESS',
      'DESIGNATION',
      'FIRM NAME',
      'OCCUPATION ADDRESS',
    ];

    const row = worksheet.getRow(1);
    const rowName: string[] = [];

    row.eachCell({ includeEmpty: false }, (cell) => {
      rowName.push(String(cell.value));
    });

    {
      worksheet.eachRow((row, rowNumber) => {
        if (rowNumber > 1) {
          const rowData = {
            first_name: row.getCell('A').value,
            last_name: row.getCell('B').value,
            home_town: row.getCell('C').value,
            course: row.getCell('D').value,
            start_date: row.getCell('E').value,
            end_date: row.getCell('F').value,
            data_of_birth: row.getCell('G').value,
            contact_no: row.getCell('H').value,
            city_id: row.getCell('I').value,
            job_id: row.getCell('K').value,
            email: row.getCell('J').value,
            designation: row.getCell('L').value,
            company_name: row.getCell('M').value,
            company_address: row.getCell('N').value,
          };
          dataRows.push(rowData);
        }
      });

      for (const data of dataRows) {
        if (!data.email || !data.contact_no) continue;

        // Check if user already exists by email or contact_no
        const existingUser = await manager.findOne(UserEntity, {
          where: [{ email: data.email }, { contact_no: data.contact_no }],
        });

        if (existingUser) {
          console.log(
            `Skipping ${data.first_name} ${data.last_name}, email or mobile already exists`,
          );
          continue; // skip this record
        }
        const createUserDto = new CreateUserDto();

        createUserDto.first_name = data.first_name;
        createUserDto.last_name = data.last_name || data.first_name;
        createUserDto.email = data.email;
        createUserDto.password =
          '$2a$10$YiObI/Ku41/EdV8Wl86AoeWpV3Ral0Fly/97EmQLgSN.32AMTCofm';
        createUserDto.date_of_birth = data.data_of_birth;
        createUserDto.contact_no = data.contact_no;
        createUserDto.city_id = this.toTitleCase(data.city_id);
        createUserDto.designation = data.designation;
        createUserDto.company_name = data.company_name;
        createUserDto.company_address = data.company_address;
        createUserDto.home_town = data.home_town;

        if (data.course) {
          createUserDto.course = data.course
            .split(',')
            .map((course) => course.trim());
        }

        const startYear = data.start_date;
        const endYear = data.end_date;

        const courseYears: any = [];
        for (let year = startYear; year <= endYear; year++) {
          courseYears.push(year);
        }

        createUserDto.batch = courseYears;

        const city = !isEmpty(data.city_id)
          ? await this.cityRepository.findByCity(data.city_id)
          : null;
        const job = !isEmpty(data.job_id)
          ? await this.jobRepository.findJob(data.job_id)
          : null;
        const userEntity = new UserEntity();
        userEntity.first_name = createUserDto.first_name;
        userEntity.last_name = createUserDto.last_name;
        userEntity.email = createUserDto.email;
        userEntity.password = createUserDto.password;
        userEntity.contact_no = createUserDto.contact_no;
        userEntity.status = 1;
        if (city) {
          userEntity.city_id = city.id;
        }
        if (job) {
          userEntity.job_id = job.id;
        }
        userEntity.designation = createUserDto.designation;
        userEntity.company_name = createUserDto.company_name;
        userEntity.company_address = createUserDto.company_address;
        userEntity.home_town = createUserDto.home_town;
        userEntity.batch = createUserDto.batch;
        if (createUserDto.date_of_birth) {
          userEntity.date_of_birth = createUserDto.date_of_birth;
        }
        if (createUserDto.course) {
          userEntity.course = createUserDto.course;
        }

        await manager.save(userEntity);
      }
    }
  }

  private toTitleCase(str: string): string {
    if (str) {
      return str.toLowerCase().replace(/\b\w/g, (char) => char.toUpperCase());
    }
  }
  async create(
    createUserDto: CreateUserDto,
    manager: EntityManager,
  ): Promise<UserEntity> {
    const existingUserByEmail = await this.userRepository.findByIdentifier(
      createUserDto.email,
      'email',
    );

    const existingUserByContact = await this.userRepository.findByIdentifier(
      createUserDto.contact_no,
      'contact_no',
    );

    const userDetails = { ...createUserDto, deleted_at: null, status: 0 };

    const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
    userDetails.password = hashedPassword;

    let userData = null;

    // check if user is already deleted then deleted_at column should be null and update the user
    if (existingUserByEmail?.deleted_at) {
      console.log('existingUserByEmail?.deleted_at -- ');

      userData = await this.user.update(existingUserByEmail.id, userDetails);
    } else if (existingUserByContact?.deleted_at) {
      console.log('existingUserByContact?.deleted_at -- ');

      userData = await this.user.update(existingUserByContact.id, userDetails);
    } else {
      console.log('else -- ');

      if (existingUserByEmail) {
        throw new ConflictException('Email already exists');
      }

      if (existingUserByContact) {
        throw new ConflictException('Contact number already exists');
      }

      const createUser = new UserEntity();

      Object.assign(createUser, createUserDto);

      userData = await manager.save(createUser);
    }

    saveLogs('Return Data', userData, this.logsRepository);

    // send welcome mail
    const content = welcomeMailContent({
      fullName: createUserDto.first_name + ' ' + createUserDto.last_name,
      username: createUserDto.email,
      temporaryPassword: createUserDto.password,
    });

    const transporter = nodemailer.createTransport(mailConfig);

    const mailOptions = {
      from: process.env.EMAIL_FROM,
      to: createUserDto.email,
      subject: 'Welcome to Shree Sthankvasi Jain Chhatralaya',
      html: content,
    };

    await transporter.sendMail(mailOptions);

    return userData;
  }

  async approveUser(id: string, status: number) {
    const user = await this.userRepository.findOneForApproval(id);

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

    user.status = status;
    await this.user.update(id, user);
    return user;
  }

  async update(id: string, updateUserDto: UpdateUserDto) {
    const updateUser = await this.userRepository.findOneForApproval(id);

    if (updateUserDto.password) {
      const hashedPassword = await bcrypt.hash(updateUserDto.password, 10);
      updateUser.password = hashedPassword;
    }

    if (updateUserDto.course && Array.isArray(updateUserDto.course)) {
      updateUser.course = updateUserDto.course;
    }

    Object.assign(updateUser, updateUserDto);

    if (updateUserDto.job_id) {
      const job = await this.jobRepository.findOne(updateUserDto.job_id);
      if (job) {
        updateUser.job_id = job.id;
      }
    }

    if (updateUserDto.city_id) {
      const city = await this.cityRepository.findOne(updateUserDto.city_id);
      if (city) {
        updateUser.city_id = city.id;
      }
    }

    return await this.user.update(id, updateUser);
  }

  async remove(id: string) {
    return this.user.softDelete(id);
  }

  async login(loginDto: UserLoginDto, deviceType: any) {
    const { identifier, password, fcm_token } = loginDto;

    const user = await this.userRepository.findOneForPassword(identifier);

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

    if (deviceType === 'web' && user.is_admin !== 1) {
      throw new UnauthorizedException(lan('common.unauthorized'));
    }

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

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

    if (user.status === 1) {
      const randomString = this.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(6, 'months').format();

      user.access_token = access_token;
      user.access_token_expiry = access_token_expiry;
      user.fcm_token = fcm_token;
    }

    return await this.user.save(user);
  }

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

    const user = await this.userRepository.findOneForPassword(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 BadRequestException(
        lan('change.password.invalid_old_password'),
      );
    }

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

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

    if (isSameAsCurrentPassword) {
      throw new ConflictException(
        lan('change.password.new_password_same_as_old'),
      );
    }

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

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

    return data;
  }

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

    if (userDetail) {
      const randomString = this.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();

      userDetail.password_reset_token = access_token;
      userDetail.password_reset_token_expiry = access_token_expiry;

      await this.user.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.findOneByEmail(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'));
    }

    let isSamePassword;
    if (validateUser.password) {
      isSamePassword = await bcrypt.compare(newPassword, validateUser.password);
    }

    if (isSamePassword) {
      throw new Error(lan('change.password.new_password_same_as_old'));
    }

    if (validateUser.password_reset_token === token) {
      const hashedNewPassword = await bcrypt.hash(newPassword, 10);

      const user = await this.userRepository.findOneByEmail(email);
      user.password_reset_token = null;
      user.password_reset_token_expiry = null;
      user.password = hashedNewPassword;
      await this.user.save(user);
    }
  }

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

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

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

  async findAllActiveUser(
    take: number,
    skip: number,
    search: string,
    years: number[],
    courseIds: string[],
    dateOfBirth: string,
    cityIds: string[],
    jobIds: string[],
    key: string,
    order: string,
  ) {
    return this.userRepository.findAllActiveUser(
      take,
      skip,
      search,
      years,
      courseIds,
      dateOfBirth,
      cityIds,
      jobIds,
      key,
      order,
    );
  }

  async findForApproval(take: number, skip: number) {
    return this.userRepository.findAllForApproval(take, skip);
  }

  async findAllUserByFilter(years: number[], courseIds: string[]) {
    return this.userRepository.filterUsersByYearsAndCourse(years, courseIds);
  }

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

  async resetPasswordById(id: string) {
    if (!isValidUuidV4(id)) {
      throw new BadRequestException(
        commonMessage.invalidUuidFormat.replace('#id', id),
      );
    }

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

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

    const defaultPassword = process.env.RESET_PASSWORD;
    const hashedPassword = await bcrypt.hash(defaultPassword, 10);
    user.password = hashedPassword;

    await this.user.save(user);

    const updatedUser = await this.userRepository.findOne(id);
    return updatedUser;
  }
}
