import { Injectable } from '@nestjs/common';
import {
  DataSource,
  FindOptionsOrder,
  FindOptionsOrderValue,
  In,
  IsNull,
} from 'typeorm';
import { Task } from './entity/task.entity';
import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants';
import { AudioOptionItem as ParamOptionItem } from '../../features/files/types/types';
import { AudioFile } from '../audio_files/entity/audio_file.entity';
import { AudioOptionItem } from '../audio_option_items/entity/audio_option_item.entity';
import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity';
import {
  SortDirection,
  TaskListSortableAttribute,
} from '../../common/types/sort';
import { UserGroupMember } from '../user_groups/entity/user_group_member.entity';
import { Assignee } from '../../features/tasks/types/types';
import { UserGroup } from '../user_groups/entity/user_group.entity';
import { User } from '../users/entity/user.entity';
import {
  AccountNotMatchError,
  CheckoutPermissionNotFoundError,
  StatusNotMatchError,
  TaskAuthorIdNotMatchError,
  TasksNotFoundError,
  TypistUserGroupNotFoundError,
  TypistUserNotFoundError,
  TypistUserNotMatchError,
} from './errors/types';
import { Roles } from '../../common/types/role';
import { TaskStatus, isTaskStatus } from '../../common/types/taskStatus';

@Injectable()
export class TasksRepositoryService {
  constructor(private dataSource: DataSource) {}

  /**
   * 音声ファイルと紐づいたTaskを取得する
   * @param audioFileId
   * @param account_id
   * @param status 配列で設定したステータスのタスクのみ取得
   * @returns task and audio file
   */
  async getTaskAndAudioFile(
    audioFileId: number,
    account_id: number,
    status: string[],
  ): Promise<Task> {
    return await this.dataSource.transaction(async (entityManager) => {
      const taskRepo = entityManager.getRepository(Task);
      // 指定した音声ファイルIDに紐づくTaskの中でステータスが一致するものを取得
      const task = await taskRepo.findOne({
        relations: {
          file: true,
          template_file: true,
        },
        where: {
          audio_file_id: audioFileId,
          account_id: account_id,
          status: In(status),
        },
      });
      if (!task) {
        throw new TasksNotFoundError(
          `task not found. audio_file_id:${audioFileId}`,
        );
      }

      // アカウントチェック
      if (task.account_id !== account_id) {
        throw new AccountNotMatchError(
          `task account_id not match. audio_file_id:${audioFileId}, task.account_id:${task.account_id}, account_id:${account_id}`,
        );
      }

      // ステータスチェック
      if (!status.includes(task.status)) {
        throw new StatusNotMatchError(
          `Unexpected task status. status:${task.status}`,
        );
      }
      return task;
    });
  }

  /**
   * 音声ファイルIDに紐づいたTaskを取得する
   * @param audio_file_id
   * @param account_id
   * @param author_id
   * @returns task from author id
   */
  async getTaskFromAudioFileId(
    audio_file_id: number,
    account_id: number,
    author_id: string,
  ): Promise<Task> {
    return await this.dataSource.transaction(async (entityManager) => {
      const taskRepo = entityManager.getRepository(Task);
      // 指定した音声ファイルIDに紐づくTaskの中でAuthorIDが一致するものを取得
      const task = await taskRepo.findOne({
        relations: {
          file: true,
        },
        where: {
          audio_file_id: audio_file_id,
        },
      });
      if (!task) {
        throw new TasksNotFoundError(
          `task not found. audio_file_id:${audio_file_id}`,
        );
      }
      // アカウントチェック
      if (task.account_id !== account_id) {
        throw new AccountNotMatchError(
          `task account_id not match. audio_file_id:${audio_file_id}, account_id(Task):${task.account_id}, account_id:${account_id}`,
        );
      }
      if (task.file?.author_id !== author_id) {
        throw new TaskAuthorIdNotMatchError(
          `task authorId not match. audio_file_id:${audio_file_id}, author_id:${author_id}, author_id(Task):${task.file?.author_id}`,
        );
      }

      return task;
    });
  }
  /**
   * 音声ファイルIDに紐づいたTaskをCheckoutする
   * @param audio_file_id
   * @param account_id
   * @param user_id
   * @param permittedSourceStatus
   * @returns checkout
   */
  async checkout(
    audio_file_id: number,
    account_id: number,
    user_id: number,
    permittedSourceStatus: TaskStatus[],
  ): Promise<void> {
    await this.dataSource.transaction(async (entityManager) => {
      const taskRepo = entityManager.getRepository(Task);
      // 指定した音声ファイルIDに紐づくTaskの中でStatusが[Uploaded,Inprogress,Pending]であるものを取得
      const task = await taskRepo.findOne({
        where: {
          audio_file_id: audio_file_id,
        },
      });
      if (!task) {
        throw new TasksNotFoundError(
          `task not found. audio_file_id:${audio_file_id}`,
        );
      }
      // アカウントチェック
      if (task.account_id !== account_id) {
        throw new AccountNotMatchError(
          `task account_id not match. audio_file_id:${audio_file_id}, account_id(Task):${task.account_id}, account_id:${account_id}`,
        );
      }
      if (!isTaskStatus(task.status)) {
        throw new Error('invalid task status');
      }
      // ステータスチェック
      if (!permittedSourceStatus.includes(task.status)) {
        throw new StatusNotMatchError(
          `Unexpected task status. audio_file_id:${audio_file_id}, status:${task.status}`,
        );
      }

      const groupMemberRepo = entityManager.getRepository(UserGroupMember);
      // ユーザーの所属するすべてのグループを列挙
      const groups = await groupMemberRepo.find({
        relations: {
          user: true,
        },
        where: {
          user: {
            id: user_id,
          },
        },
      });
      // ユーザーの所属するすべてのグループIDを列挙
      const groupIds = groups.map((member) => member.user_group_id);

      const checkoutRepo = entityManager.getRepository(CheckoutPermission);
      // 対象タスクに紐づくユーザーが含まれるチェックアウト権限を取得する
      const related = await checkoutRepo.find({
        where: [
          {
            task_id: task.id,
            // ユーザーがチェックアウト可能である
            user: {
              id: user_id,
            },
          },
          {
            task_id: task.id,
            // ユーザーの所属するユーザーグループがチェックアウト可能である
            user_group_id: In(groupIds),
          },
        ],
      });

      //チェックアウト権限がなければエラー
      if (related.length === 0) {
        throw new CheckoutPermissionNotFoundError(
          `Checkout Permission not found. task_id:${task.id}, user_id:${user_id}, user_group_id:${groupIds}`,
        );
      }

      //　対象タスクの文字起こし開始日時を現在時刻に更新。割り当てユーザーを自身のユーザーIDに更新
      // タスクのステータスがUploaded以外の場合、文字起こし開始時刻は更新しない
      await taskRepo.update(
        { audio_file_id: audio_file_id },
        {
          started_at:
            task.status === TASK_STATUS.UPLOADED
              ? new Date().toISOString()
              : undefined,
          typist_user_id: user_id,
          status: TASK_STATUS.IN_PROGRESS,
        },
      );

      //対象のタスクに紐づくチェックアウト権限レコードを削除
      await checkoutRepo.delete({
        task_id: task.id,
      });

      //対象のタスクチェックアウト権限を自身のユーザーIDで作成
      await checkoutRepo.save({
        task_id: task.id,
        user_id: user_id,
      });
    });
  }

  /**
   * 音声ファイルIDで指定したタスクをcheckinする
   * @param audio_file_id チェックインするタスクの音声ファイルID
   * @param user_id チェックインするユーザーのID
   * @param permittedSourceStatus チェックイン可能なステータス
   * @returns checkin
   */
  async checkin(
    audio_file_id: number,
    user_id: number,
    permittedSourceStatus: TaskStatus,
  ): Promise<void> {
    await this.dataSource.transaction(async (entityManager) => {
      const taskRepo = entityManager.getRepository(Task);
      const task = await taskRepo.findOne({
        where: {
          audio_file_id: audio_file_id,
        },
      });
      if (!task) {
        throw new TasksNotFoundError(
          `task not found. audio_file_id:${audio_file_id}`,
        );
      }
      if (task.status !== permittedSourceStatus) {
        throw new StatusNotMatchError(
          `Unexpected task status. audio_file_id:${audio_file_id}, status:${task.status}`,
        );
      }
      if (task.typist_user_id !== user_id) {
        throw new TypistUserNotMatchError(
          `TypistUser not match. audio_file_id:${audio_file_id}, typist_user_id:${task.typist_user_id}, user_id:${user_id}`,
        );
      }

      //　対象タスクの文字起こし終了日時を現在時刻に更新。ステータスをFinishedに更新
      await taskRepo.update(
        { audio_file_id: audio_file_id },
        {
          finished_at: new Date().toISOString(),
          status: TASK_STATUS.FINISHED,
        },
      );
    });
  }

  /**
   * 音声ファイルIDで指定したタスクをキャンセルする
   * @param audio_file_id キャンセルするタスクの音声ファイルID
   * @param permittedSourceStatus キャンセル可能なステータス
   * @param account_id キャンセルするタスクのアカウントID
   * @param user_id キャンセルするユーザーのID(API実行者がAdminのときは使用しない)
   * @returns cancel
   */
  async cancel(
    audio_file_id: number,
    permittedSourceStatus: TaskStatus[],
    account_id: number,
    user_id?: number | undefined,
  ): Promise<void> {
    await this.dataSource.transaction(async (entityManager) => {
      const taskRepo = entityManager.getRepository(Task);
      const task = await taskRepo.findOne({
        where: {
          audio_file_id: audio_file_id,
        },
      });
      if (!task) {
        throw new TasksNotFoundError(
          `task not found. audio_file_id:${audio_file_id}`,
        );
      }
      if (!isTaskStatus(task.status)) {
        throw new Error('invalid task status');
      }
      // ステータスチェック
      if (!permittedSourceStatus.includes(task.status)) {
        throw new StatusNotMatchError(
          `Unexpected task status. audio_file_id:${audio_file_id}, status:${task.status}`,
        );
      }
      if (task.account_id !== account_id) {
        throw new AccountNotMatchError(
          `task account_id not match. audio_file_id:${audio_file_id}, account_id(Task):${task.account_id}, account_id:${account_id}`,
        );
      }
      if (user_id && task.typist_user_id !== user_id) {
        throw new TypistUserNotMatchError(
          `TypistUser not match. audio_file_id:${audio_file_id}, typist_user_id:${task.typist_user_id}, user_id:${user_id}`,
        );
      }

      //　対象タスクの文字起こし担当をnull,ステータスをUploadedに更新
      await taskRepo.update(
        { audio_file_id: audio_file_id },
        {
          typist_user: null,
          status: TASK_STATUS.UPLOADED,
        },
      );

      const checkoutPermissionRepo =
        entityManager.getRepository(CheckoutPermission);

      // 対象タスクの文字起こし候補を削除
      /* 対象タスクがInprogress,Pendingの状態の場合、文字起こし担当者個人指定のレコードのみだが、ユーザーIDの指定はしない
       (データの不整合があっても問題ないように)*/
      await checkoutPermissionRepo.delete({
        task_id: task.id,
      });
    });
  }

  /**
   * 音声ファイルIDで指定したタスクをsuspendする
   * @param audio_file_id 文字起こし中断するタスクの音声ファイルID
   * @param user_id 文字起こし中断するユーザーのID
   * @param permittedSourceStatus 文字起こし中断可能なステータス
   * @returns suspend
   */
  async suspend(
    audio_file_id: number,
    user_id: number,
    permittedSourceStatus: TaskStatus,
  ): Promise<void> {
    await this.dataSource.transaction(async (entityManager) => {
      const taskRepo = entityManager.getRepository(Task);
      const task = await taskRepo.findOne({
        where: {
          audio_file_id: audio_file_id,
        },
      });
      if (!task) {
        throw new TasksNotFoundError(
          `task not found. audio_file_id:${audio_file_id}`,
        );
      }
      if (task.status !== permittedSourceStatus) {
        throw new StatusNotMatchError(
          `Unexpected task status. status:${task.status}`,
        );
      }
      if (task.typist_user_id !== user_id) {
        throw new TypistUserNotMatchError(
          `TypistUser not match. typist_user_id:${task.typist_user_id}, user_id:${user_id}`,
        );
      }

      //　対象タスクの文字起こし終了日時を現在時刻に更新。ステータスをFinishedに更新
      await taskRepo.update(
        { audio_file_id: audio_file_id },
        {
          status: TASK_STATUS.PENDING,
        },
      );
    });
  }

  /**
   * 指定したアカウントIDに紐づくTask関連情報の一覧を取得します
   * @param account_id
   * @param offset
   * @param limit
   * @param sort_criteria
   * @param direction
   * @param status
   * @returns tasks: タスク情報 / permissions:タスクに紐づくチェックアウト権限情報 / count: offset|limitを行わなかった場合の該当タスクの合計
   */
  async getTasksFromAccountId(
    account_id: number,
    offset: number,
    limit: number,
    sort_criteria: TaskListSortableAttribute,
    direction: SortDirection,
    status: string[],
  ): Promise<{
    tasks: Task[];
    permissions: CheckoutPermission[];
    count: number;
  }> {
    const order = makeOrder(sort_criteria, direction);

    const value = await this.dataSource.transaction(async (entityManager) => {
      const taskRepo = entityManager.getRepository(Task);

      // limit/offsetによらず条件に一致するすべてのレコード数を取得
      const count = await taskRepo.count({
        where: {
          account_id: account_id,
          status: In(status),
        },
      });

      // 条件に該当するTask一覧を取得
      const tasks = await taskRepo.find({
        relations: {
          file: true,
          option_items: true,
          typist_user: true,
        },
        where: {
          account_id: account_id,
          status: In(status),
        },
        order: order, // 引数によってOrderに使用するパラメータを変更
        take: limit,
        skip: offset,
      });

      // TODO [Task2249] Task内にCheckoutPermissionを含める方法が上手くいかなかった（複雑になりすぎた？ 原因未調査）ため、
      //      確実に上手くいく方法としてQueryの分割を行ったが、本来はオブジェクトの構築はTypeORMに一任したい
      const checkoutRepo = entityManager.getRepository(CheckoutPermission);

      const taskIds = tasks.map((x) => x.id);
      const permissions = await checkoutRepo.find({
        relations: {
          user: true,
          user_group: true,
        },
        where: {
          task_id: In(taskIds),
        },
      });
      return { tasks, permissions, count };
    });
    return value;
  }
  /**
   * 指定したauthor_idとaccount_idに紐づくTask関連情報の一覧を取得します
   * @param author_id
   * @param account_id
   * @param offset
   * @param limit
   * @param sort_criteria
   * @param direction
   * @param status
   * @returns tasks: タスク情報 / permissions:タスクに紐づくチェックアウト権限情報 / count: offset|limitを行わなかった場合の該当タスクの合計
   */
  async getTasksFromAuthorIdAndAccountId(
    author_id: string,
    account_id: number,
    offset: number,
    limit: number,
    sort_criteria: TaskListSortableAttribute,
    direction: SortDirection,
    status: string[],
  ): Promise<{
    tasks: Task[];
    permissions: CheckoutPermission[];
    count: number;
  }> {
    const order = makeOrder(sort_criteria, direction);
    const value = await this.dataSource.transaction(async (entityManager) => {
      const taskRepo = entityManager.getRepository(Task);
      const count = await taskRepo.count({
        where: {
          account_id: account_id,
          status: In(status),
          file: { author_id: author_id },
        },
      });

      const tasks = await taskRepo.find({
        relations: {
          file: true,
          option_items: true,
          typist_user: true,
        },
        where: {
          account_id: account_id,
          status: In(status),
          file: { author_id: author_id },
        },
        order: order, // 引数によってOrderに使用するパラメータを変更
        take: limit,
        skip: offset,
      });

      const checkoutRepo = entityManager.getRepository(CheckoutPermission);
      const taskIds = tasks.map((x) => x.id);
      const permissions = await checkoutRepo.find({
        relations: {
          user: true,
          user_group: true,
        },
        where: {
          task_id: In(taskIds),
        },
      });
      return { tasks, permissions, count };
    });
    return value;
  }

  async getTasksFromTypistRelations(
    external_user_id: string,
    offset: number,
    limit: number,
    sort_criteria: TaskListSortableAttribute,
    direction: SortDirection,
    status: string[],
  ): Promise<{
    tasks: Task[];
    permissions: CheckoutPermission[];
    count: number;
  }> {
    const order = makeOrder(sort_criteria, direction);

    const value = await this.dataSource.transaction(async (entityManager) => {
      const groupMemberRepo = entityManager.getRepository(UserGroupMember);
      // ユーザーの所属するすべてのグループを列挙
      const groups = await groupMemberRepo.find({
        relations: {
          user: true,
        },
        where: {
          user: {
            external_id: external_user_id,
          },
        },
      });
      // ユーザーの所属するすべてのグループIDを列挙
      const groupIds = groups.map((member) => member.user_group_id);

      const checkoutRepo = entityManager.getRepository(CheckoutPermission);
      // ユーザーに対するチェックアウト権限、またはユーザーの所属するユーザーグループのチェックアウト権限を取得
      const related = await checkoutRepo.find({
        relations: {
          task: true,
        },
        where: [
          {
            // ユーザーがチェックアウト可能である
            user: {
              external_id: external_user_id,
            },
          },
          {
            // ユーザーの所属するユーザーグループがチェックアウト可能である
            user_group_id: In(groupIds),
          },
        ],
      });

      // ユーザー本人、またはユーザーが所属するユーザーグループがチェックアウト可能なタスクIDの一覧を作成
      const relatedTaskIds = related.map((permission) => permission.task_id);

      const taskRepo = entityManager.getRepository(Task);

      // [✔] 自分が割り当てられている
      // [✔] または 自分が割り当て可能になっている
      // [✔] または 自分が所属しているTypistGroupが割り当て可能になっている
      // という条件のTask一覧を取得する

      // limit/offsetによらず条件に一致するすべてのレコード数を取得
      const count = await taskRepo.count({
        where: [
          {
            // Typistが割り当てられているTaskを取得
            typist_user: {
              external_id: external_user_id,
            },
            status: In(status),
          },
          {
            // TypistまたはTypistが所属するユーザーグループが割り当て可能になっているTaskを取得
            id: In(relatedTaskIds),
            status: In(status),
          },
        ],
      });

      // 条件に該当するTask一覧を取得
      const tasks = await taskRepo.find({
        relations: {
          file: true,
          option_items: true,
          typist_user: true,
        },
        where: [
          {
            // Typistが割り当てられているTaskを取得
            typist_user: {
              external_id: external_user_id,
            },
            status: In(status),
          },
          {
            // TypistまたはTypistが所属するユーザーグループが割り当て可能になっているTaskを取得
            id: In(relatedTaskIds),
            status: In(status),
          },
        ],
        order: order, // 引数によってOrderに使用するパラメータを変更
        take: limit,
        skip: offset,
      });

      // TODO [Task2249] Task内にCheckoutPermissionを含める方法が上手くいかなかった（複雑になりすぎた？ 原因未調査）ため、
      //      確実に上手くいく方法としてQueryの分割を行ったが、本来はオブジェクトの構築はTypeORMに一任したい
      const taskIds = tasks.map((x) => x.id);
      const permissions = await checkoutRepo.find({
        relations: {
          user: true,
          user_group: true,
        },
        where: {
          task_id: In(taskIds),
        },
      });
      return { tasks, permissions, count };
    });
    return value;
  }

  /**
   * 文字起こしタスクと音声ファイル、オプションアイテムを追加
   */
  async create(
    account_id: number,
    owner_user_id: number,
    priority: string,
    url: string,
    file_name: string,
    author_id: string,
    work_type_id: string,
    started_at: Date,
    duration: string,
    finished_at: Date,
    uploaded_at: Date,
    file_size: number,
    audio_format: string,
    comment: string,
    is_encrypted: boolean,
    paramOptionItems: ParamOptionItem[],
  ): Promise<Task> {
    const audioFile = new AudioFile();
    audioFile.account_id = account_id;
    audioFile.owner_user_id = owner_user_id;
    audioFile.url = url;
    audioFile.file_name = file_name;
    audioFile.author_id = author_id;
    audioFile.work_type_id = work_type_id;
    audioFile.started_at = started_at;
    audioFile.duration = duration;
    audioFile.finished_at = finished_at;
    audioFile.uploaded_at = uploaded_at;
    audioFile.file_size = file_size;
    audioFile.priority = priority;
    audioFile.audio_format = audio_format;
    audioFile.comment = comment;
    audioFile.is_encrypted = is_encrypted;

    const task = new Task();
    task.account_id = account_id;
    task.is_job_number_enabled = true;
    task.status = TASK_STATUS.UPLOADED;
    task.priority = priority;

    const createdEntity = await this.dataSource.transaction(
      async (entityManager) => {
        const audioFileRepo = entityManager.getRepository(AudioFile);
        const newAudioFile = audioFileRepo.create(audioFile);
        const savedAudioFile = await audioFileRepo.save(newAudioFile);

        task.audio_file_id = savedAudioFile.id;

        const optionItems = paramOptionItems.map((x) => {
          return {
            audio_file_id: savedAudioFile.id,
            label: x.optionItemLabel,
            value: x.optionItemValue,
          };
        });

        const optionItemRepo = entityManager.getRepository(AudioOptionItem);
        const newAudioOptionItems = optionItemRepo.create(optionItems);
        await optionItemRepo.save(newAudioOptionItems);

        const taskRepo = entityManager.getRepository(Task);

        // アカウント内でJOBナンバーが有効なタスクのうち最新のものを取得
        const lastTask = await taskRepo.findOne({
          where: { account_id: account_id, is_job_number_enabled: true },
          order: { created_at: 'DESC', job_number: 'DESC' },
        });

        let newJobNumber = '00000001';
        if (!lastTask) {
          // 初回は00000001
          newJobNumber = '00000001';
        } else if (lastTask.job_number === '99999999') {
          // 末尾なら00000001に戻る
          newJobNumber = '00000001';
        } else {
          // 最新のJOBナンバーをインクリメントして次の番号とする
          newJobNumber = `${Number(lastTask.job_number) + 1}`.padStart(8, '0');
        }
        task.job_number = newJobNumber;

        const newTask = taskRepo.create(task);
        const persisted = await taskRepo.save(newTask);
        return persisted;
      },
    );
    return createdEntity;
  }

  /**
   * Changes checkout permission
   * @param audio_file_id
   * @param assignees
   * @param accountId
   * @returns checkout permission
   */
  async changeCheckoutPermission(
    audio_file_id: number,
    author_id: string,
    account_id: number,
    roles: Roles[],
    assignees: Assignee[],
  ): Promise<void> {
    await this.dataSource.transaction(async (entityManager) => {
      // UserGroupの取得/存在確認
      const userGroupIds = assignees
        .filter((x) => x.typistGroupId !== undefined)
        .map((y) => {
          return y.typistGroupId;
        });
      const groupRepo = entityManager.getRepository(UserGroup);
      const groupRecords = await groupRepo.find({
        where: {
          id: In(userGroupIds),
          account_id: account_id,
          deleted_at: IsNull(),
        },
      });
      // idはユニークであるため取得件数の一致でグループの存在を確認
      if (userGroupIds.length !== groupRecords.length) {
        throw new TypistUserGroupNotFoundError(
          `Group not exists Error. reqUserGroupId:${userGroupIds}; resUserGroupId:${groupRecords.map(
            (x) => x.id,
          )}`,
        );
      }

      // Userの取得/存在確認
      const typistUserIds = assignees
        .filter((x) => x.typistUserId !== undefined)
        .map((y) => {
          return y.typistUserId;
        });
      const userRepo = entityManager.getRepository(User);
      const userRecords = await userRepo.find({
        where: {
          id: In(typistUserIds),
          account_id: account_id,
          role: USER_ROLES.TYPIST,
          deleted_at: IsNull(),
        },
      });
      // idはユニークであるため取得件数の一致でユーザーの存在を確認
      if (typistUserIds.length !== userRecords.length) {
        throw new TypistUserNotFoundError(
          `User not exists Error. reqUserId:${typistUserIds}; resUserId:${userRecords.map(
            (x) => x.id,
          )}`,
        );
      }

      // 引数audioFileIdを使ってTaskレコードを特定し、そのステータスを取得/存在確認
      const taskRepo = entityManager.getRepository(Task);

      const taskRecord = await taskRepo.findOne({
        where: {
          audio_file_id: audio_file_id,
          status: TASK_STATUS.UPLOADED,
          account_id: account_id,
          file: {
            author_id: roles.includes(ADMIN_ROLES.ADMIN)
              ? undefined
              : author_id,
          },
        },
      });
      //タスクが存在しない or ステータスがUploadedでなければエラー
      if (!taskRecord) {
        throw new TasksNotFoundError(
          `Task not found Error. audio_file_id:${audio_file_id}`,
        );
      }

      // 当該タスクに紐づく既存checkoutPermissionをdelete
      const checkoutPermissionRepo =
        entityManager.getRepository(CheckoutPermission);
      await checkoutPermissionRepo.delete({
        task_id: taskRecord.id,
      });

      // 当該タスクに紐づく新規checkoutPermissionをinsert
      const checkoutPermissions: CheckoutPermission[] = assignees.map(
        (assignee) => {
          const checkoutPermission = new CheckoutPermission();
          checkoutPermission.task_id = taskRecord.id;
          checkoutPermission.user_id = assignee.typistUserId;
          checkoutPermission.user_group_id = assignee.typistGroupId;
          return checkoutPermission;
        },
      );

      return await checkoutPermissionRepo.save(checkoutPermissions);
    });
  }
}

// ソート用オブジェクトを生成する
const makeOrder = (
  sort_criteria: TaskListSortableAttribute,
  direction: FindOptionsOrderValue,
): FindOptionsOrder<Task> => {
  // Priorityで最優先で昇順ソートし、
  // その後指定パラメータで任意順ソートし、
  // 最後に順序の固定のためPrimaryKeyで昇順ソートを行う
  switch (sort_criteria) {
    case 'JOB_NUMBER':
      return {
        priority: 'DESC',
        job_number: direction,
        id: 'ASC',
      };
    case 'STATUS':
      return {
        priority: 'DESC',
        status: direction,
        id: 'ASC',
      };
    case 'TRANSCRIPTION_FINISHED_DATE':
      return {
        priority: 'DESC',
        finished_at: direction,
        id: 'ASC',
      };
    case 'TRANSCRIPTION_STARTED_DATE':
      return {
        priority: 'DESC',
        started_at: direction,
        id: 'ASC',
      };
    case 'AUTHOR_ID':
      return {
        priority: 'DESC',
        file: { author_id: direction },
        id: 'ASC',
      };
    case 'ENCRYPTION':
      return {
        priority: 'DESC',
        file: { is_encrypted: direction },
        id: 'ASC',
      };
    case 'FILE_LENGTH':
      return {
        priority: 'DESC',
        file: { duration: direction },
        id: 'ASC',
      };
    case 'FILE_NAME':
      return {
        priority: 'DESC',
        file: { file_name: direction },
        id: 'ASC',
      };
    case 'FILE_SIZE':
      return {
        priority: 'DESC',
        file: { file_size: direction },
        id: 'ASC',
      };
    case 'RECORDING_FINISHED_DATE':
      return {
        priority: 'DESC',
        file: { finished_at: direction },
        id: 'ASC',
      };
    case 'RECORDING_STARTED_DATE':
      return {
        priority: 'DESC',
        file: { started_at: direction },
        id: 'ASC',
      };
    case 'UPLOAD_DATE':
      return {
        priority: 'DESC',
        file: { uploaded_at: direction },
        id: 'ASC',
      };
    case 'WORK_TYPE':
      return {
        priority: 'DESC',
        file: { work_type_id: direction },
        id: 'ASC',
      };
    default:
      // switchのcase漏れが発生した場合に型エラーになるようにする
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const _: never = sort_criteria;
      throw new Error();
  }
};
