import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { isVerifyError, verify } from '../../common/jwt';
import { getPublicKey } from '../../common/jwt/jwt';
import { makePassword } from '../../common/password/password';
import { AccessToken } from '../../common/token';
import {
  SortDirection,
  TaskListSortableAttribute,
  isSortDirection,
  isTaskListSortableAttribute,
} from '../../common/types/sort';
import {
  AdB2cService,
  ConflictError,
  isConflictError,
} from '../../gateways/adb2c/adb2c.service';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { SortCriteriaRepositoryService } from '../../repositories/sort_criteria/sort_criteria.repository.service';
import { User as EntityUser } from '../../repositories/users/entity/user.entity';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { GetRelationsResponse, User } from './types/types';
import { EmailAlreadyVerifiedError } from '../../repositories/users/errors/types';
import {
  LICENSE_EXPIRATION_THRESHOLD_DAYS,
  USER_LICENSE_STATUS,
} from '../../constants';
import { DateWithZeroTime } from '../licenses/types/types';
import { Context } from '../../common/log';

@Injectable()
export class UsersService {
  constructor(
    private readonly usersRepository: UsersRepositoryService,
    private readonly sortCriteriaRepository: SortCriteriaRepositoryService,
    private readonly adB2cService: AdB2cService,
    private readonly configService: ConfigService,
    private readonly sendgridService: SendGridService,
  ) {}
  private readonly logger = new Logger(UsersService.name);

  /**
   * Confirms user
   * @param token ユーザ仮登録時に払いだされるトークン
   */
  async confirmUser(token: string): Promise<void> {
    this.logger.log(`[IN] ${this.confirmUser.name}`);
    const pubKey = getPublicKey(this.configService);

    const decodedToken = verify<{
      accountId: number;
      userId: number;
      email: string;
    }>(token, pubKey);
    if (isVerifyError(decodedToken)) {
      throw new HttpException(
        makeErrorResponse('E000101'),
        HttpStatus.BAD_REQUEST,
      );
    }

    try {
      // トランザクションで取得と更新をまとめる
      const userId = decodedToken.userId;
      await this.usersRepository.updateUserVerified(userId);
    } catch (e) {
      console.log(e);
      if (e instanceof Error) {
        switch (e.constructor) {
          case EmailAlreadyVerifiedError:
            throw new HttpException(
              makeErrorResponse('E010202'),
              HttpStatus.BAD_REQUEST,
            );
          default:
            throw new HttpException(
              makeErrorResponse('E009999'),
              HttpStatus.INTERNAL_SERVER_ERROR,
            );
        }
      }

      this.logger.error(`error=${e}`);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
  }

  /**
   * ユーザを作成する
   * @param token
   * @returns account
   */
  async createUser(
    accessToken: AccessToken,
    name: string,
    role: string,
    email: string,
    autoRenew: boolean,
    licenseAlert: boolean,
    notification: boolean,
    authorId?: string | undefined,
    groupID?: number | undefined,
  ): Promise<void> {
    this.logger.log(`[IN] ${this.createUser.name}`);

    //DBよりアクセス者の所属するアカウントIDを取得する
    let adminUser: EntityUser;
    try {
      adminUser = await this.usersRepository.findUserByExternalId(
        accessToken.userId,
      );
    } catch (e) {
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    const accountId = adminUser.account_id;

    //authorIdが重複していないかチェックする
    if (authorId) {
      let isAuthorIdDuplicated = false;
      try {
        isAuthorIdDuplicated = await this.usersRepository.existsAuthorId(
          accountId,
          authorId,
        );
      } catch (e) {
        throw new HttpException(
          makeErrorResponse('E009999'),
          HttpStatus.INTERNAL_SERVER_ERROR,
        );
      }
      if (isAuthorIdDuplicated) {
        throw new HttpException(
          makeErrorResponse('E010302'),
          HttpStatus.BAD_REQUEST,
        );
      }
    }

    // ランダムなパスワードを生成する
    const ramdomPassword = makePassword();

    //Azure AD B2Cにユーザーを新規登録する
    let externalUser: { sub: string } | ConflictError;
    try {
      // idpにユーザーを作成
      externalUser = await this.adB2cService.createUser(
        email,
        ramdomPassword,
        name,
      );
    } catch (e) {
      console.log('create externalUser failed');
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    // メールアドレス重複エラー
    if (isConflictError(externalUser)) {
      throw new HttpException(
        makeErrorResponse('E010301'),
        HttpStatus.BAD_REQUEST,
      );
    }

    //Azure AD B2Cに登録したユーザー情報のID(sub)と受け取った情報を使ってDBにユーザーを登録する
    let newUser: EntityUser;
    // TODO [Task2246] 本来はNULLだが、テーブル定義に誤ってNOTNULLが付いているため、一時的に適当な値を設定
    const accepted_terms_version = 'xxx';
    try {
      // ユーザ作成
      newUser = await this.usersRepository.createNormalUser(
        accountId,
        externalUser.sub,
        role,
        autoRenew,
        licenseAlert,
        notification,
        authorId,
        accepted_terms_version,
      );
    } catch (e) {
      console.log('create user failed');
      switch (e.code) {
        case 'ER_DUP_ENTRY':
          //AuthorID重複エラー
          throw new HttpException(
            makeErrorResponse('E010302'),
            HttpStatus.BAD_REQUEST,
          );
        default:
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
      }
    }

    //Email送信用のコンテンツを作成する
    try {
      // メールの送信元を取得
      const from = this.configService.get<string>('MAIL_FROM') ?? '';

      // メールの内容を構成
      const { subject, text, html } =
        await this.sendgridService.createMailContentFromEmailConfirmForNormalUser(
          accountId,
          newUser.id,
          email,
        );

      //SendGridAPIを呼び出してメールを送信する
      await this.sendgridService.sendMail(email, from, subject, text, html);
    } catch (e) {
      console.log('create user failed');
      console.log(`[NOT IMPLEMENT] [RECOVER] delete user: ${newUser.id}`);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
    this.logger.log(`[OUT] ${this.createUser.name}`);
    return;
  }
  /**
   * confirm User And Init Password
   * @param token ユーザ仮登録時に払いだされるトークン
   */
  async confirmUserAndInitPassword(token: string): Promise<void> {
    this.logger.log(`[IN] ${this.confirmUserAndInitPassword.name}`);

    const pubKey = getPublicKey(this.configService);

    const decodedToken = verify<{
      accountId: number;
      userId: number;
      email: string;
    }>(token, pubKey);
    if (isVerifyError(decodedToken)) {
      throw new HttpException(
        makeErrorResponse('E000101'),
        HttpStatus.BAD_REQUEST,
      );
    }

    // ランダムなパスワードを生成する
    const ramdomPassword = makePassword();
    const { userId, email } = decodedToken;

    try {
      // ユーザー情報からAzure AD B2CのIDを特定する
      const user = await this.usersRepository.findUserById(userId);
      const extarnalId = user.external_id;
      // パスワードを変更する
      await this.adB2cService.changePassword(extarnalId, ramdomPassword);
      // ユーザを認証済みにする
      await this.usersRepository.updateUserVerified(userId);
      // メールの送信元を取得
      const from = this.configService.get<string>('MAIL_FROM') ?? '';
      // TODO [Task2163] ODMS側が正式にメッセージを決めるまで仮のメール内容とする
      const subject = 'A temporary password has been issued.';
      const text = 'temporary password: ' + ramdomPassword;
      const domains = this.configService.get<string>('APP_DOMAIN');
      const html = `<p>OMDS TOP PAGE URL.<p><a href="${domains}">${domains}"</a><br>temporary password: ${ramdomPassword}`;

      // メールを送信
      await this.sendgridService.sendMail(email, from, subject, text, html);
    } catch (e) {
      this.logger.error(`error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case EmailAlreadyVerifiedError:
            throw new HttpException(
              makeErrorResponse('E010202'),
              HttpStatus.BAD_REQUEST,
            );
          default:
            throw new HttpException(
              makeErrorResponse('E009999'),
              HttpStatus.INTERNAL_SERVER_ERROR,
            );
        }
      }
    }
  }

  /**
   * Get Users
   * @param accessToken
   * @returns users
   */
  async getUsers(externalId: string): Promise<User[]> {
    this.logger.log(`[IN] ${this.getUsers.name}`);

    try {
      // DBから同一アカウントのユーザ一覧を取得する
      const dbUsers = await this.usersRepository.findSameAccountUsers(
        externalId,
      );

      // DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
      const externalIds = dbUsers.map((x) => x.external_id);
      const adb2cUsers = await this.adB2cService.getUsers(
        // TODO: 外部連携以外のログ強化時に、ContollerからContextを取得するように修正する
        { trackingId: 'dummy' },
        externalIds,
      );

      // DBから取得した各ユーザーをもとにADB2C情報をマージしライセンス情報を算出
      const users = dbUsers.map((x) => {
        // ユーザーの所属グループ名を取得する
        const groupNames =
          x.userGroupMembers?.map((group) => group.userGroup?.name) ?? [];

        const adb2cUser = adb2cUsers.find((user) => user.id === x.external_id);

        // メールアドレスを取得する
        const mail = adb2cUser.identities.find(
          (identity) => identity.signInType === 'emailAddress',
        ).issuerAssignedId;

        let status = USER_LICENSE_STATUS.NORMAL;

        // ライセンスの有効期限と残日数は、ライセンスが存在する場合のみ算出する
        // ライセンスが存在しない場合は、undefinedのままとする
        let expiration: string | undefined = undefined;
        let remaining: number | undefined = undefined;

        if (x.license) {
          // 有効期限日付 YYYY/MM/DD
          const expiry_date = x.license.expiry_date;
          expiration = `${expiry_date.getFullYear()}/${
            expiry_date.getMonth() + 1
          }/${expiry_date.getDate()}`;

          const currentDate = new DateWithZeroTime();
          // 有効期限までの日数
          remaining = Math.floor(
            (expiry_date.getTime() - currentDate.getTime()) /
              (1000 * 60 * 60 * 24),
          );
          if (remaining <= LICENSE_EXPIRATION_THRESHOLD_DAYS) {
            status = x.auto_renew
              ? USER_LICENSE_STATUS.RENEW
              : USER_LICENSE_STATUS.ALERT;
          }
        } else {
          status = USER_LICENSE_STATUS.NO_LICENSE;
        }

        return {
          id: x.id,
          name: adb2cUser.displayName,
          role: x.role,
          authorId: x.author_id ?? undefined,
          typistGroupName: groupNames,
          email: mail,
          emailVerified: x.email_verified,
          autoRenew: x.auto_renew,
          licenseAlert: x.license_alert,
          notification: x.notification,
          encryption: x.encryption,
          prompt: x.prompt,
          expiration: expiration,
          remaining: remaining,
          licenseStatus: status,
        };
      });

      return users;
    } catch (e) {
      this.logger.error(`error=${e}`);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.NOT_FOUND,
      );
    } finally {
      this.logger.log(`[OUT] ${this.getUsers.name}`);
    }
  }
  /**
   * Updates sort criteria
   * @param paramName
   * @param direction
   * @param token
   * @returns sort criteria
   */
  async updateSortCriteria(
    paramName: TaskListSortableAttribute,
    direction: SortDirection,
    token: AccessToken,
  ): Promise<void> {
    this.logger.log(`[IN] ${this.updateSortCriteria.name}`);
    let user: EntityUser;
    try {
      // ユーザー情報を取得
      const sub = token.userId;
      user = await this.usersRepository.findUserByExternalId(sub);
    } catch (e) {
      this.logger.error(`error=${e}`);

      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    try {
      // ユーザーのソート条件を更新
      await this.sortCriteriaRepository.updateSortCriteria(
        user.id,
        paramName,
        direction,
      );
    } catch (e) {
      this.logger.error(`error=${e}`);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(`[OUT] ${this.updateSortCriteria.name}`);
    }
  }
  /**
   * Gets sort criteria
   * @param token
   * @returns sort criteria
   */
  async getSortCriteria(token: AccessToken): Promise<{
    paramName: TaskListSortableAttribute;
    direction: SortDirection;
  }> {
    this.logger.log(`[IN] ${this.getSortCriteria.name}`);
    let user: EntityUser;
    try {
      // ユーザー情報を取得
      const sub = token.userId;
      user = await this.usersRepository.findUserByExternalId(sub);
    } catch (e) {
      this.logger.error(`error=${e}`);

      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    try {
      // ユーザーのソート条件を取得
      const sortCriteria = await this.sortCriteriaRepository.getSortCriteria(
        user.id,
      );
      const { direction, parameter } = sortCriteria;
      //型チェック
      if (
        !isTaskListSortableAttribute(parameter) ||
        !isSortDirection(direction)
      ) {
        throw new Error('The value stored in the DB is invalid.');
      }
      return { direction, paramName: parameter };
    } catch (e) {
      this.logger.error(`error=${e}`);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(`[OUT] ${this.getSortCriteria.name}`);
    }
  }

  /**
   * 指定したユーザーの文字起こし業務に関連する情報を取得します
   * @param userId
   * @returns relations
   */
  async getRelations(
    context: Context,
    userId: string,
  ): Promise<GetRelationsResponse> {
    this.logger.log(`[IN] [${context.trackingId}] ${this.getRelations.name}`);
    try {
      const user = await this.usersRepository.findUserByExternalId(userId);

      // TODO: PBI2105 本実装時に修正すること
      return {
        authorId: user.author_id,
        authorIdList: [user.author_id, 'XXX'],
        isEncrypted: true,
        encryptionPassword: 'abcd@123?dcba',
        audioFormat: 'DS2(QP)',
        prompt: true,
        workTypeList: [
          {
            workTypeId: 'workType1',
            optionItemList: [
              {
                label: 'optionItem11',
                initialValueType: 2,
                defaultValue: 'default11',
              },
              {
                label: 'optionItem12',
                initialValueType: 2,
                defaultValue: 'default12',
              },
              {
                label: 'optionItem13',
                initialValueType: 2,
                defaultValue: 'default13',
              },
              {
                label: 'optionItem14',
                initialValueType: 2,
                defaultValue: 'default14',
              },
              {
                label: 'optionItem15',
                initialValueType: 2,
                defaultValue: 'default15',
              },
              {
                label: 'optionItem16',
                initialValueType: 2,
                defaultValue: 'default16',
              },
              {
                label: 'optionItem17',
                initialValueType: 2,
                defaultValue: 'default17',
              },
              {
                label: 'optionItem18',
                initialValueType: 2,
                defaultValue: 'default18',
              },
              {
                label: 'optionItem19',
                initialValueType: 1,
                defaultValue: '',
              },
              {
                label: 'optionItem110',
                initialValueType: 3,
                defaultValue: '',
              },
            ],
          },
          {
            workTypeId: 'workType2',
            optionItemList: [
              {
                label: 'optionItem21',
                initialValueType: 2,
                defaultValue: 'default21',
              },
              {
                label: 'optionItem22',
                initialValueType: 2,
                defaultValue: 'default22',
              },
              {
                label: 'optionItem23',
                initialValueType: 2,
                defaultValue: 'defaul23',
              },
              {
                label: 'optionItem24',
                initialValueType: 2,
                defaultValue: 'default24',
              },
              {
                label: 'optionItem25',
                initialValueType: 2,
                defaultValue: 'default25',
              },
              {
                label: 'optionItem26',
                initialValueType: 2,
                defaultValue: 'default26',
              },
              {
                label: 'optionItem27',
                initialValueType: 2,
                defaultValue: 'default27',
              },
              {
                label: 'optionItem28',
                initialValueType: 2,
                defaultValue: 'default28',
              },
              {
                label: 'optionItem29',
                initialValueType: 1,
                defaultValue: '',
              },
              {
                label: 'optionItem210',
                initialValueType: 3,
                defaultValue: '',
              },
            ],
          },
        ],
        activeWorktype: 'workType1',
      };
    } catch (e) {
      this.logger.error(`error=${e}`);

      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(
        `[OUT] [${context.trackingId}] ${this.getRelations.name}`,
      );
    }
  }
}
