import { Inject, Injectable, forwardRef } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { users as UserModel, Prisma } from '@prisma/client';
import { CreateUserDto } from './dto/create-user.dto';
import { AuthService } from '../auth/auth.service';
import { RolesService, Role } from '../roles/roles.service';
import { EmailService } from 'src/email/email.service';
// TODO: could likely uninstall this
// import { DateTime } from 'luxon';
import { randomBytes } from 'crypto';
import Sqids from 'sqids';

// Removed the password and refresh token
export type User = {
  user_id: number
  email: string
  firstname: string | null
  lastname: string | null
  suspended: number
  suspended_date: Date | null
  suspended_by: number | null
  created_date: Date
  created_by: number | User
  last_logon_date: Date | null
  user_class_id: number
  username: string
  updated_by: number | User
  updated_date: Date
  title: string | null
  tell: string | null
  status: number
  roles?: Role[]
}
export interface UserWithTokens extends User  {
  access_token?: string | null
  refresh_token?: string | null
}
export type UserFull = UserModel;
export type UserCreate = Prisma.usersCreateInput;
export type UserUpdate = Prisma.usersUpdateInput;

export enum EUserClass {
  IPWStaff = 1,
  IPWMember = 2
};

@Injectable()
export class UsersService {
  constructor(
    private prisma: PrismaService,
    @Inject(forwardRef(() => AuthService))
    private authService: AuthService,
    @Inject(forwardRef(() => RolesService))
    private rolesService: RolesService,
    @Inject(forwardRef(() => EmailService))
    private emailService: EmailService
  ) {}

  public ipwGroupID: number = 1364; // database id for ipw.user_group

  // These "unsafe" functions return the password
  async unsafeFindById(
    user_id: number
  ): Promise<UserFull | null> {
    return this.prisma.users.findUnique({ where: { user_id } });
  }
  async unsafeFindByUsernameOrEmailString(
    username: string
  ): Promise<UserFull | null> {
    return this.prisma.users.findFirst({
      where: {
        OR: [
          { email: username },
          { username: username }
        ]
      }
    });
  }

  // Get the users refresh token
  async getRefreshToken(
    user_id: number
  ): Promise<string | null> {
    const result = await this.prisma.user_refresh_token.findUnique({
      where: { user_id },
      select: {
        refresh_token: true
      }
    });
    if (!result) return null
    return result.refresh_token
  }

  // These functions should censor the password and refresh_token
  censorCredentialsOne(
    user: UserFull | null
  ): User | null {
    if (!user) return null;
    const { password, ...restOfUser } = user;
    return restOfUser;
  }

  censorCredentialsMany(
    users: UserFull[]
  ): User[] {
    let output: User[] = []
    for (let i = 0, n = users.length; i < n; i++) {
      const { password, ...restOfUser } = users[i];
      output.push(restOfUser);
    }
    return output;
  }

  async findById(
    user_id: number
  ): Promise<User | null> {
    const user = await this.prisma.users.findUnique({ where: { user_id } });
    return this.censorCredentialsOne(user);
  }

  async findByIds(
    user_ids: number[],
  ): Promise<User[]> {
    const users = await this.prisma.users.findMany({
      where: {
        OR: user_ids.map(user_id => ({ user_id }))
      }
    });
    return this.censorCredentialsMany(users);
  }

  async findByIdWithRoles(user_id: number): Promise<User | null> {
    const user: User | null = await this.prisma.users.findUnique({ where: { user_id } });
    if (!user) return null;
    const roles = await this.rolesService.findByUserId(user_id);
    user.roles = roles;
    return user;
  }

  async findByUsername(
    username: string,
  ): Promise<User | null> {
    const user = await this.prisma.users.findUnique({ where: { username } });
    return this.censorCredentialsOne(user);
  }

  async findByEmail(
    email: string,
  ): Promise<User | null> {
    const user = await this.prisma.users.findFirst({ where: { email } });
    return this.censorCredentialsOne(user);
  }

  // async findByUsernameOrEmail(
  //   email: string,
  //   username: string
  // ): Promise<User | null> {
  //   const user = await this.prisma.users.findFirst({
  //     where: {
  //       OR: [
  //         { email },
  //         { username }
  //       ]
  //     }
  //   });
  //   return this.censorCredentialsOne(user);
  // }

  async findAll(
    params?: Prisma.usersFindManyArgs
  ): Promise<User[]> {
    if (!params) return this.prisma.users.findMany();
    const { skip, take, cursor, where, orderBy } = params;
    return this.prisma.users.findMany({
      skip,
      take,
      cursor,
      where,
      orderBy,
    });
  }

  // async groupIds(user_id: number): Promise<any> {
  //   const result = await this.prisma.$queryRaw`
  //     SELECT user_group_id
  //     FROM users_has_group
  //     WHERE user_id = ${user_id}
  //   `;
  //   return result;
  // }

  async groupsAndRoles(user_id: number): Promise<any> {
    const result = await this.prisma.$queryRaw`
      SELECT uhg.users_has_group_id, ug.user_group_id, ug.group_name, ug.bis_id
      FROM users u
      INNER JOIN users_has_group uhg
      ON uhg.user_id = u.user_id
      INNER JOIN user_group ug
      ON ug.user_group_id = uhg.user_group_id
      WHERE u.user_id = ${user_id}
      AND ug.user_group_id NOT IN (1364,7361)
    `;
    // 1364 = IPW
    // 7361 = IPW SUSPENDED USERS
    return result;
  }

  // async roleIdForGroup(users_has_group_id: number): Promise<any> {
  //   const result = await this.prisma.$queryRaw`
  //     SELECT ur.roles_id
  //     FROM rbac_users_has_roles ur
  //     INNER JOIN users_has_group uhg
  //     ON uhg.users_has_group_id = ur.users_has_group_id
  //     WHERE uhg.users_has_group_id = ${users_has_group_id}
  //   `;
  //   return result;
  // }

  async roleIdForGroups(users_has_group_ids: bigint[]): Promise<any> {
    if (users_has_group_ids.length === 0) return [];
    const result = await this.prisma.$queryRaw`
      SELECT ur.roles_id, uhg.users_has_group_id
      FROM rbac_users_has_roles ur
      INNER JOIN users_has_group uhg
      ON uhg.users_has_group_id = ur.users_has_group_id
      WHERE uhg.users_has_group_id
      IN (${users_has_group_ids.join(',')})
    `;
    return result;
  }

  async availableGroups(user_id: number): Promise<any> {
    const result = await this.prisma.$queryRaw`
      SELECT distinct ug.user_group_id, ug.group_name
      FROM user_group ug
      INNER JOIN users_has_group uhg
      ON uhg.user_group_id = ug.user_group_id
      WHERE uhg.user_group_id NOT IN (
        SELECT user_group_id
        FROM users_has_group
        WHERE user_id = ${user_id}
      )
      AND uhg.user_group_id NOT IN (
        SELECT user_group_id
        FROM users_has_group_pending
        WHERE user_id = ${user_id}
      )
      AND uhg.user_group_id NOT IN (1364,7361)
      ORDER BY ug.group_name ASC
    `;
    // 1364 = IPW
    // 7361 = IPW SUSPENDED USERS
    return result;
  }

  async joinGroup(user_group_id: number, user_id: number, added_by_user_id: number) {
    const result = await this.prisma.$executeRaw`
      INSERT INTO users_has_group_pending (user_group_id,user_id,status,status_changed_by,status_changed_date)
      VALUES (${user_group_id},${user_id},0,${added_by_user_id},NOW())
    `;
    return result;
  }

  async groupIsExists(user_id: number, user_group_id: number) {
    // Ensure that it is not pending
    const countOfPendingGroups = await this.prisma.users_has_group_pending.count({
      where: {
        user_group_id,
        user_id
      }
    });
    if (countOfPendingGroups > 0) return true;
    // Ensure that it is not already assigned to the user
    const countOfUserGroups = await this.prisma.users_has_group.count({
      where: {
        user_group_id,
        user_id
      }
    });
    return countOfUserGroups > 0;
  }

  async pendingGroups(user_id: number): Promise<any> {
    const result = await this.prisma.$queryRaw`
      SELECT uhgp.users_has_group_pending_id, ug.group_name, ug.bis_id, uhgp.status
      FROM users_has_group_pending uhgp
      INNER JOIN user_group ug
      ON ug.user_group_id = uhgp.user_group_id
      WHERE uhgp.user_id = ${user_id}
      AND uhgp.status <> 1
      ORDER BY group_name ASC
    `;
    return result;
  }

  async cancelPendingGroup(users_has_group_pending_id: number) {
   await this.prisma.users_has_group_pending.delete({
      where: {
        users_has_group_pending_id
      }
    });
    return;
  }

  async create(
    createdByUserId: number,
    payload: CreateUserDto,
    // staff, auditors or default of regular members
    ipwMember: boolean = false
  ) {
    // Add the record to the database
    const now =  new Date();//DateTime.local().setZone('utc').toFormat('yyyy-mm-dd HH:mm:ss');
    const random = randomBytes(20).toString('hex');
    // For legacy system compatibility (v1.0/php)
    const password = await this.authService.createMd5Hash(random);
    const { title, firstname, lastname, telephone, email, username } = payload;
    const data: Prisma.usersCreateInput = {
      title,
      firstname,
      lastname,
      tell: telephone,
      email,
      username,
      password,
      created_date: now,
      created_by: createdByUserId,
      updated_date: now,
      updated_by: createdByUserId,
      user_class_id: !ipwMember ? 2 : 1, // 1 = ipw staff, auditors, 2 = members
      status: 1 // active / 0 = suspended
    };
    // Create the user record
    const user = await this.prisma.users.create({ data });
    // Create the new password (v2.0/nodejs)
    const newPassword = await this.authService.createHash(random);
    await this.prisma.user_password.create({
      data: {
        user_id: user.user_id,
        password: newPassword
      }
    });
    // In the case of IPW members, add them to the entity
    if (ipwMember) await this.prisma.users_has_group.create({
      data: {
        user_group_id: this.ipwGroupID,
        user_id: user.user_id
      }
    });
    // Generate the registration confirmation link
    const { link, token } = await this.generateConfirmationLink('registration', user.user_id);
    await this.prisma.user_registration_token.create({
      data: {
        registration_token: token,
        user_id: user.user_id
      }
    });
    // Send the email
    await this.emailService.create({
      to: user.email,
      template: 'registration',
      data: {
        first_name: user.firstname as string,
        confirmation_link: link
      }
    });
    return this.censorCredentialsOne(user);
  }

  async update(
    user_id: number,
    data: Prisma.usersUpdateInput
  ): Promise<User | null> {
    const user = await this.prisma.users.update({ where: { user_id }, data });
    return this.censorCredentialsOne(user);
  }

  async remove(
    user_id: number
  ): Promise<User> {
    return this.prisma.users.delete({ where: { user_id } });
  }

  /*
   * @description Generates a link for confirmation of high level actions on a users account
   * @param type Can be one of: registration, change-of-email, change-of-password
   * @param user_id The record id for the user account where the link is valid
   * @return A url which is commonly used within email templates
   */
  async generateConfirmationLink(type: string, user_id: number): Promise<{ link: string, token: string }> {
    const { NODE_ENV, DOMAIN, PORT, NUXT_CLIENT_LOCATION } = process.env;
    const token = await this.generateRegistrationToken(user_id);
    const hash = this.encodeId(user_id);
    const link = `${NUXT_CLIENT_LOCATION}/confirm/${type}/${hash}/${token}`;
    return { link, token };
  }

  /*
   * @description Generates a random values for email validation
   * @return A random string
   */
  randomHash(): string {
    return randomBytes(256 / 8).toString('hex');
  }

  createHashidsInstance() {
    // salt, minlen
    return new Sqids({
      alphabet: process.env.SQIDS_SALT,
      minLength: 10
    });
  }

  encodeId(user_id: number | bigint): string {
    // TODO: remove when this has been completed: https://github.com/sqids/sqids-javascript/issues/4
    // return this.createHashidsInstance().encode(user_id);
    return this.createHashidsInstance().encode([Number(user_id)]);
  }

  decodeId(hash: string): bigint {
    const result = this.createHashidsInstance().decode(hash);
    return BigInt(result[0]);
  }

  /*
   * @description This function generates a random values for email verification
   * @return A random string
   */
  async generateRegistrationToken(user_id: number): Promise<string> {
    const hash = this.randomHash();
    const exists = await this.registrationTokenExists(user_id, hash);
    if (exists) return await this.generateRegistrationToken(user_id);
    return hash;
  }

  async registrationTokenExists(
    user_id: number,
    registration_token: string
  ): Promise<boolean> {
    const count = await this.prisma.user_registration_token.count({ where: { registration_token, user_id } });
    return count > 0;
  }

  /*
   * @description This function generates a random values for email confirmation of resetting the users password
   * @return A random string
   */
  async generateResetToken(user_id: number): Promise<string> {
    const hash = this.randomHash();
    const exists = await this.resetTokenExists(user_id, hash);
    if (exists) return await this.generateResetToken(user_id);
    return hash;
  }

  async resetTokenExists(
    user_id: number,
    reset_token: string
  ): Promise<boolean> {
    const count = await this.prisma.user_reset_token.count({ where: { reset_token, user_id } });
    return count > 0;
  }

  /*
   * @description Determine the record id of the user group relationship
   * @return null if no match or the record id
   */
  async findUserHasGroupId(user_id: number, user_group_id: number): Promise<number | null> {
    const userHasGroup = await this.prisma.users_has_group.findFirst({
      where: {
        user_group_id,
        user_id
      },
      select: {
        users_has_group_id: true
      }
    });
    if (!userHasGroup) return null;
    return userHasGroup.users_has_group_id;
  }

  /*
   * @description Check to see if x user is managed by x user
   * For end-user members, does the user have access to an org that the other user manages
   * For IPW users, is the user managed by the other user
   * @return a boolean result
   */
  // async isUsersManager(user_id: number, added_by_user_id: number): Promise<boolean> {
  //   let result = false;
  //   // Get the users class
  //   const users = await this.prisma.users.findMany({
  //     where: {
  //       OR: [
  //         { user_id },
  //         { user_id: added_by_user_id }
  //       ]
  //     },
  //     select: {
  //       user_id: true,
  //       user_class_id: true
  //     }
  //   });
  //   const [user, manager] = users;
  //   // Handle regular members
  //   if (user.user_class_id === 2) {
  //     // Get the users organisations
  //   }
  //   // Handle IPW users
  //   if (user.user_class_id === 1) {
  //     // Check if the other user is a manager of IPW
  //     const roleIds = await this.rolesService.findIdsByUserId(manager.user_id);
  //     const isIpwManager = [1,2].includes()
  //   }
  //   //
  //   if (!result) throw new ForbiddenException();
  //   return result;
  // }

  /*
   * @description List the available profiles for user selection in the navigation area
   */
  async getAvailableProfiles(user_id: number): Promise<{
    user_group_id: number;
    user_group_name: string;
    bis_id: number;
    status_desc: string;
    status: number;
  }[]> {
    // Get the current assessment year
    const result = await this.prisma.eval_form.findFirst({
      where: {
        status: 10
      },
      select: {
        assessment_year: true
      },
      orderBy: {
        assessment_year: 'desc'
      },
      skip: 0,
      take: 1
    });
    // Get the available profiles and meta data
    return await this.prisma.$queryRawUnsafe(`
      SELECT DISTINCT u.user_group_id, g.group_name, g.bis_id, (
        SELECT CONCAT(
          CASE
            WHEN gef.status = 0 THEN 'New'
            WHEN gef.status = 10 THEN 'Busy'
            WHEN gef.status = 20 THEN 'Submitted for Processing'
            WHEN gef.status = 30 THEN 'Audit Busy'
            WHEN gef.status = 50 THEN 'Pass'
            WHEN gef.status = 51 THEN 'Pass Probation'
            WHEN gef.status = 55 THEN 'Audit Pass'
            WHEN gef.status = 60 THEN 'Fail'
            WHEN gef.status = 61 THEN 'Probation'
            WHEN gef.status = 65 THEN 'Audit Fail'
            WHEN gef.status = 90 THEN 'Deleted/Removed'
          END, ' (${result!.assessment_year})'
        )
        FROM group_eval_form gef
        INNER JOIN eval_form ef on ef.eval_form_id = gef.eval_form_id
        AND ef.assessment_year = ${result!.assessment_year}
        WHERE gef.user_group_id = g.user_group_id
        AND gef.status <> 90
        LIMIT 1
      ) AS statusdesc, (
        SELECT gef.status
        FROM group_eval_form gef
        INNER JOIN eval_form ef on ef.eval_form_id = gef.eval_form_id
        AND ef.assessment_year = ${result!.assessment_year}
        WHERE gef.user_group_id = g.user_group_id
        AND gef.status <> 90 LIMIT 1
      ) as status
      FROM users_has_group u
      INNER JOIN user_group g on g.user_group_id = u.user_group_id
      WHERE u.user_id = ${user_id}
      ORDER BY group_name ASC;
    `);
  }

  /*
   * @description Mock session functionality, store the serlected user profile in the database for later reference
   */
  // async selectProfile(user_id: number, user_group_id: number): Promise<void> {
  //   const countOfRecords = await this.prisma.user_selected_group.count({
  //     where: {
  //       user_id
  //     }
  //   });
  //   if (countOfRecords === 0) await this.prisma.user_selected_group.create({
  //     data: {
  //       user_id,
  //       user_group_id
  //     }
  //   });
  //   else this.prisma.user_selected_group.update({
  //     where: {
  //       user_id
  //     },
  //     data: {
  //       user_group_id
  //     }
  //   });
  //   return;
  // }

  // /*
  //  * @description Get the previous profile selection for the given user
  //  */
  // async selectedProfile(user_id: number) {
  //   const selectedProfile = await this.prisma.user_selected_group.findFirst({
  //     where: {
  //       user_id
  //     },
  //     select: {
  //       user_group_id: true
  //     }
  //   });
  //   return selectedProfile ? selectedProfile.user_group_id : null;
  // }

  // /*
  //  * @description Clear the previous profile selection for the given user
  //  */
  // async clearSelectedProfile(user_id: number) {
  //   await this.prisma.user_selected_group.update({
  //     where: {
  //       user_id
  //     },
  //     data: {
  //       user_group_id: null
  //     }
  //   });
  //   return;
  // }

  /*
   * @description Check if a username exists
   * This is used creating a new ipw staff account or member account
   * @return boolean true if it exists, false if it does not
   */
  async usernameExists(username: string): Promise<boolean> {
    const countOfMatchingUsers = await this.prisma.users.count({
      where: {
        username
      }
    });
    return countOfMatchingUsers > 0;
  }
}
