import {
  BadRequestException,
  Injectable,
  Logger,
  UnauthorizedException,
} from '@nestjs/common';
import { hashSync, compareSync } from 'bcrypt';
import { JwtService } from '@nestjs/jwt';
import {
  LoginTicket,
  OAuth2Client as GoogleOAuth2Client,
  OAuth2Client,
} from 'google-auth-library';
import { RedisClientService } from 'src/cache/cache.service';
import {
  EMAIL_EXISTS,
  EXP_FIRST_LOGIN,
  PHONE_EXISTS,
  TENANT_EXISTS,
  TENANT_NOTFOUND,
  USER_NOTFOUND,
  VERIFY_GOOGLE_TOKEN_FAILED,
  VERIFY_TENANT_FAILED,
  VERIFY_USER_FAILED,
} from 'src/share/eCode';
import { AppConfigService } from 'src/config/app/app-config.service';
import { UserEntity } from 'src/modules/user/entities/user.entity';
import { PrismaService } from 'src/prisma/prisma.service';
import { convertShortTimeToSecond } from 'src/share/tool';
import { ChangePassword, TenantSignUp, UserConfirmSignUp, UserLogin, UserSignUp } from './dto/login.dto';
import { eUserType } from 'src/common/auth/type';
import { AuthPayload } from './decorators/auth.payload';
import { bool } from 'joi';

@Injectable()
export class AuthService {
  private readonly logger = new Logger(AuthService.name);
  private readonly oAuth2client: GoogleOAuth2Client;

  constructor(
    private readonly redisService: RedisClientService,
    private readonly jwtService: JwtService,
    private readonly configService: AppConfigService,
    private prisma: PrismaService,
  ) {
    // const { clientId, clientSecret } = this.configService.get('googleAuth');
    this.oAuth2client = new OAuth2Client({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_SECRET,
    });
  }

  async generateToken(
    account: string,
    dataToken: AuthPayload,
    first_login = Date.now(),
  ) {
    const tid = hashSync(account, 10);
    const payload = {
      tid,
      ...dataToken,
      first_login,
    };

    const payloadRefreshToken = {
      tid,
      ...dataToken,
      first_login,
    };
    const [access_token, refresh_token] = await Promise.all([
      this.jwtService.signAsync(payload, {
        secret: this.configService.jwtAccessSecret,
        expiresIn: this.configService.jwtExpiresIn,
      }),
      this.jwtService.signAsync(payloadRefreshToken, {
        secret: this.configService.jwtRefreshSecret,
        expiresIn: this.configService.jwtRefreshIn,
      }),
    ]);

    return { access_token, refresh_token, tid };
  }

  async userLoginByGoogleOAuth2(token: string): Promise<{
    access_token: string;
    refresh_token: string;
    user: UserEntity;
  }> {
    try {
      const googleLoginTicket: LoginTicket =
        await this.oAuth2client.verifyIdToken({
          idToken: token,
          audience: process.env.GOOGLE_CLIENT_ID,
        });

      const { email } = googleLoginTicket.getPayload();

      let user = await this.prisma.user.findFirst({
        where: {
          email,
        },
      });

      if (!user) {
        user = await this.prisma.user.create({
          data: {
            email,
            fullname: email,
          },
        });
      }
      const { access_token, refresh_token, tid } = await this.generateToken(
        user.email,
        {
          id: user.id,
          account: user.email,
          type: eUserType.USER,
          containsPassword: Boolean(user.password),
        },
      );
      await this.redisService.markLogin(user.id, tid);
      return {
        user: user,
        access_token,
        refresh_token,
      };
    } catch (error) {
      this.logger.error(error);
      if (error instanceof BadRequestException) {
        throw error;
      }
      throw new UnauthorizedException(VERIFY_GOOGLE_TOKEN_FAILED);
    }
  }

  async userLogin(data: UserLogin): Promise<{
    access_token: string;
    refresh_token: string;
    user: UserEntity;
  }> {
    try {
      const user = await this.prisma.user.findFirst({
        where: {
          username: data.username,
        },
      });
      if (!user) {
        throw new BadRequestException(USER_NOTFOUND);
      }
      const { access_token, refresh_token, tid } = await this.generateToken(
        user.username,
        {
          id: user.id,
          account: user.username,
          type: eUserType.USER,
          containsPassword: Boolean(user.password),
        },
      );
      // Check pass
      if (!compareSync(data.password, user.password)) {
        throw new UnauthorizedException(VERIFY_TENANT_FAILED);
      }
      await this.redisService.markLogin(user.id, tid);
      return {
        access_token,
        refresh_token,
        user: user,
      };
    } catch (error) {
      this.logger.error(error);
      if (error instanceof BadRequestException) {
        throw error;
      }
      throw new UnauthorizedException(VERIFY_USER_FAILED);
    }
  }

  async tenantLogin(data: UserLogin): Promise<{
    access_token: string;
    refresh_token: string;
  }> {
    try {
      const tenant = await this.prisma.tenant.findFirst({
        where: {
          username: data.username,
        },
      });

      if (!tenant) {
        throw new BadRequestException(TENANT_NOTFOUND);
      }

      const { access_token, refresh_token, tid } = await this.generateToken(
        tenant.username,
        {
          id: tenant.id,
          account: tenant.username,
          type: eUserType.TENANT,
          containsPassword: Boolean(tenant.password),
        },
      );
      // Check pass
      if (!compareSync(data.password, tenant.password)) {
        throw new UnauthorizedException(VERIFY_TENANT_FAILED);
      }
      await this.redisService.markLogin(tenant.id, tid);
      return {
        access_token,
        refresh_token,
      };
    } catch (error) {
      this.logger.error(error);
      if (error instanceof BadRequestException) {
        throw error;
      }
      throw new UnauthorizedException(VERIFY_TENANT_FAILED);
    }
  }

  async refreshToken(refreshToken: string) {
    let decodedRefreshToken = null;
    try {
      decodedRefreshToken = await this.jwtService.verifyAsync(refreshToken, {
        secret: this.configService.jwtRefreshSecret,
      });
      const { id: sub, tid, type } = decodedRefreshToken;
      if (!(await this.redisService.checkRefreshTokenFound(sub, tid))) {
        throw new UnauthorizedException('Decoded token not found');
      }
      //check first login
      const dateExp: any = new Date(decodedRefreshToken.first_login);
      const timeAutoLogout = this.configService.jwtRefreshIn || '30d';
      dateExp.setSeconds(
        dateExp.getSeconds() + convertShortTimeToSecond(timeAutoLogout),
      );

      if (Date.now() > dateExp) {
        const { sub, tid } = decodedRefreshToken;
        await this.logout(sub, tid);
        throw new UnauthorizedException(EXP_FIRST_LOGIN);
      }

      const user =
        type == eUserType.USER
          ? await this.prisma.user.findFirstOrThrow({
            where: {
              email: decodedRefreshToken.account,
            },
          })
          : await this.prisma.tenant.findFirstOrThrow({
            where: {
              username: decodedRefreshToken.account,
            },
          });

      const {
        access_token,
        refresh_token,
        tid: _tid,
      } = await this.generateToken(
        decodedRefreshToken.account,
        {
          id: user.id,
          account: type == eUserType.USER ? user.email : user.username,
          type,
          containsPassword: Boolean(user.password),
        },
        decodedRefreshToken.first_login,
      );

      await this.logout(sub, tid);
      await this.redisService.markLogin(user.id, _tid);
      return { access_token, refresh_token };
    } catch (error) {
      console.log(error);

      this.logger.error(error.message);
      throw new UnauthorizedException(error.message);
    }
  }

  async logout(uid: string, tid: string) {
    try {
      if (!tid) return false;
      await this.redisService.delMarkLogin(uid, tid);
      return true;
    } catch (err) {
      this.logger.error(err.message);
      return false;
    }
  }

  async signUpTenant(data: TenantSignUp) {
    const { username, password } = data;
    delete data.password_confirmation;
    return await this.prisma.$transaction(async (tx) => {
      const teannt = await tx.tenant.findFirst({
        where: {
          username,
        },
      });
      if (teannt) {
        throw new BadRequestException(TENANT_EXISTS);
      }
      const newTenant = await tx.tenant.create({
        data: {
          ...data,
          password: hashSync(password, 10),
        },
      });

      return { ...newTenant, password: undefined };
    });
  }

  async signUpUser(data: UserSignUp) {
    const { username, password } = data;
    delete data.password_confirmation;
    const userQuery: any[] = [
      {
        username: username,
      },
    ];
    if (data.phone) {
      userQuery.push({ phone: data.phone });
    }
    if (data.email) {
      userQuery.push({ email: data.email });
    }
    return await this.prisma.$transaction(async (tx) => {
      const user = await tx.user.findFirst({
        where: {
          OR: userQuery,
        },
      });
      if (user) {
        if (user.username == username) {
          throw new BadRequestException(TENANT_EXISTS);
        }
        if (user.phone == data.phone) {
          throw new BadRequestException(PHONE_EXISTS);
        }
        if (user.email == data.email) {
          throw new BadRequestException(EMAIL_EXISTS);
        }
        throw new BadRequestException(TENANT_EXISTS);
      }
      const newUser = await tx.user.create({
        data: {
          ...data,
          password: hashSync(password, 10),
        },
      });

      return { ...newUser, password: undefined };
    });
  }

  async confirmSignUpUser(data: UserConfirmSignUp) {
    this.logger.log('confirmSignUpUser', data);
  }

  async changePassword(data: ChangePassword, auth: AuthPayload) {
    const { old_password, password } = data;
    return await this.prisma.$transaction(async (tx) => {
      const user = await tx.user.findFirst({
        where: {
          id: +auth.id,
        },
      });
      if (!user) {
        throw new BadRequestException(USER_NOTFOUND);
      }
      if (!user.password?.length) {
        return;
      }
      if (!compareSync(old_password, user.password)) {
        throw new UnauthorizedException(VERIFY_USER_FAILED);
      }
      const newUser = await tx.user.update({
        where: {
          id: +auth.id,
        },
        data: {
          password: hashSync(password, 10),
        },
      });

      return { ...newUser, password: undefined };
    });

  }
}
