import { Injectable } from '@nestjs/common';
import {
  Between,
  DataSource,
  In,
  IsNull,
  MoreThan,
  MoreThanOrEqual,
  Not,
  UpdateResult,
  EntityManager,
} from 'typeorm';
import { User, UserArchive } from '../users/entity/user.entity';
import { Account } from './entity/account.entity';
import {
  CardLicense,
  License,
  LicenseAllocationHistory,
  LicenseAllocationHistoryArchive,
  LicenseArchive,
  LicenseOrder,
} from '../licenses/entity/license.entity';
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
import {
  getDirection,
  getTaskListSortableAttribute,
} from '../../common/types/sort/util';
import {
  LICENSE_ALLOCATED_STATUS,
  LICENSE_EXPIRATION_THRESHOLD_DAYS,
  LICENSE_ISSUE_STATUS,
  TIERS,
} from '../../constants';
import {
  AccountNotFoundError,
  AdminUserNotFoundError,
  DealerAccountNotFoundError,
} from './errors/types';
import {
  AlreadyLicenseAllocatedError,
  AlreadyLicenseStatusChangedError,
  CancellationPeriodExpiredError,
} from '../licenses/errors/types';
import { DateWithZeroTime } from '../../features/licenses/types/types';
import { Worktype } from '../worktypes/entity/worktype.entity';
import { WorktypeIdNotFoundError } from '../worktypes/errors/types';
import { OptionItem } from '../worktypes/entity/option_item.entity';
import { Task } from '../tasks/entity/task.entity';
import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity';
import { AudioFile } from '../audio_files/entity/audio_file.entity';
import { AudioOptionItem } from '../audio_option_items/entity/audio_option_item.entity';
import { UserGroup } from '../user_groups/entity/user_group.entity';
import { UserGroupMember } from '../user_groups/entity/user_group_member.entity';
import { TemplateFile } from '../template_files/entity/template_file.entity';
import {
  insertEntity,
  insertEntities,
  updateEntity,
  deleteEntity,
} from '../../common/repository';
import { Context } from '../../common/log';
import {
  LicenseSummaryInfo,
  PartnerInfoFromDb,
  PartnerLicenseInfoForRepository,
} from '../../features/accounts/types/types';

@Injectable()
export class AccountsRepositoryService {
  // クエリログにコメントを出力するかどうか
  private readonly isCommentOut = process.env.STAGE !== 'local';
  constructor(private dataSource: DataSource) {}
  /**
   * 管理ユーザー無しでアカウントを作成する
   * @param companyName
   * @param country
   * @param dealerAccountId
   * @param tier
   * @returns create
   */
  async create(
    context: Context,
    companyName: string,
    country: string,
    dealerAccountId: number | null,
    tier: number,
  ): Promise<Account> {
    const account = new Account();
    {
      account.parent_account_id = dealerAccountId;
      account.company_name = companyName;
      account.country = country;
      account.tier = tier;
    }

    const createdEntity = await this.dataSource.transaction(
      async (entityManager) => {
        const repo = entityManager.getRepository(Account);
        const newAccount = repo.create(account);
        const persisted = await insertEntity(
          Account,
          repo,
          newAccount,
          this.isCommentOut,
          context,
        );
        return persisted;
      },
    );
    return createdEntity;
  }

  /**
   * 特定の情報でアカウントを更新する
   * @param account
   * @returns update
   */
  async update(context: Context, account: Account): Promise<UpdateResult> {
    return await this.dataSource.transaction(async (entityManager) => {
      const repo = entityManager.getRepository(Account);
      return await updateEntity(
        repo,
        { id: account.id },
        account,
        this.isCommentOut,
        context,
      );
    });
  }

  /**
   * プライマリ管理者とアカウント、ソート条件を同時に作成する
   * @param companyName
   * @param country
   * @param dealerAccountId
   * @param tier
   * @param adminExternalUserId
   * @param adminUserRole
   * @param adminUserAcceptedEulaVersion
   * @param adminUserAcceptedDpaVersion
   * @returns account/admin user
   */
  async createAccount(
    context: Context,
    companyName: string,
    country: string,
    dealerAccountId: number | undefined,
    tier: number,
    adminExternalUserId: string,
    adminUserRole: string,
    adminUserAcceptedEulaVersion?: string,
    adminUserAcceptedPrivacyNoticeVersion?: string,
    adminUserAcceptedDpaVersion?: string,
  ): Promise<{ newAccount: Account; adminUser: User }> {
    return await this.dataSource.transaction(async (entityManager) => {
      const account = new Account();
      {
        account.parent_account_id = dealerAccountId ?? null;
        account.company_name = companyName;
        account.country = country;
        account.tier = tier;
      }
      const accountsRepo = entityManager.getRepository(Account);
      const newAccount = accountsRepo.create(account);
      const persistedAccount = await insertEntity(
        Account,
        accountsRepo,
        newAccount,
        this.isCommentOut,
        context,
      );

      // 作成されたAccountのIDを使用してユーザーを作成
      const user = new User();
      {
        user.account_id = persistedAccount.id;
        user.external_id = adminExternalUserId;
        user.role = adminUserRole;
        user.accepted_eula_version = adminUserAcceptedEulaVersion ?? null;
        user.accepted_privacy_notice_version =
          adminUserAcceptedPrivacyNoticeVersion ?? null;
        user.accepted_dpa_version = adminUserAcceptedDpaVersion ?? null;
      }
      const usersRepo = entityManager.getRepository(User);
      const newUser = usersRepo.create(user);
      const persistedUser = await insertEntity(
        User,
        usersRepo,
        newUser,
        this.isCommentOut,
        context,
      );

      // アカウントに管理者を設定して更新
      persistedAccount.primary_admin_user_id = persistedUser.id;

      const result = await updateEntity(
        accountsRepo,
        { id: persistedAccount.id },
        persistedAccount,
        this.isCommentOut,
        context,
      );

      // 想定外の更新が行われた場合はロールバックを行った上でエラー送出
      if (result.affected !== 1) {
        throw new Error(`invalid update. result.affected=${result.affected}`);
      }

      // ユーザーのタスクソート条件を作成
      const sortCriteria = new SortCriteria();
      {
        sortCriteria.parameter = getTaskListSortableAttribute('JOB_NUMBER');
        sortCriteria.direction = getDirection('ASC');
        sortCriteria.user_id = persistedUser.id;
      }
      const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
      const newSortCriteria = sortCriteriaRepo.create(sortCriteria);
      await insertEntity(
        SortCriteria,
        sortCriteriaRepo,
        newSortCriteria,
        this.isCommentOut,
        context,
      );

      return { newAccount: persistedAccount, adminUser: persistedUser };
    });
  }

  /**
   * プライマリ管理者とアカウント、ソート条件を同時に削除する
   * @param accountId
   * @returns delete
   */
  async deleteAccount(
    context: Context,
    accountId: number,
    userId: number,
  ): Promise<void> {
    await this.dataSource.transaction(async (entityManager) => {
      const accountsRepo = entityManager.getRepository(Account);
      const usersRepo = entityManager.getRepository(User);
      const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
      // ソート条件を削除
      await deleteEntity(
        sortCriteriaRepo,
        { user_id: userId },
        this.isCommentOut,
        context,
      );
      // プライマリ管理者を削除
      await deleteEntity(usersRepo, { id: userId }, this.isCommentOut, context);
      // アカウントを削除
      await deleteEntity(
        accountsRepo,
        { id: accountId },
        this.isCommentOut,
        context,
      );
    });
  }

  /**
   * アカウントIDからアカウント情報を取得する
   * @param id
   * @returns account
   */
  async findAccountById(context: Context, id: number): Promise<Account> {
    const account = await this.dataSource.getRepository(Account).findOne({
      where: {
        id: id,
      },
      comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
    });

    if (!account) {
      throw new AccountNotFoundError(`Account is Not Found.`);
    }
    return account;
  }

  /**
   * ※サブルーチンとして、別途トランザクション開始された処理から呼び出されることを想定
   * 有効期限が現在日付からしきい値以内のライセンス数を取得する
   * @param entityManager
   * @param id
   * @param currentDate
   * @param expiringSoonDate
   * @returns expiringSoonLicense
   */
  private async getExpiringSoonLicense(
    context: Context,
    entityManager: EntityManager,
    id: number,
    currentDate: Date,
    expiringSoonDate: Date,
  ): Promise<number> {
    const license = entityManager.getRepository(License);

    // 有効期限が現在日付からしきい値以内のライセンス数を取得する
    const expiringSoonLicense = await license.count({
      where: {
        account_id: id,
        expiry_date: Between(currentDate, expiringSoonDate),
        status: Not(LICENSE_ALLOCATED_STATUS.DELETED),
      },
      comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
    });

    return expiringSoonLicense;
  }

  /**
   * ※サブルーチンとして、別途トランザクション開始された処理から呼び出されることを想定
   * 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う
   * @param entityManager
   * @param id
   * @param expiringSoonDate
   * @returns allocatableLicenseWithMargin
   */
  private async getAllocatableLicenseWithMargin(
    context: Context,
    entityManager: EntityManager,
    id: number,
    expiringSoonDate: Date,
  ): Promise<number> {
    const license = entityManager.getRepository(License);

    // 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う
    const allocatableLicenseWithMargin = await license.count({
      where: [
        {
          account_id: id,
          status: In([
            LICENSE_ALLOCATED_STATUS.UNALLOCATED,
            LICENSE_ALLOCATED_STATUS.REUSABLE,
          ]),
          expiry_date: MoreThan(expiringSoonDate),
        },
        {
          account_id: id,
          status: In([
            LICENSE_ALLOCATED_STATUS.UNALLOCATED,
            LICENSE_ALLOCATED_STATUS.REUSABLE,
          ]),
          expiry_date: IsNull(),
        },
      ],
      comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
    });

    return allocatableLicenseWithMargin;
  }

  /**
   * アカウントIDからライセンス情報を取得する
   * @param id
   * @param currentDate
   * @param expiringSoonDate
   * @returns licenseSummary
   */
  async getLicenseSummaryInfo(
    context: Context,
    id: number,
    currentDate: Date,
    expiringSoonDate: Date,
  ): Promise<{
    licenseSummary: LicenseSummaryInfo;
    isStorageAvailable: boolean;
  }> {
    return await this.dataSource.transaction(async (entityManager) => {
      const license = entityManager.getRepository(License);
      const licenseOrder = entityManager.getRepository(LicenseOrder);

      // 有効な総ライセンス数を取得する
      const totalLicense = await license.count({
        where: [
          {
            account_id: id,
            expiry_date: MoreThanOrEqual(currentDate),
            status: In([
              LICENSE_ALLOCATED_STATUS.REUSABLE,
              LICENSE_ALLOCATED_STATUS.UNALLOCATED,
            ]),
          },
          {
            account_id: id,
            expiry_date: IsNull(),
            status: In([
              LICENSE_ALLOCATED_STATUS.REUSABLE,
              LICENSE_ALLOCATED_STATUS.UNALLOCATED,
            ]),
          },
        ],
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      // 有効な総ライセンス数のうち、ユーザーに割り当て済みのライセンス数を取得する
      const allocatedLicense = await license.count({
        where: [
          {
            account_id: id,
            allocated_user_id: Not(IsNull()),
            expiry_date: MoreThanOrEqual(currentDate),
            status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
          },
          {
            account_id: id,
            allocated_user_id: Not(IsNull()),
            expiry_date: IsNull(),
            status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
          },
        ],
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      // 総ライセンス数のうち、ユーザーに割り当てたことがあるが、現在は割り当て解除され誰にも割り当たっていないライセンス数を取得する
      const reusableLicense = await license.count({
        where: [
          {
            account_id: id,
            expiry_date: MoreThanOrEqual(currentDate),
            status: LICENSE_ALLOCATED_STATUS.REUSABLE,
          },
          {
            account_id: id,
            expiry_date: IsNull(),
            status: LICENSE_ALLOCATED_STATUS.REUSABLE,
          },
        ],
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      // 総ライセンス数のうち、一度もユーザーに割り当てたことのないライセンス数を取得する
      const freeLicense = await license.count({
        where: [
          {
            account_id: id,
            expiry_date: MoreThanOrEqual(currentDate),
            status: LICENSE_ALLOCATED_STATUS.UNALLOCATED,
          },
          {
            account_id: id,
            expiry_date: IsNull(),
            status: LICENSE_ALLOCATED_STATUS.UNALLOCATED,
          },
        ],
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      // 有効期限が現在日付からしきい値以内のライセンス数を取得する
      const expiringSoonLicense = await this.getExpiringSoonLicense(
        context,
        entityManager,
        id,
        currentDate,
        expiringSoonDate,
      );

      // 未発行状態あるいは発行キャンセルされた注文数を取得する
      const numberOfRequesting = await licenseOrder.count({
        where: {
          from_account_id: id,
          status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      // 未発行状態あるいは発行キャンセルされた注文の総ライセンス数を取得する
      const result = await licenseOrder
        .createQueryBuilder('license_orders')
        .select('SUM(license_orders.quantity)', 'sum')
        .where('license_orders.from_account_id = :id', { id })
        .andWhere('license_orders.status = :status', {
          status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
        })
        .comment(`${context.getTrackingId()}_${new Date().toUTCString()}`)
        .getRawOne();
      const issueRequesting = parseInt(result.sum, 10) || 0;

      // 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う
      const allocatableLicenseWithMargin =
        await this.getAllocatableLicenseWithMargin(
          context,
          entityManager,
          id,
          expiringSoonDate,
        );

      // アカウントのロック状態を取得する
      const isStorageAvailable = (await this.findAccountById(context, id))
        .locked;

      let licenseSummary = new LicenseSummaryInfo();
      licenseSummary = {
        totalLicense: totalLicense,
        allocatedLicense: allocatedLicense,
        reusableLicense: reusableLicense,
        freeLicense: freeLicense,
        expiringSoonLicense: expiringSoonLicense,
        allocatableLicenseWithMargin: allocatableLicenseWithMargin,
        issueRequesting: issueRequesting,
        numberOfRequesting: numberOfRequesting,
      };
      return { licenseSummary: licenseSummary, isStorageAvailable };
    });
  }

  /**
   * ※サブルーチンとして、別途トランザクション開始された処理から呼び出されることを想定
   * アカウントIDをもとに、ライセンスの注文状況を取得する
   * @param id
   * @param currentDate
   * @param entityManager
   * @returns stockLicense
   * @returns issuedRequested
   * @returns issueRequesting
   */
  private async getAccountLicenseOrderStatus(
    context: Context,
    id: number,
    currentDate: Date,
    entityManager: EntityManager,
  ): Promise<{
    stockLicense: number;
    issuedRequested: number;
    issueRequesting: number;
  }> {
    const license = entityManager.getRepository(License);
    const licenseOrder = entityManager.getRepository(LicenseOrder);

    // 有効な総ライセンス数を取得する
    const stockLicense = await license.count({
      where: [
        {
          account_id: id,
          expiry_date: MoreThanOrEqual(currentDate),
          status: In([
            LICENSE_ALLOCATED_STATUS.REUSABLE,
            LICENSE_ALLOCATED_STATUS.UNALLOCATED,
          ]),
        },
        {
          account_id: id,
          expiry_date: IsNull(),
          status: In([
            LICENSE_ALLOCATED_STATUS.REUSABLE,
            LICENSE_ALLOCATED_STATUS.UNALLOCATED,
          ]),
        },
      ],
      comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
    });

    // 子アカウントからの、未発行状態あるいは発行キャンセルされた注文の総ライセンス数を取得する
    const issuedRequestedSqlResult = await licenseOrder
      .createQueryBuilder('license_orders')
      .select('SUM(license_orders.quantity)', 'sum')
      .where('license_orders.to_account_id = :id', { id })
      .andWhere('license_orders.status = :status', {
        status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
      })
      .comment(`${context.getTrackingId()}_${new Date().toUTCString()}`)
      .getRawOne();
    const issuedRequested = parseInt(issuedRequestedSqlResult.sum, 10) || 0;

    // 未発行状態あるいは発行キャンセルされた注文の総ライセンス数を取得する
    const issuedRequestingSqlResult = await licenseOrder
      .createQueryBuilder('license_orders')
      .select('SUM(license_orders.quantity)', 'sum')
      .where('license_orders.from_account_id = :id', { id })
      .andWhere('license_orders.status = :status', {
        status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
      })
      .comment(`${context.getTrackingId()}_${new Date().toUTCString()}`)
      .getRawOne();
    const issuedRequesting = parseInt(issuedRequestingSqlResult.sum, 10) || 0;

    return {
      stockLicense: stockLicense,
      issuedRequested: issuedRequested,
      issueRequesting: issuedRequesting,
    };
  }

  /**
   * アカウントIDをもとに、自分と子アカウントのライセンス情報を取得する
   * @param id
   * @param offset
   * @param limit
   * @returns total: 総件数
   * @returns ownPartnerLicenseFromRepository: リポジトリから取得した自アカウントのライセンス情報
   * @returns childrenPartnerLicensesFromRepository: リポジトリから取得した子アカウントのライセンス情報
   */
  async getPartnerLicense(
    context: Context,
    id: number,
    currentDate: Date,
    expiringSoonDate: Date,
    offset: number,
    limit: number,
  ): Promise<{
    total: number;
    ownPartnerLicenseFromRepository: PartnerLicenseInfoForRepository;
    childPartnerLicensesFromRepository: PartnerLicenseInfoForRepository[];
  }> {
    return await this.dataSource.transaction(async (entityManager) => {
      const account = entityManager.getRepository(Account);

      // 自アカウントの情報を取得する
      const ownAccount = await account.findOne({
        where: {
          id: id,
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      if (!ownAccount) {
        throw new AccountNotFoundError(`Account is Not Found.`);
      }

      // 自アカウントのライセンス注文状況を取得する
      const ownLicenseOrderStatus = await this.getAccountLicenseOrderStatus(
        context,
        id,
        currentDate,
        entityManager,
      );

      // 自アカウントの戻り値を設定する
      const ownPartnerLicenseFromRepository: PartnerLicenseInfoForRepository = {
        accountId: id,
        tier: ownAccount.tier,
        companyName: ownAccount.company_name,
        stockLicense: ownLicenseOrderStatus.stockLicense,
        issuedRequested: ownLicenseOrderStatus.issuedRequested,
        issueRequesting: ownLicenseOrderStatus.issueRequesting,
      };

      // 子アカウントのアカウント情報を取得する
      const childAccounts = await account.find({
        where: {
          parent_account_id: id,
        },
        order: {
          company_name: 'ASC',
        },
        take: limit,
        skip: offset,
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      // 各子アカウントのライセンス注文状況を取得する
      const childPartnerLicensesFromRepository: PartnerLicenseInfoForRepository[] =
        [];
      for (const childAccount of childAccounts) {
        // ライセンス注文状況を取得する
        const childLicenseOrderStatus = await this.getAccountLicenseOrderStatus(
          context,
          childAccount.id,
          currentDate,
          entityManager,
        );

        // 第五の不足数を算出するためのライセンス数情報を取得する
        let expiringSoonLicense: number = 0;
        let allocatableLicenseWithMargin: number = 0;
        if (childAccount.tier === TIERS.TIER5) {
          expiringSoonLicense = await this.getExpiringSoonLicense(
            context,
            entityManager,
            childAccount.id,
            currentDate,
            expiringSoonDate,
          );
          allocatableLicenseWithMargin =
            await this.getAllocatableLicenseWithMargin(
              context,
              entityManager,
              childAccount.id,
              expiringSoonDate,
            );
        }

        // 戻り値用の値を設定
        const childPartnerLicenseFromRepository: PartnerLicenseInfoForRepository =
          {
            accountId: childAccount.id,
            tier: childAccount.tier,
            companyName: childAccount.company_name,
            stockLicense: childLicenseOrderStatus.stockLicense,
            issuedRequested: childLicenseOrderStatus.issuedRequested,
            issueRequesting: childLicenseOrderStatus.issueRequesting,
            expiringSoonLicense: expiringSoonLicense,
            allocatableLicenseWithMargin: allocatableLicenseWithMargin,
          };

        childPartnerLicensesFromRepository.push(
          childPartnerLicenseFromRepository,
        );
      }

      // limit/offsetによらない総件数を取得する
      const total = await account.count({
        where: {
          parent_account_id: id,
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      return {
        total: total,
        ownPartnerLicenseFromRepository: ownPartnerLicenseFromRepository,
        childPartnerLicensesFromRepository: childPartnerLicensesFromRepository,
      };
    });
  }

  /**
   * Dealer(Tier4)アカウント情報を取得する
   * @returns dealer accounts
   */
  async findDealerAccounts(context: Context): Promise<Account[]> {
    const accounts = await this.dataSource.getRepository(Account).find({
      where: {
        tier: TIERS.TIER4,
      },
      comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
    });

    return accounts;
  }

  /**
   * 対象のアカウントIDの親世代のアカウントIDをすべて取得する
   * 順番は、階層(tier)の下位から上位に向かって格納
   * @param targetAccountId
   * @returns accountIds
   */
  async getHierarchyParents(
    context: Context,
    targetAccountId: number,
  ): Promise<number[]> {
    return await this.dataSource.transaction(async (entityManager) => {
      const accountRepository = entityManager.getRepository(Account);
      const maxTierDifference = TIERS.TIER5 - TIERS.TIER1;
      const parentAccountIds: number[] = [];

      let currentAccountId = targetAccountId;
      // システム的な最大の階層差異分、親を参照する
      for (let i = 0; i < maxTierDifference; i++) {
        const account = await accountRepository.findOne({
          where: {
            id: currentAccountId,
          },
          comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
        });
        if (!account) {
          break;
        }
        if (!account.parent_account_id) {
          throw new Error("Parent account doesn't exist.");
        }

        parentAccountIds.push(account.parent_account_id);
        currentAccountId = account.parent_account_id;
      }

      return parentAccountIds;
    });
  }

  /**
   * 注文元アカウントIDとPOナンバーに紐づくライセンス発行をキャンセルする
   * @param orderedAccountId:キャンセルしたい発行の注文元アカウントID
   * @param poNumber:POナンバー
   * @returns { canceledIssueLicenseOrderId } : キャンセルされたライセンス発行に紐づく注文ID
   */
  async cancelIssue(
    context: Context,
    orderedAccountId: number,
    poNumber: string,
  ): Promise<{ canceledIssueLicenseOrderId: number }> {
    return await this.dataSource.transaction(async (entityManager) => {
      const orderRepo = entityManager.getRepository(LicenseOrder);

      // キャンセル対象の発行を取得
      const targetOrder = await orderRepo.findOne({
        where: {
          from_account_id: orderedAccountId,
          po_number: poNumber,
          status: LICENSE_ISSUE_STATUS.ISSUED,
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      // キャンセル対象の発行が存在しない場合エラー
      if (!targetOrder) {
        throw new AlreadyLicenseStatusChangedError(
          `Cancel issue is failed. Already license order status changed. fromAccountId: ${orderedAccountId}, poNumber: ${poNumber}`,
        );
      }

      // キャンセル可能な日付（発行日から14日経過）かどうかに判定する時刻を取得する
      const currentDateWithoutTime = new DateWithZeroTime();
      const issuedDateWithoutTime = new DateWithZeroTime(targetOrder.issued_at);
      const timeDifference =
        currentDateWithoutTime.getTime() - issuedDateWithoutTime.getTime();
      const daysDifference = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
      // 発行日から14日経過しているかをチェック
      if (daysDifference > LICENSE_EXPIRATION_THRESHOLD_DAYS) {
        throw new CancellationPeriodExpiredError(
          `Cancel issue is failed. Cancellation period expired. fromAccountId: ${orderedAccountId}, poNumber: ${poNumber}`,
        );
      }
      // すでに割り当て済みライセンスを含む注文か確認する
      const licenseRepo = entityManager.getRepository(License);
      const allocatedLicense = await licenseRepo.findOne({
        where: {
          order_id: targetOrder.id,
          status: Not(LICENSE_ALLOCATED_STATUS.UNALLOCATED),
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
        lock: { mode: 'pessimistic_write' },
      });

      // 存在した場合エラー
      if (allocatedLicense) {
        throw new AlreadyLicenseAllocatedError(
          `Cancel issue is failed. Already license allocated. fromAccountId: ${orderedAccountId}, poNumber: ${poNumber}`,
        );
      }

      // 更新用の変数に値をコピー
      const updatedOrder = { ...targetOrder };

      // 注文を発行待ちに戻す
      updatedOrder.issued_at = null;
      updatedOrder.status = LICENSE_ISSUE_STATUS.ISSUE_REQUESTING;
      await updateEntity(
        orderRepo,
        { id: targetOrder.id },
        updatedOrder,
        this.isCommentOut,
        context,
      );

      // 発行時に削除したライセンスを未割当に戻す
      await updateEntity(
        licenseRepo,
        { delete_order_id: targetOrder.id },
        {
          status: LICENSE_ALLOCATED_STATUS.UNALLOCATED,
          deleted_at: null,
          delete_order_id: null,
        },
        this.isCommentOut,
        context,
      );
      // 発行時に発行されたライセンスを削除する
      await deleteEntity(
        licenseRepo,
        { order_id: targetOrder.id },
        this.isCommentOut,
        context,
      );

      return { canceledIssueLicenseOrderId: targetOrder.id };
    });
  }

  /**
   * アカウントIDをもとに、パートナー一覧を取得する
   * @param id
   * @param limit
   * @param offset
   * @returns total: 総件数
   * @returns partners: DBから取得できるパートナー一覧情報
   */
  async getPartners(
    context: Context,
    id: number,
    limit: number,
    offset: number,
  ): Promise<{
    total: number;
    partnersInfo: PartnerInfoFromDb[];
  }> {
    return await this.dataSource.transaction(async (entityManager) => {
      const accountRepo = entityManager.getRepository(Account);

      // limit/offsetによらない総件数を取得する
      const total = await accountRepo.count({
        where: {
          parent_account_id: id,
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      const partnerAccounts = await accountRepo.find({
        where: {
          parent_account_id: id,
        },
        order: {
          company_name: 'ASC',
        },
        take: limit,
        skip: offset,
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      // ADB2Cから情報を取得するための外部ユーザIDを取得する（念のためプライマリ管理者IDが存在しない場合を考慮）
      const primaryUserIds = partnerAccounts.flatMap((x) => {
        if (x.primary_admin_user_id) {
          return [x.primary_admin_user_id];
        } else if (x.secondary_admin_user_id) {
          return [x.secondary_admin_user_id];
        } else {
          return [];
        }
      });
      const userRepo = entityManager.getRepository(User);
      const primaryUsers = await userRepo.find({
        where: {
          id: In(primaryUserIds),
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      // アカウント情報とプライマリ管理者の外部ユーザIDをマージ
      const partners = partnerAccounts.map((account) => {
        const primaryUser = primaryUsers.find(
          (user) =>
            user.id === account.primary_admin_user_id ||
            user.id === account.secondary_admin_user_id,
        );
        if (!primaryUser) {
          throw new AdminUserNotFoundError(
            `Primary admin user is not found. id: ${account.primary_admin_user_id}, account_id: ${account.id}`,
          );
        }

        return {
          name: account.company_name,
          tier: account.tier,
          accountId: account.id,
          country: account.country,
          primaryAccountExternalId: primaryUser.external_id,
          dealerManagement: account.delegation_permission,
        };
      });

      return {
        total: total,
        partnersInfo: partners,
      };
    });
  }

  /**
   * 一階層上のアカウントを取得する
   * @param accountId
   * @param tier
   * @returns account: 一階層上のアカウント
   */
  async getOneUpperTierAccount(
    context: Context,
    accountId: number,
    tier: number,
  ): Promise<Account | null> {
    return await this.dataSource.transaction(async (entityManager) => {
      const accountRepo = entityManager.getRepository(Account);
      return await accountRepo.findOne({
        where: {
          id: accountId,
          tier: tier - 1,
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
    });
  }

  /**
   * アカウント情報を保存する
   * @param myAccountId
   * @param tier
   * @param delegationPermission
   * @param primaryAdminUserId
   * @param parentAccountId
   * @param secondryAdminUserId
   */
  async updateAccountInfo(
    context: Context,
    myAccountId: number,
    tier: number,
    delegationPermission: boolean,
    primaryAdminUserId: number,
    parentAccountId?: number,
    secondryAdminUserId?: number,
  ): Promise<void> {
    await this.dataSource.transaction(async (entityManager) => {
      // ディーラーアカウントが指定されている場合、存在チェックを行う
      if (parentAccountId) {
        const dealerAccount = await this.getOneUpperTierAccount(
          context,
          parentAccountId,
          tier,
        );
        // 取得できない場合、エラー
        if (!dealerAccount) {
          throw new DealerAccountNotFoundError(
            `Dealer account is not found. id: ${parentAccountId}}`,
          );
        }
      }

      const userRepo = entityManager.getRepository(User);
      // プライマリ管理者ユーザーの存在チェック
      if (primaryAdminUserId) {
        const primaryAdminUser = await userRepo.findOne({
          where: {
            id: primaryAdminUserId,
            account_id: myAccountId,
            email_verified: true,
          },
          comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
          lock: { mode: 'pessimistic_write' },
        });
        if (!primaryAdminUser) {
          throw new AdminUserNotFoundError(
            `Primary admin user is not found or email not verified. id: ${primaryAdminUserId}, account_id: ${myAccountId}`,
          );
        }
      }

      // セカンダリ管理者ユーザーの存在チェック
      if (secondryAdminUserId) {
        const secondryAdminUser = await userRepo.findOne({
          where: {
            id: secondryAdminUserId,
            account_id: myAccountId,
            email_verified: true,
          },
          comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
          lock: { mode: 'pessimistic_write' },
        });
        if (!secondryAdminUser) {
          throw new AdminUserNotFoundError(
            `Secondary admin user is not found or email not verified. id: ${secondryAdminUserId}, account_id: ${myAccountId}`,
          );
        }
      }
      const accountRepo = entityManager.getRepository(Account);
      // アカウント情報を更新
      await updateEntity(
        accountRepo,
        { id: myAccountId },
        {
          parent_account_id: parentAccountId ?? null,
          delegation_permission: delegationPermission,
          primary_admin_user_id: primaryAdminUserId,
          secondary_admin_user_id: secondryAdminUserId ?? null,
        },
        this.isCommentOut,
        context,
      );
    });
  }

  /*
   * ActiveWorktypeIdを更新する
   * @param accountId
   * @param [id] ActiveWorktypeIdの内部ID
   * @returns active worktype id
   */
  async updateActiveWorktypeId(
    context: Context,
    accountId: number,
    id?: number | undefined,
  ): Promise<void> {
    return await this.dataSource.transaction(async (entityManager) => {
      const worktypeRepo = entityManager.getRepository(Worktype);
      const accountRepo = entityManager.getRepository(Account);

      if (id) {
        // 自アカウント内に指定IDのワークタイプが存在するか確認
        const worktype = await worktypeRepo.findOne({
          where: { account_id: accountId, id: id },
          comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
        });

        // ワークタイプが存在しない場合はエラー
        if (!worktype) {
          throw new WorktypeIdNotFoundError(`Worktype is not found. id: ${id}`);
        }
      }

      // アカウントのActiveWorktypeIDを更新
      await updateEntity(
        accountRepo,
        { id: accountId },
        { active_worktype_id: id ?? null },
        this.isCommentOut,
        context,
      );
    });
  }

  /**
   * 指定されたアカウントを削除する
   * @param accountId
   * @returns users 削除対象のユーザー
   */
  async deleteAccountAndInsertArchives(
    context: Context,
    accountId: number,
  ): Promise<User[]> {
    return await this.dataSource.transaction(async (entityManager) => {
      // 削除対象のユーザーを退避テーブルに退避
      const users = await this.dataSource.getRepository(User).find({
        where: {
          account_id: accountId,
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      const userArchiveRepo = entityManager.getRepository(UserArchive);
      await insertEntities(
        UserArchive,
        userArchiveRepo,
        users,
        this.isCommentOut,
        context,
      );

      // 削除対象のライセンスを退避テーブルに退避
      const licenses = await this.dataSource.getRepository(License).find({
        where: {
          account_id: accountId,
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      const licenseArchiveRepo = entityManager.getRepository(LicenseArchive);
      await insertEntities(
        LicenseArchive,
        licenseArchiveRepo,
        licenses,
        this.isCommentOut,
        context,
      );

      // 削除対象のライセンス割り当て履歴を退避テーブルに退避
      const licenseHistories = await this.dataSource
        .getRepository(LicenseAllocationHistory)
        .find({
          where: {
            account_id: accountId,
          },
          comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
        });
      const licenseHistoryArchiveRepo = entityManager.getRepository(
        LicenseAllocationHistoryArchive,
      );
      await insertEntities(
        LicenseAllocationHistoryArchive,
        licenseHistoryArchiveRepo,
        licenseHistories,
        this.isCommentOut,
        context,
      );

      // アカウントを削除
      const accountRepo = entityManager.getRepository(Account);
      await deleteEntity(
        accountRepo,
        { id: accountId },
        this.isCommentOut,
        context,
      );
      // ライセンス系(card_license_issue以外)のテーブルのレコードを削除する
      const orderRepo = entityManager.getRepository(LicenseOrder);
      await deleteEntity(
        orderRepo,
        { from_account_id: accountId },
        this.isCommentOut,
        context,
      );
      const licenseRepo = entityManager.getRepository(License);
      const targetLicenses = await licenseRepo.find({
        where: {
          account_id: accountId,
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      const cardLicenseRepo = entityManager.getRepository(CardLicense);
      await deleteEntity(
        cardLicenseRepo,
        { license_id: In(targetLicenses.map((license) => license.id)) },
        this.isCommentOut,
        context,
      );
      await deleteEntity(
        licenseRepo,
        { account_id: accountId },
        this.isCommentOut,
        context,
      );
      const LicenseAllocationHistoryRepo = entityManager.getRepository(
        LicenseAllocationHistory,
      );
      await deleteEntity(
        LicenseAllocationHistoryRepo,
        { account_id: accountId },
        this.isCommentOut,
        context,
      );

      // ワークタイプ系のテーブルのレコードを削除する
      const worktypeRepo = entityManager.getRepository(Worktype);
      const taggerWorktypes = await worktypeRepo.find({
        where: { account_id: accountId },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      const optionItemRepo = entityManager.getRepository(OptionItem);
      await deleteEntity(
        optionItemRepo,
        { worktype_id: In(taggerWorktypes.map((worktype) => worktype.id)) },
        this.isCommentOut,
        context,
      );
      await deleteEntity(
        worktypeRepo,
        { account_id: accountId },
        this.isCommentOut,
        context,
      );

      // タスク系のテーブルのレコードを削除する
      const taskRepo = entityManager.getRepository(Task);
      const targetTasks = await taskRepo.find({
        where: {
          account_id: accountId,
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      const checkoutPermissionRepo =
        entityManager.getRepository(CheckoutPermission);
      await deleteEntity(
        checkoutPermissionRepo,
        { task_id: In(targetTasks.map((task) => task.id)) },
        this.isCommentOut,
        context,
      );
      await deleteEntity(
        taskRepo,
        { account_id: accountId },
        this.isCommentOut,
        context,
      );

      // オーディオファイル系のテーブルのレコードを削除する
      const audioFileRepo = entityManager.getRepository(AudioFile);
      const targetaudioFiles = await audioFileRepo.find({
        where: {
          account_id: accountId,
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      const audioOptionItemsRepo = entityManager.getRepository(AudioOptionItem);
      await deleteEntity(
        audioOptionItemsRepo,
        {
          audio_file_id: In(targetaudioFiles.map((audioFile) => audioFile.id)),
        },
        this.isCommentOut,
        context,
      );
      await deleteEntity(
        audioFileRepo,
        { account_id: accountId },
        this.isCommentOut,
        context,
      );

      // ユーザーグループ系のテーブルのレコードを削除する
      const userGroupRepo = entityManager.getRepository(UserGroup);
      const targetUserGroup = await userGroupRepo.find({
        where: {
          account_id: accountId,
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      const userGroupMemberRepo = entityManager.getRepository(UserGroupMember);
      await deleteEntity(
        userGroupMemberRepo,
        {
          user_group_id: In(targetUserGroup.map((userGroup) => userGroup.id)),
        },
        this.isCommentOut,
        context,
      );
      await deleteEntity(
        userGroupRepo,
        { account_id: accountId },
        this.isCommentOut,
        context,
      );

      // テンプレートファイルテーブルのレコードを削除する
      const templateFileRepo = entityManager.getRepository(TemplateFile);
      await deleteEntity(
        templateFileRepo,
        { account_id: accountId },
        this.isCommentOut,
        context,
      );

      // ユーザテーブルのレコードを削除する
      const userRepo = entityManager.getRepository(User);
      await deleteEntity(
        userRepo,
        { account_id: accountId },
        this.isCommentOut,
        context,
      );

      // ソート条件のテーブルのレコードを削除する
      const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
      await deleteEntity(
        sortCriteriaRepo,
        { user_id: In(users.map((user) => user.id)) },
        this.isCommentOut,
        context,
      );
      return users;
    });
  }
}
