import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import jwt from 'jsonwebtoken';
import jwkToPem from 'jwk-to-pem';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { sign } from '../../common/jwt';
import {
  getPrivateKey,
  getPublicKey,
  isVerifyError,
  verify,
} from '../../common/jwt/jwt';
import {
  AccessToken,
  IDToken,
  JwkSignKey,
  RefreshToken,
  isIDToken,
} from '../../common/token';
import { ADMIN_ROLES, USER_ROLES } from '../../constants';
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
import { User } from '../../repositories/users/entity/user.entity';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { Context } from '../../common/log';

@Injectable()
export class AuthService {
  constructor(
    private readonly adB2cService: AdB2cService,
    private readonly configService: ConfigService,
    private readonly usersRepository: UsersRepositoryService,
  ) {}
  private readonly logger = new Logger(AuthService.name);
  /**
   * Determines whether verified user is
   * @param idToken AzureAD B2Cにより発行されたIDトークン
   * @returns verified user
   */
  async isVerifiedUser(idToken: IDToken): Promise<boolean> {
    this.logger.log(`[IN] ${this.isVerifiedUser.name}`);
    try {
      // IDトークンのユーザーがDBに登録されていてメール認証が完了しているユーザーか検証
      const user = await this.usersRepository.findVerifiedUser(idToken.sub);

      return user !== undefined;
    } catch (e) {
      this.logger.error(`error=${e}`);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(`[OUT] ${this.isVerifiedUser.name}`);
    }
  }

  /**
   * Generates refresh token
   * @param idToken AzureAD B2Cにより発行されたIDトークン
   * @param type 環境を表す文字列(web/desktop/mobile)
   * @returns refresh token
   */
  async generateRefreshToken(
    context: Context,
    idToken: IDToken,
    type: string,
  ): Promise<string> {
    this.logger.log(
      `[IN] [${context.trackingId}] ${this.generateRefreshToken.name}`,
    );
    const lifetimeWeb = this.configService.get('REFRESH_TOKEN_LIFETIME_WEB');
    const lifetimeDefault = this.configService.get(
      'REFRESH_TOKEN_LIFETIME_DEFAULT',
    );
    let user: User;
    // ユーザー情報とユーザーが属しているアカウント情報を取得
    try {
      user = await this.usersRepository.findUserByExternalId(idToken.sub);
      if (!user.account) {
        throw new Error('Account information not found');
      }
    } catch (e) {
      this.logger.error(`error=${e}`);
      this.logger.log(
        `[OUT] [${context.trackingId}] ${this.generateRefreshToken.name}`,
      );
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
    // Tierのチェック
    const minTier = 1;
    const maxTier = 5;
    const userTier = user.account.tier;
    if (userTier < minTier || userTier > maxTier) {
      this.logger.error(
        `Tier from DB is unexpected value. tier=${user.account.tier}`,
      );
      this.logger.log(
        `[OUT] [${context.trackingId}] ${this.generateRefreshToken.name}`,
      );
      throw new HttpException(
        makeErrorResponse('E010206'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
    // 要求された環境用トークンの寿命を決定
    const refreshTokenLifetime = type === 'web' ? lifetimeWeb : lifetimeDefault;
    const privateKey = getPrivateKey(this.configService);

    // ユーザーのロールを設定
    // 万一不正なRoleが登録されていた場合、そのままDBの値を使用すると不正なロールのリフレッシュトークンが発行されるため、
    // ロールの設定値はDBに保存したRoleの値を直接トークンに入れないように定数で設定する
    // ※none/author/typist以外はロールに設定されない
    let role = '';
    if (user.role === USER_ROLES.NONE) {
      role = USER_ROLES.NONE;
    } else if (user.role === USER_ROLES.AUTHOR) {
      role = USER_ROLES.AUTHOR;
    } else if (user.role === USER_ROLES.TYPIST) {
      role = USER_ROLES.TYPIST;
    } else {
      this.logger.error(`Role from DB is unexpected value. role=${user.role}`);
      this.logger.log(
        `[OUT] [${context.trackingId}] ${this.generateRefreshToken.name}`,
      );
      throw new HttpException(
        makeErrorResponse('E010205'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    const token = sign<RefreshToken>(
      {
        //ユーザーの属しているアカウントの管理者にユーザーが設定されていればadminをセットする
        role: `${role} ${
          user.account.primary_admin_user_id === user.id ||
          user.account.secondary_admin_user_id === user.id
            ? ADMIN_ROLES.ADMIN
            : ADMIN_ROLES.STANDARD
        }`,
        tier: user.account.tier,
        userId: idToken.sub,
      },
      refreshTokenLifetime,
      privateKey,
    );

    this.logger.log(
      `[OUT] [${context.trackingId}] ${this.generateRefreshToken.name}`,
    );
    return token;
  }

  /**
   * Generates access token
   * @param refreshToken リフレッシュトークン(jwt)
   * @returns access token(jwt)
   */
  async generateAccessToken(
    context: Context,
    refreshToken: string,
  ): Promise<string> {
    this.logger.log(
      `[IN] [${context.trackingId}] ${this.generateAccessToken.name}`,
    );
    const lifetime = this.configService.get('ACCESS_TOKEN_LIFETIME_WEB');

    const privateKey = getPrivateKey(this.configService);
    const pubkey = getPublicKey(this.configService);

    const token = verify<RefreshToken>(refreshToken, pubkey);
    if (isVerifyError(token)) {
      this.logger.error(`${token.reason} | ${token.message}`);
      this.logger.log(
        `[OUT] [${context.trackingId}] ${this.generateAccessToken.name}`,
      );
      throw new HttpException(
        makeErrorResponse('E000101'),
        HttpStatus.UNAUTHORIZED,
      );
    }

    const accessToken = sign<AccessToken>(
      {
        role: token.role,
        tier: token.tier,
        userId: token.userId,
      },
      lifetime,
      privateKey,
    );

    this.logger.log(
      `[OUT] [${context.trackingId}] ${this.generateAccessToken.name}`,
    );
    return accessToken;
  }
  /**
   * Gets id token
   * @param token
   * @returns id token
   */
  async getVerifiedIdToken(token: string): Promise<IDToken> {
    this.logger.log(`[IN] ${this.getVerifiedIdToken.name}`);

    let kid = '';
    try {
      // JWTトークンのヘッダを見るため一度デコードする
      const decodedToken = jwt.decode(token, { complete: true });
      kid = decodedToken.header.kid;
    } catch (e) {
      this.logger.error(`error=${e}`);
      throw new HttpException(
        makeErrorResponse('E000101'),
        HttpStatus.UNAUTHORIZED,
      );
    }

    let issuer = '';
    try {
      const metadata = await this.adB2cService.getMetaData();
      const keySets = await this.adB2cService.getSignKeySets();

      issuer = metadata.issuer;

      const jwkKey = keySets.find((x) => x.kid === kid);

      if (!jwkKey) {
        throw new Error('Public Key Not Found.');
      }

      const publicKey = this.getPublicKeyFromJwk(jwkKey);

      const verifiedToken = jwt.verify(token, publicKey, {
        algorithms: ['RS256'],
        issuer: [issuer],
      });

      if (!isIDToken(verifiedToken)) {
        throw new Error('invalid format ID token');
      }
      return verifiedToken;
    } catch (e) {
      if (e instanceof Error) {
        const { name, message } = e;
        this.logger.error(`error=${name}: ${message}`);

        switch (e.constructor) {
          case jwt.TokenExpiredError:
            throw new HttpException(
              makeErrorResponse('E000102'),
              HttpStatus.UNAUTHORIZED,
            );
          case jwt.NotBeforeError:
            throw new HttpException(
              makeErrorResponse('E000103'),
              HttpStatus.UNAUTHORIZED,
            );
          case jwt.JsonWebTokenError:
            // メッセージごとにエラーを判定しHTTPエラーを生成
            throw this.makeHttpErrorFromJsonWebTokenErrorMessage(
              message,
              issuer,
            );
          default:
            break;
        }
      } else {
        this.logger.error(`error=${e}`);
      }
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(`[OUT] ${this.getVerifiedIdToken.name}`);
    }
  }

  getPublicKeyFromJwk(jwkKey: JwkSignKey): string {
    try {
      // JWK形式のJSONなのでJWTの公開鍵として使えるようにPEM形式に変換
      const publicKey = jwkToPem({
        kty: 'RSA',
        n: jwkKey.n,
        e: jwkKey.e,
      });

      return publicKey;
    } catch (e) {
      this.logger.error(`error=${e}`);
      throw e;
    } finally {
      this.logger.log(`[OUT] ${this.getPublicKeyFromJwk.name}`);
    }
  }

  /**
   * JWT検証時のError、JsonWebTokenErrorをメッセージごとに仕分けてHTTPエラーを生成
   */
  makeHttpErrorFromJsonWebTokenErrorMessage = (
    message: string,
    issuer: string,
  ): Error => {
    // 署名が不正
    if (message === 'invalid signature') {
      return new HttpException(
        makeErrorResponse('E000104'),
        HttpStatus.UNAUTHORIZED,
      );
    }
    // 想定発行元と異なる
    if (message === `jwt issuer invalid. expected: ${issuer}`) {
      return new HttpException(
        makeErrorResponse('E000105'),
        HttpStatus.UNAUTHORIZED,
      );
    }
    // アルゴリズムが想定と異なる
    if (message === 'invalid algorithm') {
      return new HttpException(
        makeErrorResponse('E000106'),
        HttpStatus.UNAUTHORIZED,
      );
    }
    // トークンの形式が不正
    return new HttpException(
      makeErrorResponse('E000101'),
      HttpStatus.UNAUTHORIZED,
    );
  };
}
