import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
import {
  AdB2cService,
  ConflictError,
  isConflictError,
} from '../../gateways/adb2c/adb2c.service';
import { Account } from '../../repositories/accounts/entity/account.entity';
import { User } from '../../repositories/users/entity/user.entity';
import {
  TIERS,
  USER_ROLES,
  OPTION_ITEM_VALUE_TYPE,
  MANUAL_RECOVERY_REQUIRED,
  LICENSE_ISSUE_STATUS,
} from '../../constants';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import {
  TypistGroup,
  GetPartnerLicensesResponse,
  PartnerLicenseInfo,
  GetOrderHistoriesResponse,
  LicenseOrder,
  GetDealersResponse,
  Dealer,
  GetMyAccountResponse,
  GetTypistGroupResponse,
  GetWorktypesResponse,
  GetOptionItemsResponse,
  GetPartnersResponse,
  PostWorktypeOptionItem,
  Author,
  Partner,
  GetCompanyNameResponse,
} from './types/types';
import {
  DateWithZeroTime,
  ExpirationThresholdDate,
} from '../licenses/types/types';
import { GetLicenseSummaryResponse, Typist } from './types/types';
import { UserNotFoundError } from '../../repositories/users/errors/types';
import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service';
import { makePassword } from '../../common/password';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
import {
  AccountNotFoundError,
  AdminUserNotFoundError,
  DealerAccountNotFoundError,
} from '../../repositories/accounts/errors/types';
import { Context } from '../../common/log';
import {
  LicensesShortageError,
  AlreadyIssuedError,
  OrderNotFoundError,
  AlreadyLicenseStatusChangedError,
  AlreadyLicenseAllocatedError,
  CancellationPeriodExpiredError,
} from '../../repositories/licenses/errors/types';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import {
  TypistGroupNameAlreadyExistError,
  TypistGroupNotExistError,
  TypistIdInvalidError,
} from '../../repositories/user_groups/errors/types';
import { WorktypesRepositoryService } from '../../repositories/worktypes/worktypes.repository.service';
import {
  WorktypeIdAlreadyExistsError,
  WorktypeIdInUseError,
  WorktypeIdMaxCountError,
  WorktypeIdNotFoundError,
} from '../../repositories/worktypes/errors/types';
import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils';

@Injectable()
export class AccountsService {
  constructor(
    private readonly accountRepository: AccountsRepositoryService,
    private readonly licensesRepository: LicensesRepositoryService,
    private readonly usersRepository: UsersRepositoryService,
    private readonly userGroupsRepository: UserGroupsRepositoryService,
    private readonly worktypesRepository: WorktypesRepositoryService,
    private readonly adB2cService: AdB2cService,
    private readonly sendgridService: SendGridService,
    private readonly blobStorageService: BlobstorageService,
  ) {}
  private readonly logger = new Logger(AccountsService.name);
  /**
   * 第五階層用のライセンス情報を取得する
   * @param accountId
   * @returns LicenseSummary
   */
  async getLicenseSummary(
    context: Context,
    accountId: number,
  ): Promise<GetLicenseSummaryResponse> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getLicenseSummary.name
      } | params: { ` + `accountId: ${accountId}, };`,
    );

    try {
      const currentDate = new DateWithZeroTime();
      const expiringSoonDate = new ExpirationThresholdDate(
        currentDate.getTime(),
      );

      const { licenseSummary, isStorageAvailable } =
        await this.accountRepository.getLicenseSummaryInfo(
          context,
          accountId,
          currentDate,
          expiringSoonDate,
        );

      const {
        allocatableLicenseWithMargin,
        expiringSoonLicense,
        totalLicense,
        allocatedLicense,
        reusableLicense,
        freeLicense,
        issueRequesting,
        numberOfRequesting,
      } = licenseSummary;

      let shortage = allocatableLicenseWithMargin - expiringSoonLicense;
      shortage = shortage >= 0 ? 0 : Math.abs(shortage);

      const licenseSummaryResponse: GetLicenseSummaryResponse = {
        totalLicense,
        allocatedLicense,
        reusableLicense,
        freeLicense,
        expiringWithin14daysLicense: expiringSoonLicense,
        issueRequesting,
        numberOfRequesting,
        storageSize: 0, // XXX PBI1201対象外
        usedSize: 0, // XXX PBI1201対象外
        shortage,
        isStorageAvailable,
      };
      return licenseSummaryResponse;
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      this.logger.error(
        `[${context.getTrackingId()}] get licenseSummary failed`,
      );
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.getLicenseSummary.name}`,
      );
    }
  }
  /**
   * アカウント情報をDBに作成する
   * @param companyName
   * @param country
   * @param [dealerAccountId]
   * @returns account
   */
  async createAccount(
    context: Context,
    companyName: string,
    country: string,
    dealerAccountId: number | undefined,
    email: string,
    password: string,
    username: string,
    role: string,
    acceptedEulaVersion: string,
    acceptedPrivacyNoticeVersion: string,
    acceptedDpaVersion: string,
  ): Promise<{ accountId: number; userId: number; externalUserId: string }> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.createAccount.name
      } | params: { ` +
        `dealerAccountId: ${dealerAccountId}, ` +
        `role: ${role}, ` +
        `acceptedEulaVersion: ${acceptedEulaVersion}, ` +
        `acceptedPrivacyNoticeVersion: ${acceptedPrivacyNoticeVersion}, ` +
        `acceptedDpaVersion: ${acceptedDpaVersion} };`,
    );
    try {
      let externalUser: { sub: string } | ConflictError;
      try {
        // idpにユーザーを作成
        externalUser = await this.adB2cService.createUser(
          context,
          email,
          password,
          username,
        );
      } 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)) {
        this.logger.error(
          `[${context.getTrackingId()}] email conflict. externalUser: ${externalUser}`,
        );
        throw new HttpException(
          makeErrorResponse('E010301'),
          HttpStatus.BAD_REQUEST,
        );
      }

      let account: Account;
      let user: User;
      try {
        // アカウントと管理者をセットで作成
        const { newAccount, adminUser } =
          await this.accountRepository.createAccount(
            context,
            companyName,
            country,
            dealerAccountId,
            TIERS.TIER5,
            externalUser.sub,
            role,
            acceptedEulaVersion,
            acceptedPrivacyNoticeVersion,
            acceptedDpaVersion,
          );
        account = newAccount;
        user = adminUser;
        this.logger.log(
          `[${context.getTrackingId()}] adminUser.external_id: ${
            user.external_id
          }`,
        );
      } catch (e) {
        this.logger.error(`[${context.getTrackingId()}] error=${e}`);
        this.logger.error(`[${context.getTrackingId()}] create account failed`);
        //リカバリ処理
        // idpのユーザーを削除
        await this.deleteAdB2cUser(externalUser.sub, context);

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

      // 新規作成アカウント用のBlobコンテナを作成
      try {
        await this.blobStorageService.createContainer(
          context,
          account.id,
          country,
        );
      } catch (e) {
        this.logger.error(`[${context.getTrackingId()}] error=${e}`);
        this.logger.error(
          `[${context.getTrackingId()}] create container failed`,
        );
        //リカバリ処理
        // idpのユーザーを削除
        await this.deleteAdB2cUser(externalUser.sub, context);

        // DBのアカウントを削除
        await this.deleteAccount(account.id, user.id, context);

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

      try {
        await this.sendgridService.sendMailWithU102(
          context,
          email,
          account.id,
          user.id,
        );
      } catch (e) {
        this.logger.error(`[${context.getTrackingId()}] error=${e}`);
        this.logger.error(`[${context.getTrackingId()}] send E-mail failed`);
        //リカバリ処理
        // idpのユーザーを削除
        await this.deleteAdB2cUser(externalUser.sub, context);

        // DBのアカウントを削除
        await this.deleteAccount(account.id, user.id, context);

        // Blobコンテナを削除
        await this.deleteBlobContainer(account.id, country, context);

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

      return {
        accountId: account.id,
        userId: user.id,
        externalUserId: user.external_id,
      };
    } catch (e) {
      throw e;
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.createAccount.name}`,
      );
    }
  }

  // AdB2cのユーザーを削除
  // TODO「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
  private async deleteAdB2cUser(
    externalUserId: string,
    context: Context,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.createAccount.name
      } | params: { ` + `externalUserId: ${externalUserId}};`,
    );
    try {
      await this.adB2cService.deleteUser(externalUserId, context);
      this.logger.log(
        `[${context.getTrackingId()}] delete externalUser: ${externalUserId} | params: { ` +
          `externalUserId: ${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.deleteAdB2cUser.name}`,
      );
    }
  }

  // DBのアカウントを削除
  private async deleteAccount(
    accountId: number,
    userId: number,
    context: Context,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.deleteAccount.name
      } | params: { accountId: ${accountId}, userId: ${userId} };`,
    );
    try {
      await this.accountRepository.deleteAccount(context, accountId, userId);
      this.logger.log(
        `[${context.getTrackingId()}] delete account: ${accountId}, user: ${userId}`,
      );
    } catch (error) {
      this.logger.error(`[${context.getTrackingId()}] error=${error}`);
      this.logger.error(
        `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete account: ${accountId}, user: ${userId}`,
      );
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.deleteAccount.name}`,
      );
    }
  }

  // Blobコンテナを削除
  // TODO「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
  private async deleteBlobContainer(
    accountId: number,
    country: string,
    context: Context,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.deleteBlobContainer.name
      } | params: { accountId: ${accountId} };`,
    );
    try {
      await this.blobStorageService.deleteContainer(
        context,
        accountId,
        country,
      );
      this.logger.log(
        `[${context.getTrackingId()}] delete container: ${accountId}, country: ${country}`,
      );
    } catch (error) {
      this.logger.error(
        `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete container: ${accountId}, country: ${country}`,
      );
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.deleteBlobContainer.name}`,
      );
    }
  }

  /**
   * パラメータのユーザIDからアカウント情報を取得する
   * @param externalId
   * @returns GetMyAccountResponse
   */
  async getAccountInfo(
    context: Context,
    externalId: string,
  ): Promise<GetMyAccountResponse> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getAccountInfo.name
      } | params: { ` + `externalId: ${externalId}, };`,
    );
    try {
      const userInfo = await this.usersRepository.findUserByExternalId(
        context,
        externalId,
      );

      const accountInfo = await this.accountRepository.findAccountById(
        context,
        userInfo.account_id,
      );

      let parentInfo: Account | undefined;
      if (accountInfo.parent_account_id) {
        parentInfo = await this.accountRepository.findAccountById(
          context,
          accountInfo.parent_account_id,
        );
      }

      return {
        account: {
          accountId: userInfo.account_id,
          companyName: accountInfo.company_name,
          tier: accountInfo.tier,
          country: accountInfo.country,
          parentAccountId: accountInfo.parent_account_id ?? undefined,
          delegationPermission: accountInfo.delegation_permission,
          primaryAdminUserId: accountInfo.primary_admin_user_id ?? undefined,
          secondryAdminUserId: accountInfo.secondary_admin_user_id ?? undefined,
          parentAccountName: parentInfo ? parentInfo.company_name : undefined,
        },
      };
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      switch (e.constructor) {
        case UserNotFoundError:
          throw new HttpException(
            makeErrorResponse('E010204'),
            HttpStatus.BAD_REQUEST,
          );
        case AccountNotFoundError:
          throw new HttpException(
            makeErrorResponse('E010501'),
            HttpStatus.BAD_REQUEST,
          );
        default:
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
      }
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.getAccountInfo.name}`,
      );
    }
  }

  async getTypistGroups(
    context: Context,
    externalId: string,
  ): Promise<TypistGroup[]> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getTypistGroups.name
      } | params: { externalId: ${externalId} };`,
    );

    // TypistGroup取得
    try {
      const user = await this.usersRepository.findUserByExternalId(
        context,
        externalId,
      );
      const userGroups = await this.userGroupsRepository.getUserGroups(
        context,
        user.account_id,
      );

      return userGroups.map((x) => ({ id: x.id, name: x.name }));
    } 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.getTypistGroups.name}`,
      );
    }
  }
  /**
   * IDを指定してタイピストグループを取得する
   * @param context
   * @param externalId
   * @param typistGroupId
   * @returns typist group
   */
  async getTypistGroup(
    context: Context,
    externalId: string,
    typistGroupId: number,
  ): Promise<GetTypistGroupResponse> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getTypistGroup.name
      } | params: { externalId: ${externalId}, typistGroupId: ${typistGroupId} };`,
    );

    try {
      const { account_id } = await this.usersRepository.findUserByExternalId(
        context,
        externalId,
      );
      const { name, userGroupMembers } =
        await this.userGroupsRepository.getTypistGroup(
          context,
          account_id,
          typistGroupId,
        );
      if (!userGroupMembers) {
        throw new TypistGroupNotExistError(
          `Typist Group is not exist. typistGroupId: ${typistGroupId}`,
        );
      }

      return {
        typistGroupName: name,
        typistIds: userGroupMembers.map((x) => x.user_id),
      };
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case TypistGroupNotExistError:
            throw new HttpException(
              makeErrorResponse('E010908'),
              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.getTypistGroup.name}`,
      );
    }
  }

  /**
   * Gets typists
   * @param externalId
   * @returns typists
   */
  async getTypists(context: Context, externalId: string): Promise<Typist[]> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getTypists.name
      } | params: { externalId: ${externalId} };`,
    );

    // Typist取得
    try {
      const typistUsers = await this.usersRepository.findTypistUsers(
        context,
        externalId,
      );
      const externalIds = typistUsers.map((x) => x.external_id);

      // B2Cからユーザー名を取得する
      const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);

      const typists = typistUsers.map((x) => {
        const user = adb2cUsers.find((adb2c) => adb2c.id === x.external_id);
        if (!user) {
          throw new Error(
            `user not found. externalId: ${x.external_id}, userId: ${x.id}`,
          );
        }
        return {
          id: x.id,
          name: user.displayName,
        };
      });

      return typists;
    } 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.getTypists.name}`,
      );
    }
  }

  /**
   * アカウント内のAuthorを取得する
   * @param context
   * @param externalId
   * @returns authors
   */
  async getAuthors(context: Context, externalId: string): Promise<Author[]> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getAuthors.name
      } | params: { externalId: ${externalId} };`,
    );

    try {
      const { account } = await this.usersRepository.findUserByExternalId(
        context,
        externalId,
      );

      if (!account) {
        throw new AccountNotFoundError(
          `account not found. externalId: ${externalId}`,
        );
      }

      const authorUsers = await this.usersRepository.findAuthorUsers(
        context,
        account.id,
      );

      const authors = authorUsers.map((x) => {
        if (!x.author_id) {
          throw new Error(
            `author_id is Not Found. externalId: ${x.external_id}, userId: ${x.id}`,
          );
        }
        return {
          id: x.id,
          authorId: x.author_id,
        };
      });
      return authors;
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case AccountNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010501'),
              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.getAuthors.name}`,
      );
    }
  }

  /**
   * パートナーを追加する
   * @param companyName パートナーの会社名
   * @param country パートナーの所属する国
   * @param email パートナーの管理者のメールアドレス
   * @param adminName パートナーの管理者の名前
   * @param creatorUserId パートナーを作成する上位階層アカウントのユーザーID
   * @param creatorAccountTier パートナーを作成する上位階層アカウントの階層
   */
  async createPartnerAccount(
    context: Context,
    companyName: string,
    country: string,
    email: string,
    adminName: string,
    creatorUserId: string,
    creatorAccountTier: number,
  ): Promise<{ accountId: number }> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.createPartnerAccount.name
      } | params: { creatorUserId: ${creatorUserId}, creatorAccountTier: ${creatorAccountTier} };`,
    );

    try {
      let creatorAccountId: number;
      let creatorAccountName: string | null;

      try {
        const creatorAccount = await this.usersRepository.findUserByExternalId(
          context,
          creatorUserId,
        );
        creatorAccountId = creatorAccount.account_id;
        // メール送信処理で使用するため、追加パートナーの上位階層アカウントの会社名を取得する
        creatorAccountName = creatorAccount.account?.company_name ?? null;
      } catch (e) {
        this.logger.error(`[${context.getTrackingId()}] error=${e}`);
        if (e instanceof UserNotFoundError) {
          throw new HttpException(
            makeErrorResponse('E010204'),
            HttpStatus.BAD_REQUEST,
          );
        } else {
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
        }
      }

      const ramdomPassword = makePassword();

      let externalUser: { sub: string } | ConflictError;

      try {
        // 管理者ユーザを作成し、AzureADB2C IDを取得する
        externalUser = await this.adB2cService.createUser(
          context,
          email,
          ramdomPassword,
          adminName,
        );
      } catch (e) {
        this.logger.error(`[${context.getTrackingId()}] error=${e}`);
        throw new HttpException(
          makeErrorResponse('E009999'),
          HttpStatus.INTERNAL_SERVER_ERROR,
        );
      }
      // メールアドレスが重複していた場合はエラーを返す
      if (isConflictError(externalUser)) {
        throw new HttpException(
          makeErrorResponse('E010301'),
          HttpStatus.BAD_REQUEST,
        );
      }

      let account: Account;
      let user: User;
      try {
        // アカウントと管理者をセットで作成
        const { newAccount, adminUser } =
          await this.accountRepository.createAccount(
            context,
            companyName,
            country,
            creatorAccountId,
            creatorAccountTier + 1,
            externalUser.sub,
            USER_ROLES.NONE,
            undefined,
            undefined,
          );
        account = newAccount;
        user = adminUser;
      } catch (e) {
        this.logger.error(`[${context.getTrackingId()}] error=${e}`);
        this.logger.error(
          `[${context.getTrackingId()}] create partner account failed`,
        );
        //リカバリ処理
        // idpのユーザーを削除
        await this.deleteAdB2cUser(externalUser.sub, context);

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

      try {
        // 新規作成アカウント用のBlobコンテナを作成
        await this.blobStorageService.createContainer(
          context,
          account.id,
          country,
        );
      } catch (e) {
        this.logger.error(`[${context.getTrackingId()}] error=${e}`);
        this.logger.error(
          `[${context.getTrackingId()}] create partner container failed`,
        );
        //リカバリ処理
        // idpのユーザーを削除
        await this.deleteAdB2cUser(externalUser.sub, context);

        // DBのアカウントとユーザーを削除
        await this.deleteAccount(account.id, user.id, context);

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

      // メール送信処理
      try {
        if (creatorAccountName === null) {
          throw new Error(
            `Account is not found. creatorUserId: ${creatorUserId}`,
          );
        }

        await this.sendgridService.sendMailWithU114(
          context,
          account.id,
          user.id,
          email,
          creatorAccountName,
        );

        return { accountId: account.id };
      } catch (e) {
        this.logger.error(`[${context.getTrackingId()}] error=${e}`);
        this.logger.error(
          `[${context.getTrackingId()}] create partner account send mail failed`,
        );
        //リカバリ処理
        // idpのユーザーを削除
        await this.deleteAdB2cUser(externalUser.sub, context);

        // DBのアカウントを削除
        await this.deleteAccount(account.id, user.id, context);

        // Blobコンテナを削除
        await this.deleteBlobContainer(account.id, country, context);

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

  /**
   * パートナーライセンス情報を取得する
   * @param limit
   * @param offset
   * @param accountId
   * @returns getPartnerLicensesResponse
   */
  async getPartnerLicenses(
    context: Context,
    limit: number,
    offset: number,
    accountId: number,
  ): Promise<GetPartnerLicensesResponse> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getPartnerLicenses.name
      } | params: { limit: ${limit}, offset: ${offset}, accountId: ${accountId} };`,
    );

    try {
      const currentDate = new DateWithZeroTime();
      // 第五階層のshortage算出に使用する日付情報
      // 「有効期限が現在日付からしきい値以内のライセンス数」を取得するため、しきい値となる日付を作成する
      const expiringSoonDate = new ExpirationThresholdDate(
        currentDate.getTime(),
      );

      const getPartnerLicenseResult =
        await this.accountRepository.getPartnerLicense(
          context,
          accountId,
          currentDate,
          expiringSoonDate,
          offset,
          limit,
        );

      // 自アカウントのShortageを算出してreturn用の変数にマージする
      let ownShortage =
        getPartnerLicenseResult.ownPartnerLicenseFromRepository.stockLicense -
        getPartnerLicenseResult.ownPartnerLicenseFromRepository.issuedRequested;
      // 「不足している値」を取得するため、負数の場合は絶対値とし、0以上の場合は0とする
      ownShortage = ownShortage >= 0 ? 0 : Math.abs(ownShortage);

      // return用の型にリポジトリから取得した型をマージし、不足項目（shortage）を設定する
      const ownPartnerLicense: PartnerLicenseInfo = Object.assign(
        {},
        getPartnerLicenseResult.ownPartnerLicenseFromRepository,
        {
          shortage: ownShortage,
        },
      );

      // 各子アカウントのShortageを算出してreturn用の変数にマージする
      const childrenPartnerLicenses: PartnerLicenseInfo[] = [];
      for (const childPartnerLicenseFromRepository of getPartnerLicenseResult.childPartnerLicensesFromRepository) {
        const { allocatableLicenseWithMargin, expiringSoonLicense } =
          childPartnerLicenseFromRepository;
        let childShortage = 0;
        if (childPartnerLicenseFromRepository.tier === TIERS.TIER5) {
          if (
            allocatableLicenseWithMargin === undefined ||
            expiringSoonLicense === undefined
          ) {
            throw new Error(
              `Tier5 account has no allocatableLicenseWithMargin or expiringSoonLicense. accountId: ${accountId}`,
            );
          }
          childShortage = allocatableLicenseWithMargin - expiringSoonLicense;
        } else {
          childShortage =
            childPartnerLicenseFromRepository.stockLicense -
            childPartnerLicenseFromRepository.issuedRequested;
        }
        // 「不足している値」を取得するため、負数の場合は絶対値とし、0以上の場合は0とする
        childShortage = childShortage >= 0 ? 0 : Math.abs(childShortage);

        const childPartnerLicense: PartnerLicenseInfo = Object.assign(
          {},
          childPartnerLicenseFromRepository,
          {
            shortage: childShortage,
          },
        );

        childrenPartnerLicenses.push(childPartnerLicense);
      }

      const getPartnerLicensesResponse: GetPartnerLicensesResponse = {
        total: getPartnerLicenseResult.total,
        ownPartnerLicense: ownPartnerLicense,
        childrenPartnerLicenses: childrenPartnerLicenses,
      };

      return getPartnerLicensesResponse;
    } 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.getPartnerLicenses.name}`,
      );
    }
  }
  /**
   * 注文履歴情報を取得する
   * @param limit
   * @param offset
   * @param accountId
   * @returns getOrderHistoriesResponse
   */
  async getOrderHistories(
    context: Context,
    limit: number,
    offset: number,
    accountId: number,
  ): Promise<GetOrderHistoriesResponse> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getOrderHistories.name
      } | params: { limit: ${limit}, offset: ${offset}, accountId: ${accountId} };`,
    );

    try {
      const licenseHistoryInfo =
        await this.licensesRepository.getLicenseOrderHistoryInfo(
          context,
          accountId,
          offset,
          limit,
        );

      // 戻り値用に配列の詰めなおしを行う
      const orderHistories: LicenseOrder[] = [];
      for (const licenseOrder of licenseHistoryInfo.licenseOrders) {
        const returnLicenseOrder: LicenseOrder = {
          issueDate:
            licenseOrder.issued_at !== null
              ? new Date(licenseOrder.issued_at).toISOString()
              : undefined,
          numberOfOrder: licenseOrder.quantity,
          orderDate: new Date(licenseOrder.ordered_at).toISOString(),
          poNumber: licenseOrder.po_number,
          status: licenseOrder.status,
        };
        orderHistories.push(returnLicenseOrder);
      }
      const getOrderHistoriesResponse: GetOrderHistoriesResponse = {
        total: licenseHistoryInfo.total,
        orderHistories: orderHistories,
      };
      return getOrderHistoriesResponse;
    } 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.getOrderHistories.name}`,
      );
    }
  }

  /**
   * 対象の注文を発行する
   * @param context
   * @param orderedAccountId
   * @param userId
   * @param tier
   * @param poNumber
   */
  async issueLicense(
    context: Context,
    orderedAccountId: number,
    userId: string,
    tier: number,
    poNumber: string,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.issueLicense.name
      } | params: { ` +
        `orderedAccountId: ${orderedAccountId}, ` +
        `userId: ${userId}, ` +
        `tier: ${tier}, ` +
        `poNumber: ${poNumber} };`,
    );
    try {
      // アクセストークンからユーザーIDを取得する
      const myAccountId = (
        await this.usersRepository.findUserByExternalId(context, userId)
      ).account_id;
      const { issuedOrderId } = await this.licensesRepository.issueLicense(
        context,
        orderedAccountId,
        myAccountId,
        tier,
        poNumber,
      );

      try {
        // 発行済みの注文をID指定して取得する
        const orderLicense = await this.licensesRepository.getLicenseOrder(
          context,
          orderedAccountId,
          poNumber,
          issuedOrderId,
        );

        if (orderLicense == null) {
          throw new Error(
            `issue target order not found. fromAccountId: ${orderedAccountId}, poNumber:${poNumber}`,
          );
        }
        // 未発行の注文の場合、エラー
        if (orderLicense.status !== LICENSE_ISSUE_STATUS.ISSUED) {
          throw new AlreadyIssuedError(
            `An order for PONumber:${poNumber} has not been issued.`,
          );
        }

        // 注文したアカウントと自分のアカウントの情報を取得
        const customer = await this.getAccountInformation(
          context,
          orderedAccountId,
        );
        const dealer = await this.getAccountInformation(context, myAccountId);

        await this.sendgridService.sendMailWithU107(
          context,
          customer.adminEmails,
          customer.companyName,
          orderLicense.quantity,
          poNumber,
          dealer.adminEmails,
          dealer.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 OrderNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010801'),
              HttpStatus.BAD_REQUEST,
            );
          case AlreadyIssuedError:
            throw new HttpException(
              makeErrorResponse('E010803'),
              HttpStatus.BAD_REQUEST,
            );
          case LicensesShortageError:
            throw new HttpException(
              makeErrorResponse('E010804'),
              HttpStatus.BAD_REQUEST,
            );
          default:
            throw new HttpException(
              makeErrorResponse('E009999'),
              HttpStatus.INTERNAL_SERVER_ERROR,
            );
        }
      }
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.issueLicense.name}`,
      );
    }
  }

  // dealersのアカウント情報を取得する
  async getDealers(context: Context): Promise<GetDealersResponse> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${this.getDealers.name}`,
    );

    try {
      const dealerAccounts = await this.accountRepository.findDealerAccounts(
        context,
      );

      const dealers: GetDealersResponse = {
        dealers: dealerAccounts.map((dealerAccount): Dealer => {
          return {
            id: dealerAccount.id,
            name: dealerAccount.company_name,
            country: dealerAccount.country,
          };
        }),
      };

      return dealers;
    } 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.getDealers.name}`,
      );
    }
  }
  /**
   * タイピストグループを作成する
   * @param context
   * @param externalId
   * @param typistGroupName
   * @param typistIds
   * @returns createTypistGroupResponse
   **/
  async createTypistGroup(
    context: Context,
    externalId: string,
    typistGroupName: string,
    typistIds: number[],
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.createTypistGroup.name
      } | params: { ` +
        `externalId: ${externalId}, ` +
        `typistGroupName: ${typistGroupName}, ` +
        `typistIds: ${typistIds} };`,
    );
    try {
      // 外部IDをもとにユーザー情報を取得する
      const { account_id } = await this.usersRepository.findUserByExternalId(
        context,
        externalId,
      );
      // API実行ユーザーのアカウントIDでタイピストグループを作成し、タイピストグループとtypistIdsのユーザーを紐付ける
      await this.userGroupsRepository.createTypistGroup(
        context,
        typistGroupName,
        typistIds,
        account_id,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case TypistIdInvalidError:
            throw new HttpException(
              makeErrorResponse('E010204'),
              HttpStatus.BAD_REQUEST,
            );
          // 同名のタイピストグループが存在する場合は400エラーを返す
          case TypistGroupNameAlreadyExistError:
            throw new HttpException(
              makeErrorResponse('E010909'),
              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.createTypistGroup.name}`,
      );
    }
  }

  /**
   * タイピストグループを更新する
   * @param context
   * @param externalId
   * @param typistGroupId
   * @param typistGroupName
   * @param typistIds
   * @returns typist group
   */
  async updateTypistGroup(
    context: Context,
    externalId: string,
    typistGroupId: number,
    typistGroupName: string,
    typistIds: number[],
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.updateTypistGroup.name
      } | params: { ` +
        `externalId: ${externalId}, ` +
        `typistGroupId: ${typistGroupId}, ` +
        `typistGroupName: ${typistGroupName}, ` +
        `typistIds: ${typistIds} };`,
    );
    try {
      // 外部IDをもとにユーザー情報を取得する
      const { account_id } = await this.usersRepository.findUserByExternalId(
        context,
        externalId,
      );

      // タイピストグループと所属するタイピストを更新する
      await this.userGroupsRepository.updateTypistGroup(
        context,
        account_id,
        typistGroupId,
        typistGroupName,
        typistIds,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          // タイピストIDが存在しない場合は400エラーを返す
          case TypistIdInvalidError:
            throw new HttpException(
              makeErrorResponse('E010204'),
              HttpStatus.BAD_REQUEST,
            );
          // タイピストグループIDが存在しない場合は400エラーを返す
          case TypistGroupNotExistError:
            throw new HttpException(
              makeErrorResponse('E010908'),
              HttpStatus.BAD_REQUEST,
            );
          // 同名のタイピストグループが存在する場合は400エラーを返す
          case TypistGroupNameAlreadyExistError:
            throw new HttpException(
              makeErrorResponse('E010909'),
              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.updateTypistGroup.name}`,
      );
    }
  }

  /**
   * ライセンス発行をキャンセルする
   * @param context
   * @param extarnalId
   * @param poNumber
   * @param orderedAccountId
   */
  async cancelIssue(
    context: Context,
    extarnalId: string,
    poNumber: string,
    orderedAccountId: number,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.cancelIssue.name
      } | params: { ` +
        `extarnalId: ${extarnalId}, ` +
        `poNumber: ${poNumber}, ` +
        `orderedAccountId: ${orderedAccountId}, };`,
    );
    let myAccountId: number;

    try {
      // ユーザIDからアカウントIDを取得する
      myAccountId = (
        await this.usersRepository.findUserByExternalId(context, extarnalId)
      ).account_id;
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      switch (e.constructor) {
        default:
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
      }
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.cancelIssue.name}`,
      );
    }

    // 注文元アカウントIDの親世代を取得
    const parentAccountIds = await this.accountRepository.getHierarchyParents(
      context,
      orderedAccountId,
    );
    // 自身が存在しない場合、エラー
    if (!parentAccountIds.includes(myAccountId)) {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.cancelIssue.name}`,
      );
      throw new HttpException(
        makeErrorResponse('E000108'),
        HttpStatus.UNAUTHORIZED,
      );
    }

    try {
      // 発行キャンセル処理
      const { canceledIssueLicenseOrderId } =
        await this.accountRepository.cancelIssue(
          context,
          orderedAccountId,
          poNumber,
        );
      try {
        // 発行キャンセルされ、発行済状態から注文中状態に戻った注文を取得する
        const order = await this.licensesRepository.getLicenseOrder(
          context,
          orderedAccountId,
          poNumber,
          canceledIssueLicenseOrderId,
        );
        if (order == null) {
          throw new Error('order not found.');
        }
        const { quantity, from_account_id, to_account_id } = order;
        const customer = await this.getAccountInformation(
          context,
          from_account_id,
        );
        const dealer = await this.getAccountInformation(context, to_account_id);
        await this.sendgridService.sendMailWithU109(
          context,
          dealer.adminEmails,
          dealer.companyName,
          quantity,
          poNumber,
          customer.adminEmails,
          customer.companyName,
        );
      } catch (e) {
        this.logger.error(`[${context.getTrackingId()}] error=${e}`);
        // メール送信の例外はログだけ出して握りつぶす
      }
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      switch (e.constructor) {
        case AlreadyLicenseStatusChangedError:
          throw new HttpException(
            makeErrorResponse('E010809'),
            HttpStatus.BAD_REQUEST,
          );
        case CancellationPeriodExpiredError:
          throw new HttpException(
            makeErrorResponse('E010810'),
            HttpStatus.BAD_REQUEST,
          );
        case AlreadyLicenseAllocatedError:
          throw new HttpException(
            makeErrorResponse('E010811'),
            HttpStatus.BAD_REQUEST,
          );
        default:
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
      }
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.cancelIssue.name}`,
      );
    }
  }

  /**
   * ワークタイプ一覧を取得します
   * @param context
   * @param externalId
   * @returns worktypes
   */
  async getWorktypes(
    context: Context,
    externalId: string,
  ): Promise<GetWorktypesResponse> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getWorktypes.name
      } | params: { externalId: ${externalId} };`,
    );
    try {
      // 外部IDをもとにユーザー情報を取得する
      const { account_id: accountId } =
        await this.usersRepository.findUserByExternalId(context, externalId);

      // ワークタイプ一覧とActiveWorktypeIDを取得する
      const { worktypes, active_worktype_id } =
        await this.worktypesRepository.getWorktypes(context, accountId);

      return {
        worktypes: worktypes.map((x) => ({
          id: x.id,
          worktypeId: x.custom_worktype_id,
          description: x.description ?? undefined,
        })),
        active: active_worktype_id,
      };
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case AccountNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010501'),
              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.getWorktypes.name}`,
      );
    }
  }
  /**
   * ワークタイプを作成します
   * @param context
   * @param externalId
   * @param worktypeId
   * @param [description]
   * @returns worktype
   */
  async createWorktype(
    context: Context,
    externalId: string,
    worktypeId: string,
    description?: string,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.createWorktype.name
      } | params: { externalId: ${externalId}, worktypeId: ${worktypeId}, description: ${description} };`,
    );

    try {
      // 外部IDをもとにユーザー情報を取得する
      const { account_id: accountId } =
        await this.usersRepository.findUserByExternalId(context, externalId);

      await this.worktypesRepository.createWorktype(
        context,
        accountId,
        worktypeId,
        description,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          // WorktypeIDが既に存在する場合は400エラーを返す
          case WorktypeIdAlreadyExistsError:
            throw new HttpException(
              makeErrorResponse('E011001'),
              HttpStatus.BAD_REQUEST,
            );
          // WorktypeIDが登録上限以上の場合は400エラーを返す
          case WorktypeIdMaxCountError:
            throw new HttpException(
              makeErrorResponse('E011002'),
              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.createWorktype.name}`,
      );
    }
  }

  /**
   * ワークタイプを更新します
   * @param context
   * @param externalId
   * @param id ワークタイプの内部ID
   * @param worktypeId ユーザーが設定するワークタイプ名
   * @param [description]
   * @returns worktype
   */
  async updateWorktype(
    context: Context,
    externalId: string,
    id: number,
    worktypeId: string,
    description?: string,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.updateWorktype.name
      } | params: { ` +
        `externalId: ${externalId}, ` +
        `id: ${id}, ` +
        `worktypeId: ${worktypeId}, ` +
        `description: ${description} };`,
    );

    try {
      // 外部IDをもとにユーザー情報を取得する
      const { account_id: accountId } =
        await this.usersRepository.findUserByExternalId(context, externalId);

      // ワークタイプを更新する
      await this.worktypesRepository.updateWorktype(
        context,
        accountId,
        id,
        worktypeId,
        description,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          // ユーザーが設定したWorktypeIDが既存WorktypeのWorktypeIDと重複する場合は400エラーを返す
          case WorktypeIdAlreadyExistsError:
            throw new HttpException(
              makeErrorResponse('E011001'),
              HttpStatus.BAD_REQUEST,
            );
          // 内部IDで指定されたWorktypeが存在しない場合は400エラーを返す
          case WorktypeIdNotFoundError:
            throw new HttpException(
              makeErrorResponse('E011003'),
              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.updateWorktype.name}`,
      );
    }
  }

  /**
   * ワークタイプを削除します
   * @param context
   * @param externalId
   * @param id
   * @returns worktype
   */
  async deleteWorktype(
    context: Context,
    externalId: string,
    id: number,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.deleteWorktype.name
      } | params: { ` +
        `externalId: ${externalId}, ` +
        `id: ${id} };`,
    );

    try {
      // 外部IDをもとにユーザー情報を取得する
      const { account, account_id: accountId } =
        await this.usersRepository.findUserByExternalId(context, externalId);

      if (!account) {
        throw new AccountNotFoundError(
          `account not found. externalId: ${externalId}`,
        );
      }

      // ワークタイプを削除する
      await this.worktypesRepository.deleteWorktype(context, accountId, id);
    } 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,
            );
          // 内部IDで指定されたWorktypeが存在しない場合は400エラーを返す
          case WorktypeIdNotFoundError:
            throw new HttpException(
              makeErrorResponse('E011003'),
              HttpStatus.BAD_REQUEST,
            );
          // 内部IDで指定されたWorktypeがWorkflowで使用中の場合は400エラーを返す
          case WorktypeIdInUseError:
            throw new HttpException(
              makeErrorResponse('E011004'),
              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.deleteWorktype.name}`,
      );
    }
  }

  /**
   * ワークタイプに紐づいたオプションアイテム一覧を取得します
   * @param context
   * @param externalId
   * @param id Worktypeの内部ID
   * @returns option items
   */
  async getOptionItems(
    context: Context,
    externalId: string,
    id: number,
  ): Promise<GetOptionItemsResponse> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getOptionItems.name
      } | params: { ` +
        `externalId: ${externalId}, ` +
        `id: ${id} };`,
    );
    try {
      // 外部IDをもとにユーザー情報を取得する
      const { account_id: accountId } =
        await this.usersRepository.findUserByExternalId(context, externalId);

      // オプションアイテム一覧を取得する
      const optionItems = await this.worktypesRepository.getOptionItems(
        context,
        accountId,
        id,
      );

      return {
        optionItems: optionItems.map((x) => ({
          id: x.id,
          itemLabel: x.item_label,
          defaultValueType: x.default_value_type,
          initialValue: x.initial_value,
        })),
      };
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          // 内部IDで指定されたWorktypeが存在しない場合は400エラーを返す
          case WorktypeIdNotFoundError:
            throw new HttpException(
              makeErrorResponse('E011003'),
              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.getOptionItems.name}`,
      );
    }
  }

  /**
   * オプションアイテムを更新します
   * @param context
   * @param externalId
   * @param id ワークタイプの内部ID
   * @param optionItems
   * @returns option items
   */
  async updateOptionItems(
    context: Context,
    externalId: string,
    id: number,
    optionItems: PostWorktypeOptionItem[],
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.updateOptionItems.name
      } | params: { ` +
        `externalId: ${externalId}, ` +
        `id: ${id}, ` +
        `optionItems: ${JSON.stringify(optionItems)} };`,
    );
    try {
      // 外部IDをもとにユーザー情報を取得する
      const { account_id: accountId } =
        await this.usersRepository.findUserByExternalId(context, externalId);

      // オプションアイテムを更新する
      await this.worktypesRepository.updateOptionItems(
        context,
        accountId,
        id,
        // initialValueはdefaultValueTypeがDEFAULTの場合以外は空文字を設定する
        optionItems.map((item) => ({
          itemLabel: item.itemLabel,
          defaultValueType: item.defaultValueType,
          initialValue:
            item.defaultValueType === OPTION_ITEM_VALUE_TYPE.DEFAULT
              ? item.initialValue
              : '',
        })),
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          // 内部IDで指定されたWorktypeが存在しない場合は400エラーを返す
          case WorktypeIdNotFoundError:
            throw new HttpException(
              makeErrorResponse('E011003'),
              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.updateOptionItems.name}`,
      );
    }
  }

  /**
   * ActiveWorktypeの更新
   * @param context
   * @param externalId
   * @param id ActiveWorktypeの内部ID
   * @returns active worktype
   */
  async updateActiveWorktype(
    context: Context,
    externalId: string,
    id: number | undefined,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.updateActiveWorktype.name
      } | params: { ` +
        `externalId: ${externalId}, ` +
        `id: ${id} };`,
    );
    try {
      // 外部IDをもとにユーザー情報を取得する
      const { account_id: accountId } =
        await this.usersRepository.findUserByExternalId(context, externalId);

      // ActiveWorktypeを更新する
      await this.accountRepository.updateActiveWorktypeId(
        context,
        accountId,
        id,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          // 内部IDで指定されたWorktypeが存在しない場合は400エラーを返す
          case WorktypeIdNotFoundError:
            throw new HttpException(
              makeErrorResponse('E011003'),
              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.updateActiveWorktype.name}`,
      );
    }
  }

  /**
   * パートナー一覧を取得します
   * @param context
   * @param externalId
   * @param limit
   * @param offset
   * @returns GetPartnersResponse
   */
  async getPartners(
    context: Context,
    externalId: string,
    limit: number,
    offset: number,
  ): Promise<GetPartnersResponse> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getPartners.name
      } | params: { ` +
        `externalId: ${externalId}, ` +
        `limit: ${limit}, ` +
        `offset: ${offset}, };`,
    );

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

      const partnersRecords = await this.accountRepository.getPartners(
        context,
        accountId,
        limit,
        offset,
      );

      // DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
      let externalIds = partnersRecords.partnersInfo.map(
        (x) => x.primaryAccountExternalId,
      );
      externalIds = externalIds.filter((item) => item !== undefined);
      const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);

      // DBから取得した情報とADB2Cから取得した情報をマージ
      const partners = partnersRecords.partnersInfo.map((db): Partner => {
        const adb2cUser = adb2cUsers.find(
          (adb2c) => db.primaryAccountExternalId === adb2c.id,
        );
        if (!adb2cUser) {
          throw new Error(
            `adb2c user not found. externalId: ${db.primaryAccountExternalId}`,
          );
        }

        const { displayName: primaryAdmin, emailAddress: mail } =
          getUserNameAndMailAddress(adb2cUser);
        if (!mail) {
          throw new Error(
            `adb2c user mail not found. externalId: ${db.primaryAccountExternalId}`,
          );
        }

        return {
          name: db.name,
          tier: db.tier,
          accountId: db.accountId,
          country: db.country,
          primaryAdmin: primaryAdmin,
          email: mail,
          dealerManagement: db.dealerManagement,
        };
      });

      return {
        total: partnersRecords.total,
        partners: partners,
      };
    } 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.getPartners.name}`,
      );
    }
  }

  /**
   * アカウント情報を設定する
   * @param context
   * @param externalId
   * @param tier
   * @param delegationPermission
   * @param primaryAdminUserId
   * @param parentAccountId
   * @param secondryAdminUserId
   * @returns UpdateAccountInfoResponse
   */
  async updateAccountInfo(
    context: Context,
    externalId: string,
    tier: number,
    delegationPermission: boolean,
    primaryAdminUserId: number,
    parentAccountId?: number,
    secondryAdminUserId?: number,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.updateAccountInfo.name
      } | params: { ` +
        `externalId: ${externalId}, ` +
        `tier: ${tier}, ` +
        `delegationPermission: ${delegationPermission}, ` +
        `primaryAdminUserId: ${primaryAdminUserId}, ` +
        `parentAccountId: ${parentAccountId}, ` +
        `secondryAdminUserId: ${secondryAdminUserId}, };`,
    );

    try {
      const { account_id: accountId, account } =
        await this.usersRepository.findUserByExternalId(context, externalId);
      await this.accountRepository.updateAccountInfo(
        context,
        accountId,
        tier,
        delegationPermission,
        primaryAdminUserId,
        parentAccountId,
        secondryAdminUserId,
      );

      // メール送信処理
      try {
        if (account === null) {
          throw new Error(`account not found. accountId: ${accountId}`);
        }

        let dealerName: string | null = null;
        if (parentAccountId !== undefined) {
          const dealer = await this.accountRepository.findAccountById(
            context,
            parentAccountId,
          );
          dealerName = dealer.company_name;
        }

        const { external_id: externalId } =
          await this.usersRepository.findUserById(context, primaryAdminUserId);

        const primaryAdmin = await this.adB2cService.getUser(
          context,
          externalId,
        );

        const {
          displayName: primaryAdminName,
          emailAddress: primaryAdminEmail,
        } = getUserNameAndMailAddress(primaryAdmin);

        if (!primaryAdminEmail) {
          throw new Error(
            `adb2c user mail not found. externalId: ${externalId}`,
          );
        }

        await this.sendgridService.sendMailWithU112(
          context,
          primaryAdminName,
          primaryAdminEmail,
          account.company_name,
          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 DealerAccountNotFoundError:
          case AdminUserNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010502'),
              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.updateAccountInfo.name}`,
      );
    }
  }

  /**
   * アカウントと紐づくデータを削除する
   * @param context
   * @param externalId
   * @param accountId // 削除対象のアカウントID
   */
  async deleteAccountAndData(
    context: Context,
    externalId: string,
    accountId: number,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.deleteAccountAndData.name
      } | params: { ` +
        `externalId: ${externalId}, ` +
        `accountId: ${accountId}, };`,
    );
    let country: string;
    let dbUsers: User[];

    // メール送信に必要な情報
    let companyName: string | null = null;
    let primaryAdminName: string | null = null;
    let primaryAdminEmail: string | null = null;
    try {
      // パラメータとトークンから取得したアカウントIDの突き合わせ
      const { account_id: myAccountId } =
        await this.usersRepository.findUserByExternalId(context, externalId);
      if (myAccountId !== accountId) {
        throw new HttpException(
          makeErrorResponse('E000108'),
          HttpStatus.UNAUTHORIZED,
        );
      }

      // アカウント削除前に必要な情報を退避する
      const targetAccount = await this.accountRepository.findAccountById(
        context,
        accountId,
      );

      // メール送信に必要な情報を取得する
      try {
        companyName = targetAccount.company_name;

        if (!targetAccount.primary_admin_user_id) {
          throw new Error(
            `primary_admin_user_id not found. accountId: ${accountId}`,
          );
        }

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

        const adb2cAdmin = await this.adB2cService.getUser(
          context,
          primaryAdmin.external_id,
        );
        const { displayName, emailAddress } =
          getUserNameAndMailAddress(adb2cAdmin);
        primaryAdminName = displayName;
        primaryAdminEmail = emailAddress ?? null;
      } catch (e) {
        // メール送信に関する例外はログだけ出して握りつぶす
        this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      }

      // 削除対象アカウントを削除する
      dbUsers = await this.accountRepository.deleteAccountAndInsertArchives(
        context,
        accountId,
      );
      this.logger.log(
        `[${context.getTrackingId()}] delete account: ${accountId}`,
      );
      country = targetAccount.country;
    } catch (e) {
      // アカウントの削除に失敗した場合はエラーを返す
      this.logger.log(`[${context.getTrackingId()}] ${e}`);
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.deleteAccountAndData.name}`,
      );
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    try {
      // 削除対象アカウント内のADB2Cユーザーをすべて削除する
      await this.adB2cService.deleteUsers(
        dbUsers.map((x) => x.external_id),
        context,
      );
      this.logger.log(
        `[${context.getTrackingId()}] delete ADB2C users: ${accountId}, users_id: ${dbUsers.map(
          (x) => x.external_id,
        )}`,
      );
    } catch (e) {
      // ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行
      this.logger.log(`[${context.getTrackingId()}] ${e}`);
      this.logger.log(
        `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete ADB2C users: ${accountId}, users_id: ${dbUsers.map(
          (x) => x.external_id,
        )}`,
      );
    }

    try {
      // blobstorageコンテナを削除する
      await this.deleteBlobContainer(accountId, country, context);
      this.logger.log(
        `[${context.getTrackingId()}] delete blob container: ${accountId}-${country}`,
      );
    } catch (e) {
      // blobstorageコンテナを削除で失敗した場合は、MANUAL_RECOVERY_REQUIRED出して正常終了
      this.logger.log(`[${context.getTrackingId()}] ${e}`);
      this.logger.log(
        `${MANUAL_RECOVERY_REQUIRED}[${context.getTrackingId()}] Failed to delete blob container: ${accountId}, country: ${country}`,
      );
    }

    // メール送信処理
    try {
      if (companyName === null) {
        throw new Error('companyName is null');
      }
      if (primaryAdminName === null) {
        throw new Error('primaryAdminName is null');
      }
      if (primaryAdminEmail === null) {
        throw new Error('primaryAdminEmail is null');
      }

      await this.sendgridService.sendMailWithU111(
        context,
        primaryAdminName,
        primaryAdminEmail,
        companyName,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      // メール送信に関する例外はログだけ出して握りつぶす
    }

    this.logger.log(
      `[OUT] [${context.getTrackingId()}] ${this.deleteAccountAndData.name}`,
    );
  }

  /**
   * IDトークンのsubからアカウントの階層情報を取得します
   * @param context
   * @param externalId
   * @returns account info minimal access
   */
  async getAccountInfoMinimalAccess(
    context: Context,
    externalId: string,
  ): Promise<number> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getAccountInfoMinimalAccess.name
      } | params: { externalId: ${externalId} };`,
    );

    try {
      const { account } = await this.usersRepository.findUserByExternalId(
        context,
        externalId,
      );
      if (!account) {
        throw new AccountNotFoundError(
          `Account not found. externalId: ${externalId}`,
        );
      }

      return account.tier;
    } 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,
            );
          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.getAccountInfoMinimalAccess.name
        }`,
      );
    }
  }
  /**
   * 自アカウントの会社名を取得する
   * @param accountId
   * @returns CompanyName
   */
  async getCompanyName(
    context: Context,
    accountId: number,
  ): Promise<GetCompanyNameResponse> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getCompanyName.name
      } | params: { accountId: ${accountId}, };`,
    );

    try {
      const { company_name } = await this.accountRepository.findAccountById(
        context,
        accountId,
      );

      return { companyName: company_name };
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case AccountNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010501'),
              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.getCompanyName.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.accountRepository.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(`admin email-address is not found. id=${x.id}`);
      }
      return emailAddress;
    });

    return {
      companyName: company_name,
      adminEmails: adminEmails,
    };
  }
}
