import {
  CanActivate,
  ExecutionContext,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { isVerifyError, decode } from '../../jwt';
import { AccessToken } from '../../token';
import { Request } from 'express';
import { retrieveAuthorizationToken } from '../../../common/http/helper';
import { makeErrorResponse } from '../../../common/error/makeErrorResponse';
import { Roles } from '../../types/role';
import { Tiers } from '../../types/tier';
import { ADMIN_ROLES, USER_ROLES, TIERS } from '../../../constants';
export interface RoleSetting {
  roles?: (Roles | Roles[])[];
  tiers?: Tiers[];
}

export class RoleGuard implements CanActivate {
  settings?: RoleSetting;

  // eslint-disable-next-line @typescript-eslint/no-empty-function
  private constructor() {}

  canActivate(context: ExecutionContext): boolean | Promise<boolean> {
    const req = context.switchToHttp().getRequest<Request>();

    const token = retrieveAuthorizationToken(req);
    if (!token) {
      throw new HttpException(
        makeErrorResponse('E000101'),
        HttpStatus.UNAUTHORIZED,
      );
    }

    const payload = decode<AccessToken>(token);
    if (isVerifyError(payload)) {
      throw new HttpException(
        makeErrorResponse('E000101'),
        HttpStatus.UNAUTHORIZED,
      );
    }

    // 設定が空なら通過
    if (!this.settings) {
      return true;
    }

    // 値チェック
    // settingsで設定されていない場合チェック自体行わないので初期値はtrueとする
    let isRoleValid = true;
    let isTierValid = true;
    if (this.settings.roles) {
      isRoleValid = this.isRoles(payload.role);
    }
    if (this.settings.tiers) {
      isTierValid = this.isTier(payload.tier);
    }
    if (!isRoleValid || !isTierValid) {
      throw new HttpException(
        makeErrorResponse('E000101'),
        HttpStatus.UNAUTHORIZED,
      );
    }

    // 権限チェック
    // settingsで設定されていない場合チェック自体行わないので初期値はtrueとする
    let hasRolePermission = true;
    let hasTierPermission = true;
    if (this.settings.roles) {
      hasRolePermission = this.checkRole(payload.role);
    }
    if (this.settings.tiers) {
      hasTierPermission = this.checkTier(payload.tier);
    }
    if (hasRolePermission && hasTierPermission) {
      return true;
    }

    // すべての権限セットに合致していなければ例外を送出
    throw new HttpException(
      makeErrorResponse('E000108'),
      HttpStatus.UNAUTHORIZED,
    );
  }

  /**
   * ※ テストコード以外からの直接呼び出しは禁止。テスト容易性のため、publicメソッドとして切り出したもの。
   * 役割の判別を行う
   * @param role アクセストークンに含まれるroleの値
   * @returns true/false
   */
  checkRole(role: string): boolean {
    const { roles } = this.settings;

    const userRoles = role.split(' ');

    // Role毎にAccessTokenの権限チェックを行う
    for (let i = 0; i < roles.length; i++) {
      const role = roles[i];
      let isValid = false;
      if (Array.isArray(role)) {
        isValid = role.every((x) => userRoles.includes(x));
      } else {
        isValid = userRoles.includes(role);
      }

      // 一つでも合格したら通過
      if (isValid) {
        return true;
      }
    }
    return false;
  }

  /**
   * ※ テストコード以外からの直接呼び出しは禁止。テスト容易性のため、publicメソッドとして切り出したもの。
   * Roleの値の妥当性チェック
   * @param role アクセストークンに含まれるroleの値
   * @returns true/false
   */
  isRoles = (role: string): boolean => {
    const userRoles = role.split(' ') as Roles[];
    const rolesValues = [
      ...Object.values(ADMIN_ROLES),
      ...Object.values(USER_ROLES),
    ];
    //重複チェック
    if (userRoles.length !== new Set(userRoles).size) {
      return false;
    }
    //各要素が正しい値かチェック
    return userRoles.every((role) => {
      if (!rolesValues.includes(role)) {
        return false;
      }
      return true;
    });
  };

  /**
   * 権限の許可設定を指定したGuardを作成する
   * { roles: ['admin', 'author'] } "admin"または"author"なら許可
   * { roles: [['admin', 'author']] } "adminかつauthor"なら許可
   * { roles: ['typist', ['admin', 'author']] } "typist"または"adminかつauthor"なら許可
   * @param [settings]
   * @returns requireds
   */
  static requireds(settings?: RoleSetting): RoleGuard {
    const guard = new RoleGuard();
    guard.settings = settings;
    return guard;
  }

  /**
   * Tierの値の妥当性チェック
   * @param tier アクセストークンに含まれるtierの値
   * @returns true/false
   */
  private isTier = (tier: number): boolean => {
    //値のチェック
    return Object.values(TIERS).includes(
      tier as (typeof TIERS)[keyof typeof TIERS],
    );
  };

  /**
   * ※ テストコード以外からの直接呼び出しは禁止。テスト容易性のため、publicメソッドとして切り出したもの。
   * 階層の判別を行う
   * @param tier アクセストークンに含まれるtierの値
   * @returns true/false
   */
  checkTier(tier: number): boolean {
    const { tiers } = this.settings;

    // 宣言された階層中にパラメータの内容が含まれていればtrue
    return tiers.includes(tier as (typeof TIERS)[keyof typeof TIERS]);
  }
}
