import { BadGatewayException, BadRequestException, Injectable, NotFoundException, ServiceUnavailableException, UnauthorizedException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { AppConfigService } from "../../share/app-config.service";
import { generateHash, validateHash } from "src/common/hash";
import { User } from "../../database/models/user.model";
import { UsersService } from "../users/users.service";
import { LoginAdminDto } from "./dto/login-admin.dto";
import { TokenDto } from "./dto/token.dto";
import { Op } from "sequelize";
import { RegisterDto } from "./dto/register.dto";
import { VerifyUserAccountDto } from "./dto/verify-user-account.dto";
import { UserVerification } from "src/database/models/user_verification.model";
import { InjectModel } from "@nestjs/sequelize";
import * as moment from "moment";
import { PasswordReset } from "src/database/models/password_reset.model";
import { HttpService } from "../../share/http.service";
import { ResetPasswordDto } from "./dto/reset-password.dto";
import { Client, ClientGrpc } from "@nestjs/microservices";
import { lastValueFrom } from "rxjs";
import { toUrl } from "./../../common/cast";
import { LoginStaffDto } from "./dto/login-staff.dto";
import { error_message } from "src/common/error_message";
import { join as pathJoin } from "path";
import _ from "lodash";
import { TemplateService } from "src/share/template.service";
import { MailService } from "src/share/mail.service";
import { LoggerService } from "src/common/logger/logger.service";
import { randomNumberString } from "src/common/string";
import { ACTIVE, CANCELLED, DEACTIVATE, EXPIRED } from "src/common/constant";
import { ChangePasswordDto } from "./dto/change-password.dto";
import { ChangePinDto } from "./dto/change-pin.dto";
const short = require('short-uuid');


@Injectable()
export class AuthService {
  constructor(
    @InjectModel(UserVerification)
    private readonly userVerificationModel: typeof UserVerification,
    @InjectModel(PasswordReset)
    private readonly passwordResetModel: typeof PasswordReset,
    private readonly usersService: UsersService,
    private readonly jwtService: JwtService,
    private readonly httpService: HttpService,
    private readonly templateService: TemplateService,
    private readonly mailService: MailService,
    private appConfigService: AppConfigService,
    private loggerService: LoggerService
  ) {}

  async isPasswordValid(password, hash): Promise<boolean> {
    return await validateHash(password, hash);
  }

  async validateUser(payload: LoginAdminDto, errorMessage?: string): Promise<User> {
    const user = await this.usersService.findNoScope({
      where: {
        [Op.or]: [
          { username: payload.username},
          { email: payload.username},
          { phoneNumber: payload.username}
        ]
      },
    });
    if (!user || !user.length) throw new UnauthorizedException(error_message.invalid_account);

    const isPasswordValid = await this.isPasswordValid(
      payload.password,
      user[0].password
    );

    if (!isPasswordValid) throw new UnauthorizedException(error_message.invalid_account);

    if (!user[0].active) throw new UnauthorizedException(error_message.account_not_active);

    return user[0];
  }

  async createAccessToken(data: {
    user: number;
  }): Promise<TokenDto> {

    let expiresIn = this.appConfigService.jwt().expirationTime;


    return new TokenDto({
      expiresIn: expiresIn,
      accessToken: await this.jwtService.signAsync({
        type: this.appConfigService.jwt().accessTokenType,
        user: data.user,
      }, {
        expiresIn: expiresIn,
      }),
    });
  }

  async requestResetPassword(username) {
    const user = await this.usersService.findBy({
      [Op.or]: [
        { username: username },
        { email: username },
        { phoneNumber: username },
      ]
    });
    if (!user) throw new NotFoundException(error_message.invalid_account);

    const password_resets = await this.passwordResetModel.findAll({
      where: {
        userId: user.id,
        createdAt: {
          [Op.gte]: moment().startOf('days')
        }
      },
      order: [
        ['createdAt', 'DESC']
      ]
    });
    if (password_resets.length > 0) {
      const last_createdat = password_resets[0].createdAt;
      if (moment().diff(moment(last_createdat), 'minutes') < 1) {
        throw new BadRequestException(error_message.once_a_minute)
      }
      if (password_resets.length > 2) {
        const statuses = [ACTIVE, EXPIRED, CANCELLED] 
        if (statuses.includes(password_resets[0].status) && statuses.includes(password_resets[1].status) && statuses.includes(password_resets[2].status)) {
          throw new BadRequestException(error_message.once_3_times_a_minute)
        }
      }
    }

    await this.passwordResetModel.update({
      status: CANCELLED,
    }, {
      where: {
        userId: user.id,
        status: ACTIVE
      }
    })

    const token = randomNumberString(4);
    const passwordReset = await this.passwordResetModel.create({
      userId: user.id,
      email: user.email,
      token: token,
      expiredAt: moment().add(3, 'minutes').toString(),
      status: ACTIVE
    });

    try {
      const filePath = pathJoin(
        "src/templates/email/forgot-password.hbs"
      );

      const template = await this.templateService.render(filePath, {
        pin: token,
      });

      let name = user.email;

      await this.mailService.sendMail({
        to: [
          {
            name: name,
            email: user.email,
          },
        ],
        subject: "Reset Password",
        htmlContent: template,
      });

      return {
        message: "Email has been sent",
        data: {
          email: passwordReset.email,
          expiredAt: passwordReset.expiredAt
        }
      };
    } catch (error) {
      this.loggerService.error(error);
      throw new BadGatewayException(error_message.failed_send_email);
    }
  }

  async resetPassword(payload: ResetPasswordDto): Promise<any> {
    const { pin, username, newPassword } = payload;

    const user = await this.usersService.findBy({
      [Op.or]: [
        { username: username },
        { email: username },
        { phoneNumber: username },
      ]
    });
    if (!user) throw new NotFoundException(error_message.invalid_account);

    const password_reset = await this.passwordResetModel.findOne({
      where: {
        userId: user.id,
        createdAt: {
          [Op.gte]: moment().startOf('days')
        }
      },
      order: [
        ['createdAt', 'DESC']
      ]
    });
    if (!password_reset) throw new NotFoundException(error_message.invalid_pin);
    if (moment() > moment(password_reset.expiredAt)) throw new BadRequestException(error_message.expired_pin);
    if (password_reset.token != pin) throw new NotFoundException(error_message.invalid_pin);

    (await user).password = newPassword;
    (await user).save();

    (await password_reset).status = DEACTIVATE;
    (await password_reset).save();

    return "Password has been changed";
  }

  async findUserByToken(token: string) {
    const tokenDecode = this.jwtService.decode(token, {
      json: false
    });
    if (!tokenDecode) return null;
    const userId = tokenDecode['user'] ?? null;

    if (moment().unix() > tokenDecode['exp']) {
      return null;
    }

    return this.usersService.findBy({id: userId});
  }

  async changePassword(user, payload: ChangePasswordDto) {
    user = await this.usersService.findOneNoScope({
      where: {
        id: user.id
      }
    })
    const isPasswordValid = await this.isPasswordValid(
      payload.oldPassword,
      user.password
    );
    if (!isPasswordValid) throw new BadRequestException(error_message.invalid_credentials);

    (await user).password = payload.newPassword;
    (await user).save();

    return "Password has been changed";
  }
}
