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 {
  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,
  newUser,
} from '../../repositories/users/entity/user.entity';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
import { GetRelationsResponse, User } from './types/types';
import {
  AuthorIdAlreadyExistsError,
  EmailAlreadyVerifiedError,
  EncryptionPasswordNeedError,
  InvalidRoleChangeError,
  UpdateTermsVersionNotSetError,
  UserNotFoundError,
} from '../../repositories/users/errors/types';
import {
  LICENSE_EXPIRATION_THRESHOLD_DAYS,
  MANUAL_RECOVERY_REQUIRED,
  OPTION_ITEM_VALUE_TYPE_NUMBER,
  USER_AUDIO_FORMAT,
  USER_LICENSE_EXPIRY_STATUS,
  USER_ROLES,
} from '../../constants';
import { DateWithZeroTime } from '../licenses/types/types';
import { Context } from '../../common/log';
import { UserRoles } from '../../common/types/role';
import {
  LicenseAlreadyDeallocatedError,
  LicenseExpiredError,
  LicenseUnavailableError,
} from '../../repositories/licenses/errors/types';
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
import { Account } from '../../repositories/accounts/entity/account.entity';

@Injectable()
export class UsersService {
  private readonly logger = new Logger(UsersService.name);
  private readonly mailFrom: string;
  private readonly appDomain: string;
  constructor(
    private readonly accountsRepository: AccountsRepositoryService,
    private readonly usersRepository: UsersRepositoryService,
    private readonly licensesRepository: LicensesRepositoryService,
    private readonly sortCriteriaRepository: SortCriteriaRepositoryService,
    private readonly adB2cService: AdB2cService,
    private readonly configService: ConfigService,
    private readonly sendgridService: SendGridService,
  ) {
    this.mailFrom = this.configService.getOrThrow<string>('MAIL_FROM');
    this.appDomain = this.configService.getOrThrow<string>('APP_DOMAIN');
  }

  /**
   * Confirms user
   * @param token ユーザ仮登録時に払いだされるトークン
   */
  async confirmUser(context: Context, token: string): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${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.updateUserVerifiedAndCreateTrialLicense(
        context,
        userId,
      );

      try {
        const { company_name: companyName } =
          await this.accountsRepository.findAccountById(
            context,
            decodedToken.accountId,
          );

        // アカウント認証が完了した旨をメール送信する
        await this.sendgridService.sendMailWithU101(
          context,
          decodedToken.email,
          companyName,
        );
      } catch (e) {
        this.logger.error(`[${context.getTrackingId()}] error=${e}`);
        // メール送信に関する例外はログだけ出して握りつぶす
      }
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] 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,
            );
        }
      }

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

  /**
   * Creates user
   * @param context
   * @param externalId
   * @param name
   * @param role
   * @param email
   * @param autoRenew
   * @param notification
   * @param [authorId]
   * @param [encryption]
   * @param [encryptionPassword]
   * @param [prompt]
   * @returns user
   */
  async createUser(
    context: Context,
    externalId: string,
    name: string,
    role: UserRoles,
    email: string,
    autoRenew: boolean,
    notification: boolean,
    authorId?: string | undefined,
    encryption?: boolean | undefined,
    encryptionPassword?: string | undefined,
    prompt?: boolean | undefined,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${this.createUser.name} | params: { ` +
        `externalId: ${externalId}, ` +
        `role: ${role}, ` +
        `autoRenew: ${autoRenew}, ` +
        `notification: ${notification}, ` +
        `authorId: ${authorId}, ` +
        `encryption: ${encryption}, ` +
        `prompt: ${prompt} };`,
    );
    //DBよりアクセス者の所属するアカウントIDを取得する
    let adminUser: EntityUser;
    let account: Account | null;
    try {
      adminUser = await this.usersRepository.findUserByExternalId(
        context,
        externalId,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    const accountId = adminUser.account_id;
    account = adminUser.account;

    //authorIdが重複していないかチェックする
    if (authorId) {
      let isAuthorIdDuplicated = false;
      try {
        isAuthorIdDuplicated = await this.usersRepository.existsAuthorId(
          context,
          accountId,
          authorId,
        );
      } catch (e) {
        this.logger.error(`[${context.getTrackingId()}] error=${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(
        context,
        email,
        ramdomPassword,
        name,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      this.logger.error(
        `[${context.getTrackingId()}] 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;

    try {
      //roleに応じてユーザー情報を作成する
      const newUserInfo = this.createNewUserInfo(
        context,
        role,
        accountId,
        externalUser.sub,
        autoRenew,
        notification,
        authorId,
        encryption,
        encryptionPassword,
        prompt,
      );
      // ユーザ作成
      newUser = await this.usersRepository.createNormalUser(
        context,
        newUserInfo,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      this.logger.error(`[${context.getTrackingId()}]create user failed`);
      //リカバリー処理
      //Azure AD B2Cに登録したユーザー情報を削除する
      await this.deleteB2cUser(externalUser.sub, context);

      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 {
      if (account === null) {
        throw new Error(`account is null. account_id=${accountId}`);
      }
      const { primary_admin_user_id: primaryAdminUserId } = account;
      if (primaryAdminUserId === null) {
        throw new Error(
          `primary_admin_user_id is null. account_id=${accountId}`,
        );
      }
      const { external_id: extarnalId } =
        await this.usersRepository.findUserById(context, primaryAdminUserId);

      const primaryAdmimAdb2cUser = await this.adB2cService.getUser(
        context,
        extarnalId,
      );
      const { displayName: primaryAdminUserName } = getUserNameAndMailAddress(
        primaryAdmimAdb2cUser,
      );

      await this.sendgridService.sendMailWithU114(
        context,
        accountId,
        newUser.id,
        email,
        primaryAdminUserName,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      this.logger.error(`[${context.getTrackingId()}] create user failed`);
      //リカバリー処理
      //Azure AD B2Cに登録したユーザー情報を削除する
      await this.deleteB2cUser(externalUser.sub, context);
      // DBからユーザーを削除する
      await this.deleteUser(newUser.id, context);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
    this.logger.log(
      `[OUT] [${context.getTrackingId()}] ${this.createUser.name}`,
    );
    return;
  }

  // Azure AD B2Cに登録したユーザー情報を削除する
  // TODO 「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
  private async deleteB2cUser(externalUserId: string, context: Context) {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.deleteB2cUser.name
      } | params: { externalUserId: ${externalUserId} }`,
    );
    try {
      await this.adB2cService.deleteUser(externalUserId, context);
      this.logger.log(
        `[${context.getTrackingId()}] delete externalUser: ${externalUserId}`,
      );
    } catch (error) {
      this.logger.error(`[${context.getTrackingId()}] error=${error}`);
      this.logger.error(
        `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete externalUser: ${externalUserId}`,
      );
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.deleteB2cUser.name}`,
      );
    }
  }

  // DBに登録したユーザー情報を削除する
  private async deleteUser(userId: number, context: Context) {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.deleteUser.name
      } | params: { userId: ${userId} }`,
    );
    try {
      await this.usersRepository.deleteNormalUser(context, userId);
      this.logger.log(`[${context.getTrackingId()}] delete user: ${userId}`);
    } catch (error) {
      this.logger.error(`[${context.getTrackingId()}] error=${error}`);
      this.logger.error(
        `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete user: ${userId}`,
      );
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.deleteUser.name}`,
      );
    }
  }

  // roleを受け取って、roleに応じたnewUserを作成して返却する
  private createNewUserInfo(
    context: Context,
    role: UserRoles,
    accountId: number,
    externalId: string,
    autoRenew: boolean,
    notification: boolean,
    authorId?: string | undefined,
    encryption?: boolean | undefined,
    encryptionPassword?: string | undefined,
    prompt?: boolean | undefined,
  ): newUser {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.createNewUserInfo.name
      } | params: { ` +
        `role: ${role}, ` +
        `accountId: ${accountId}, ` +
        `authorId: ${authorId}, ` +
        `externalId: ${externalId}, ` +
        `autoRenew: ${autoRenew}, ` +
        `notification: ${notification}, ` +
        `authorId: ${authorId}, ` +
        `encryption: ${encryption}, ` +
        `prompt: ${prompt} };`,
    );
    try {
      switch (role) {
        case USER_ROLES.NONE:
        case USER_ROLES.TYPIST:
          return {
            account_id: accountId,
            external_id: externalId,
            auto_renew: autoRenew,
            notification,
            role,
            accepted_dpa_version: null,
            accepted_eula_version: null,
            accepted_privacy_notice_version: null,
            encryption: false,
            encryption_password: null,
            prompt: false,
            author_id: null,
          };
        case USER_ROLES.AUTHOR:
          return {
            account_id: accountId,
            external_id: externalId,
            auto_renew: autoRenew,
            notification,
            role,
            author_id: authorId ?? null,
            encryption: encryption ?? false,
            encryption_password: encryptionPassword ?? null,
            prompt: prompt ?? false,
            accepted_dpa_version: null,
            accepted_eula_version: null,
            accepted_privacy_notice_version: null,
          };
        default:
          //不正なroleが指定された場合はログを出力してエラーを返す
          this.logger.error(
            `[${context.getTrackingId()}] [NOT IMPLEMENT] [RECOVER] role: ${role}`,
          );
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
      }
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      return e;
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.createNewUserInfo.name}`,
      );
    }
  }

  /**
   * confirm User And Init Password
   * @param token ユーザ仮登録時に払いだされるトークン
   */
  async confirmUserAndInitPassword(
    context: Context,
    token: string,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        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 { accountId, userId, email } = decodedToken;

    try {
      // ユーザー情報からAzure AD B2CのIDを特定する
      const user = await this.usersRepository.findUserById(context, userId);
      const extarnalId = user.external_id;
      // パスワードを変更する
      await this.adB2cService.changePassword(
        context,
        extarnalId,
        ramdomPassword,
      );
      // ユーザを認証済みにする
      await this.usersRepository.updateUserVerified(context, userId);

      // メール送信処理
      try {
        const { external_id: primaryAdminUserExternalId } =
          await this.getPrimaryAdminUser(context, accountId);

        const adb2cUser = await this.adB2cService.getUser(
          context,
          primaryAdminUserExternalId,
        );

        const { displayName: primaryAdminName } =
          getUserNameAndMailAddress(adb2cUser);

        await this.sendgridService.sendMailWithU113(
          context,
          email,
          primaryAdminName,
          ramdomPassword,
        );
      } catch (e) {
        this.logger.error(`[${context.getTrackingId()}] error=${e}`);
        // メール送信に関する例外はログだけ出して握りつぶす
      }
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] 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,
            );
        }
      }
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${
          this.confirmUserAndInitPassword.name
        }`,
      );
    }
  }

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

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

      // DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
      const externalIds = dbUsers.map((x) => x.external_id);
      const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);

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

        //所属グループ名の配列にする
        const groupNames = userGroupMembers.flatMap((userGroupMember) =>
          userGroupMember.userGroup ? [userGroupMember.userGroup.name] : [],
        );

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

        if (adb2cUser == null) {
          throw new Error('mail not found.'); // TODO: リファクタ時に挙動を変更しないようエラー文面をmail not foundのまま据え置き。影響がない事が確認できたらエラー文面を変更する。
        }

        // メールアドレスを取得する
        const { emailAddress: mail } = getUserNameAndMailAddress(adb2cUser);

        //メールアドレスが取得できない場合はエラー
        if (!mail) {
          throw new Error('mail not found.');
        }

        let status = USER_LICENSE_EXPIRY_STATUS.NORMAL;

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

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

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

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

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

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

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

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

    try {
      // ユーザーのソート条件を取得
      const sortCriteria = await this.sortCriteriaRepository.getSortCriteria(
        context,
        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(`[${context.getTrackingId()}] error=${e}`);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.getSortCriteria.name}`,
      );
    }
  }

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

      // ユーザー関連情報を取得
      const { user, authors, worktypes, activeWorktype } =
        await this.usersRepository.getUserRelations(context, id);

      // AuthorIDのリストを作成
      const authorIds = authors.flatMap((author) =>
        author.author_id ? [author.author_id] : [],
      );

      const workTypeList = worktypes?.map((worktype) => {
        return {
          workTypeId: worktype.custom_worktype_id,
          optionItemList: worktype.option_items.map((optionItem) => {
            const initialValueType = OPTION_ITEM_VALUE_TYPE_NUMBER.find(
              (x) => x.type === optionItem.default_value_type,
            )?.value;

            if (!initialValueType) {
              throw new Error(
                `invalid default_value_type ${optionItem.default_value_type}`,
              );
            }

            return {
              label: optionItem.item_label,
              initialValueType,
              defaultValue: optionItem.initial_value,
            };
          }),
        };
      });

      return {
        authorId: user.author_id ?? undefined,
        authorIdList: authorIds,
        workTypeList,
        isEncrypted: user.encryption,
        encryptionPassword: user.encryption_password ?? undefined,
        activeWorktype: activeWorktype?.custom_worktype_id ?? '',
        audioFormat: USER_AUDIO_FORMAT,
        prompt: user.prompt,
      };
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);

      if (e instanceof Error) {
        switch (e.constructor) {
          case UserNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010204'),
              HttpStatus.BAD_REQUEST,
            );
          default:
            throw new HttpException(
              makeErrorResponse('E009999'),
              HttpStatus.INTERNAL_SERVER_ERROR,
            );
        }
      }

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

  /**
   * Updates user
   * @param context
   * @param extarnalId
   * @param id
   * @param role
   * @param authorId
   * @param autoRenew
   * @param notification
   * @param encryption
   * @param encryptionPassword
   * @param prompt
   * @returns user
   */
  async updateUser(
    context: Context,
    extarnalId: string,
    id: number,
    role: string,
    authorId: string | undefined,
    autoRenew: boolean,
    notification: boolean,
    encryption: boolean | undefined,
    encryptionPassword: string | undefined,
    prompt: boolean | undefined,
  ): Promise<void> {
    try {
      this.logger.log(
        `[IN] [${context.getTrackingId()}] ${
          this.updateUser.name
        } | params: { ` +
          `extarnalId: ${extarnalId}, ` +
          `id: ${id}, ` +
          `role: ${role}, ` +
          `authorId: ${authorId}, ` +
          `autoRenew: ${autoRenew}, ` +
          `notification: ${notification}, ` +
          `encryption: ${encryption}, ` +
          `prompt: ${prompt} }`,
      );

      // 実行ユーザーのアカウントIDを取得
      const accountId = (
        await this.usersRepository.findUserByExternalId(context, extarnalId)
      ).account_id;

      // ユーザー情報を更新
      await this.usersRepository.update(
        context,
        accountId,
        id,
        role,
        authorId,
        autoRenew,
        notification,
        encryption,
        encryptionPassword,
        prompt,
      );

      // メール送信処理
      try {
        const { adminEmails } = await this.getAccountInformation(
          context,
          accountId,
        );

        // 変更ユーザー情報を取得
        const { external_id: userExtarnalId } =
          await this.usersRepository.findUserById(context, id);
        const adb2cUser = await this.adB2cService.getUser(
          context,
          userExtarnalId,
        );
        const { displayName: userName, emailAddress: userEmail } =
          getUserNameAndMailAddress(adb2cUser);

        if (userEmail === undefined) {
          throw new Error(`userEmail is null. externalId=${extarnalId}`);
        }

        // プライマリ管理者を取得
        const { external_id: adminExternalId } = await this.getPrimaryAdminUser(
          context,
          accountId,
        );
        const adb2cAdminUser = await this.adB2cService.getUser(
          context,
          adminExternalId,
        );
        const { displayName: primaryAdminName } =
          getUserNameAndMailAddress(adb2cAdminUser);

        await this.sendgridService.sendMailWithU115(
          context,
          userName,
          userEmail,
          primaryAdminName,
          adminEmails,
        );
      } catch (e) {
        this.logger.error(`[${context.getTrackingId()}] error=${e}`);
        // メール送信に関する例外はログだけ出して握りつぶす
      }
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case UserNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010204'),
              HttpStatus.BAD_REQUEST,
            );
          case AuthorIdAlreadyExistsError:
            throw new HttpException(
              makeErrorResponse('E010302'),
              HttpStatus.BAD_REQUEST,
            );
          case InvalidRoleChangeError:
            throw new HttpException(
              makeErrorResponse('E010207'),
              HttpStatus.BAD_REQUEST,
            );
          case EncryptionPasswordNeedError:
            throw new HttpException(
              makeErrorResponse('E010208'),
              HttpStatus.BAD_REQUEST,
            );
          default:
            throw new HttpException(
              makeErrorResponse('E009999'),
              HttpStatus.INTERNAL_SERVER_ERROR,
            );
        }
      }
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.updateUser.name}`,
      );
    }
  }

  /**
   * ライセンスをユーザーに割り当てます
   * @param context
   * @param userId
   * @param newLicenseId
   */
  async allocateLicense(
    context: Context,
    userId: number,
    newLicenseId: number,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.allocateLicense.name
      } | params: { ` +
        `userId: ${userId}, ` +
        `newLicenseId: ${newLicenseId}, };`,
    );

    try {
      const { external_id: externalId, account_id: accountId } =
        await this.usersRepository.findUserById(context, userId);

      await this.licensesRepository.allocateLicense(
        context,
        userId,
        newLicenseId,
        accountId,
      );

      // メール送信処理
      try {
        const { parent_account_id: dealerId } =
          await this.accountsRepository.findAccountById(context, accountId);

        if (dealerId == null) {
          throw new Error(`dealer is null. account_id=${accountId}`);
        }

        const { company_name: dealerName } =
          await this.accountsRepository.findAccountById(context, dealerId);

        const { companyName, adminEmails } = await this.getAccountInformation(
          context,
          accountId,
        );

        const adb2cUser = await this.adB2cService.getUser(context, externalId);
        const { displayName, emailAddress } =
          getUserNameAndMailAddress(adb2cUser);

        if (emailAddress == null) {
          throw new Error(`emailAddress is null. externalId=${externalId}`);
        }

        await this.sendgridService.sendMailWithU108(
          context,
          displayName,
          emailAddress,
          adminEmails,
          companyName,
          dealerName,
        );
      } catch (e) {
        this.logger.error(`[${context.getTrackingId()}] error=${e}`);
        // メール送信に関する例外はログだけ出して握りつぶす
      }
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case LicenseExpiredError:
            throw new HttpException(
              makeErrorResponse('E010805'),
              HttpStatus.BAD_REQUEST,
            );
          case LicenseUnavailableError:
            throw new HttpException(
              makeErrorResponse('E010806'),
              HttpStatus.BAD_REQUEST,
            );
          default:
            throw new HttpException(
              makeErrorResponse('E009999'),
              HttpStatus.INTERNAL_SERVER_ERROR,
            );
        }
      }
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.allocateLicense.name}`,
      );
    }
  }

  /**
   * ユーザーに割り当てられているライセンスを解除します
   * @param context
   * @param userId
   */
  async deallocateLicense(context: Context, userId: number): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.deallocateLicense.name
      } | params: { ` + `userId: ${userId}, };`,
    );

    try {
      const accountId = (
        await this.usersRepository.findUserById(context, userId)
      ).account_id;

      await this.licensesRepository.deallocateLicense(
        context,
        userId,
        accountId,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case LicenseAlreadyDeallocatedError:
            throw new HttpException(
              makeErrorResponse('E010807'),
              HttpStatus.BAD_REQUEST,
            );
          default:
            throw new HttpException(
              makeErrorResponse('E009999'),
              HttpStatus.INTERNAL_SERVER_ERROR,
            );
        }
      }
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.deallocateLicense.name}`,
      );
    }
  }

  /**
   * 同意済み利用規約バージョンを更新する
   * @param context
   * @param idToken
   * @param eulaVersion
   * @param privacyNoticeVersion
   * @param dpaVersion
   */
  async updateAcceptedVersion(
    context: Context,
    externalId: string,
    eulaVersion: string,
    privacyNoticeVersion: string,
    dpaVersion?: string,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.updateAcceptedVersion.name
      } | params: { ` +
        `externalId: ${externalId}, ` +
        `eulaVersion: ${eulaVersion}, ` +
        `privacyNoticeVersion: ${privacyNoticeVersion}, ` +
        `dpaVersion: ${dpaVersion}, };`,
    );

    try {
      await this.usersRepository.updateAcceptedTermsVersion(
        context,
        externalId,
        eulaVersion,
        privacyNoticeVersion,
        dpaVersion,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case UserNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010204'),
              HttpStatus.BAD_REQUEST,
            );
          case AccountNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010501'),
              HttpStatus.BAD_REQUEST,
            );
          case UpdateTermsVersionNotSetError:
            throw new HttpException(
              makeErrorResponse('E010001'),
              HttpStatus.BAD_REQUEST,
            );
          default:
            throw new HttpException(
              makeErrorResponse('E009999'),
              HttpStatus.INTERNAL_SERVER_ERROR,
            );
        }
      }
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.updateAcceptedVersion.name}`,
      );
    }
  }
  /**
   * Azure AD B2Cからユーザー名を取得する
   * @param context
   * @param externalId
   */
  async getUserName(context: Context, externalId: string): Promise<string> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getUserName.name
      } | params: { externalId: ${externalId} };`,
    );

    try {
      // extarnalIdの存在チェックを行う
      await this.usersRepository.findUserByExternalId(context, externalId);
      // ADB2Cからユーザー名を取得する
      const adb2cUser = await this.adB2cService.getUser(context, externalId);
      return adb2cUser.displayName;
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case UserNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010204'),
              HttpStatus.BAD_REQUEST,
            );
          default:
            throw new HttpException(
              makeErrorResponse('E009999'),
              HttpStatus.INTERNAL_SERVER_ERROR,
            );
        }
      }
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.getUserName.name}`,
      );
    }
  }

  /**
   * アカウントIDを指定して、アカウント情報と管理者情報を取得する
   * @param context
   * @param accountId 対象アカウントID
   * @returns 企業名/管理者メールアドレス
   */
  private async getAccountInformation(
    context: Context,
    accountId: number,
  ): Promise<{
    companyName: string;
    adminEmails: string[];
  }> {
    // アカウントIDから企業名を取得する
    const { company_name } = await this.accountsRepository.findAccountById(
      context,
      accountId,
    );

    // 管理者一覧を取得
    const admins = await this.usersRepository.findAdminUsers(
      context,
      accountId,
    );
    const adminExternalIDs = admins.map((x) => x.external_id);

    // ADB2Cから管理者IDを元にメールアドレスを取得する
    const usersInfo = await this.adB2cService.getUsers(
      context,
      adminExternalIDs,
    );

    // 生のAzure AD B2Cのユーザー情報からメールアドレスを抽出する
    const adminEmails = usersInfo.map((x) => {
      const { emailAddress } = getUserNameAndMailAddress(x);
      if (emailAddress == null) {
        throw new Error('dealer admin email-address is not found');
      }
      return emailAddress;
    });

    return {
      companyName: company_name,
      adminEmails: adminEmails,
    };
  }
  /**
   * アカウントのプライマリ管理者を取得する
   * @param context
   * @param accountId
   * @returns primary admin user
   */
  private async getPrimaryAdminUser(
    context: Context,
    accountId: number,
  ): Promise<EntityUser> {
    const accountInfo = await this.accountsRepository.findAccountById(
      context,
      accountId,
    );
    if (!accountInfo || !accountInfo.primary_admin_user_id) {
      throw new Error(`account or primary admin not found. id=${accountId}`);
    }

    const primaryAdmin = await this.usersRepository.findUserById(
      context,
      accountInfo.primary_admin_user_id,
    );

    return primaryAdmin;
  }
}
