import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
import { Assignee, Task } from './types/types';
import { Task as TaskEntity } from '../../repositories/tasks/entity/task.entity';
import { createTasks } from './types/convert';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import {
  SortDirection,
  TaskListSortableAttribute,
} from '../../common/types/sort';
import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants';
import {
  AdB2cService,
  Adb2cTooManyRequestsError,
} from '../../gateways/adb2c/adb2c.service';
import { AdB2cUser } from '../../gateways/adb2c/types/types';
import { CheckoutPermission } from '../../repositories/checkout_permissions/entity/checkout_permission.entity';
import {
  AccountNotMatchError,
  AlreadyHasInProgressTaskError,
  CheckoutPermissionNotFoundError,
  StatusNotMatchError,
  TaskAuthorIdNotMatchError,
  TasksNotFoundError,
  TypistUserGroupNotFoundError,
  TypistUserNotFoundError,
  TypistUserNotMatchError,
} from '../../repositories/tasks/errors/types';
import { Roles } from '../../common/types/role';
import { InvalidRoleError } from './errors/types';
import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service';
import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service';
import { Context } from '../../common/log';
import { User } from '../../repositories/users/entity/user.entity';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';

@Injectable()
export class TasksService {
  private readonly logger = new Logger(TasksService.name);
  constructor(
    private readonly accountsRepository: AccountsRepositoryService,
    private readonly taskRepository: TasksRepositoryService,
    private readonly usersRepository: UsersRepositoryService,
    private readonly userGroupsRepositoryService: UserGroupsRepositoryService,
    private readonly adB2cService: AdB2cService,
    private readonly sendgridService: SendGridService,
    private readonly notificationhubService: NotificationhubService,
  ) {}

  async getTasks(
    context: Context,
    userId: string,
    roles: Roles[],
    offset: number,
    limit: number,
    status?: string[],
    paramName?: TaskListSortableAttribute,
    direction?: SortDirection,
  ): Promise<{ tasks: Task[]; total: number }> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${this.getTasks.name} | params: { ` +
        `userId: ${userId}, ` +
        `roles: ${roles}, ` +
        `offset: ${offset},` +
        `limit: ${limit}, ` +
        `status: ${status}, ` +
        `paramName: ${paramName}, ` +
        `direction: ${direction} };`,
    );

    // パラメータが省略された場合のデフォルト値: 保存するソート条件の値の初期値と揃える
    const defaultParamName: TaskListSortableAttribute = 'JOB_NUMBER';
    const defaultDirection: SortDirection = 'ASC';
    // statusが省略された場合のデフォルト値: 全てのステータス
    const defaultStatus = Object.values(TASK_STATUS);

    try {
      const { account_id, author_id } =
        await this.usersRepository.findUserByExternalId(context, userId);

      if (roles.includes(ADMIN_ROLES.ADMIN)) {
        const result = await this.taskRepository.getTasksFromAccountId(
          context,
          account_id,
          offset,
          limit,
          paramName ?? defaultParamName,
          direction ?? defaultDirection,
          status ?? defaultStatus,
        );

        // B2Cからユーザー名を取得する
        const b2cUsers = await this.getB2cUsers(
          context,
          result.tasks,
          result.permissions,
        );

        const tasks = createTasks(result.tasks, result.permissions, b2cUsers);

        return { tasks: tasks, total: result.count };
      }
      if (roles.includes(USER_ROLES.AUTHOR)) {
        // API実行者がAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする
        if (!author_id) {
          throw new Error('AuthorID not found');
        }
        const result =
          await this.taskRepository.getTasksFromAuthorIdAndAccountId(
            context,
            author_id,
            account_id,
            offset,
            limit,
            paramName ?? defaultParamName,
            direction ?? defaultDirection,
            status ?? defaultStatus,
          );

        // B2Cからユーザー名を取得する
        const b2cUsers = await this.getB2cUsers(
          context,
          result.tasks,
          result.permissions,
        );

        const tasks = createTasks(result.tasks, result.permissions, b2cUsers);
        return { tasks: tasks, total: result.count };
      }

      if (roles.includes(USER_ROLES.TYPIST)) {
        const result = await this.taskRepository.getTasksFromTypistRelations(
          context,
          userId,
          offset,
          limit,
          paramName ?? defaultParamName,
          direction ?? defaultDirection,
          status ?? defaultStatus,
        );
        // B2Cからユーザー名を取得する
        const b2cUsers = await this.getB2cUsers(
          context,
          result.tasks,
          result.permissions,
        );

        const tasks = createTasks(result.tasks, result.permissions, b2cUsers);

        return { tasks: tasks, total: result.count };
      }

      throw new Error(`invalid roles: ${roles.join(',')}`);
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        if (e.constructor === Adb2cTooManyRequestsError) {
          throw new HttpException(
            makeErrorResponse('E000301'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
        }
      }
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.getTasks.name}`,
      );
    }
  }

  /**
   * 完了したタスクの次のタスクを取得します
   * @param context
   * @param externalId
   * @param fileId
   * @returns next task
   */
  async getNextTask(
    context: Context,
    externalId: string,
    fileId: number,
  ): Promise<number | undefined> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getNextTask.name
      } | params: { externalId: ${externalId}, fileId: ${fileId} };`,
    );

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

      // タスク一覧を取得する
      const tasks = await this.taskRepository.getSortedTasks(
        context,
        accountId,
        id,
        fileId,
      );

      // 指定タスクのインデックスを取得する
      const targetTaskIndex = tasks.findIndex(
        (x) => x.audio_file_id === fileId,
      );

      // 指定したタスクが見つからない場合はエラーとする（リポジトリからは必ず取得できる想定）
      if (targetTaskIndex === -1) {
        throw new TasksNotFoundError(`task not found: ${fileId}`);
      }

      // ソート順に並んだタスクについて、指定した完了済みタスクの次のタスクを取得する
      let nextTaskIndex = targetTaskIndex + 1;

      // 次のタスクがない場合は先頭のタスクを返す
      if (tasks.length - 1 < nextTaskIndex) {
        nextTaskIndex = 0;
      }

      const nextTask = tasks[nextTaskIndex];

      // 先頭のタスクが指定した完了済みタスクの場合は次のタスクがないためundefinedを返す
      return nextTask.audio_file_id === fileId
        ? undefined
        : nextTask.audio_file_id;
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case TasksNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010603'),
              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.getNextTask.name}`,
      );
    }
  }

  /**
   * 指定した音声ファイルに紐づくタスクをcheckoutする
   * @param audioFileId
   * @param roles
   * @param externalId
   * @returns checkout
   */
  async checkout(
    context: Context,
    audioFileId: number,
    roles: Roles[],
    externalId: string,
  ): Promise<void> {
    try {
      this.logger.log(
        `[IN] [${context.getTrackingId()}] ${
          this.checkout.name
        } | params: { audioFileId: ${audioFileId}, roles: ${roles}, externalId: ${externalId} };`,
      );

      const { id, account_id, author_id } =
        await this.usersRepository.findUserByExternalId(context, externalId);

      if (roles.includes(USER_ROLES.AUTHOR)) {
        // API実行者がAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする
        if (!author_id) {
          throw new Error('AuthorID not found');
        }
        await this.taskRepository.getTaskFromAudioFileId(
          context,
          audioFileId,
          account_id,
          author_id,
        );
        return;
      }

      if (roles.includes(USER_ROLES.TYPIST)) {
        return await this.taskRepository.checkout(
          context,
          audioFileId,
          account_id,
          id,
          [TASK_STATUS.UPLOADED, TASK_STATUS.PENDING, TASK_STATUS.IN_PROGRESS],
        );
      }

      throw new InvalidRoleError(`invalid roles: ${roles.join(',')}`);
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case CheckoutPermissionNotFoundError:
          case TaskAuthorIdNotMatchError:
          case InvalidRoleError:
            throw new HttpException(
              makeErrorResponse('E010602'),
              HttpStatus.BAD_REQUEST,
            );
          case TasksNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010601'),
              HttpStatus.NOT_FOUND,
            );
          case AccountNotMatchError:
          case StatusNotMatchError:
          case AlreadyHasInProgressTaskError:
            throw new HttpException(
              makeErrorResponse('E010601'),
              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.checkout.name}`,
      );
    }
  }

  /**
   * 指定した音声ファイルに紐づくタスクをcheckinする
   * @param audioFileId
   * @param externalId
   * @returns checkin
   */
  async checkin(
    context: Context,
    audioFileId: number,
    externalId: string,
  ): Promise<void> {
    try {
      this.logger.log(
        `[IN] [${context.getTrackingId()}] ${
          this.checkin.name
        } | params: { audioFileId: ${audioFileId}, externalId: ${externalId} };`,
      );
      const user = await this.usersRepository.findUserByExternalId(
        context,
        externalId,
      );

      await this.taskRepository.checkin(
        context,
        audioFileId,
        user.id,
        TASK_STATUS.IN_PROGRESS,
      );

      // メール送信処理
      try {
        // タスク情報の取得
        const task = await this.taskRepository.getTaskAndAudioFile(
          context,
          audioFileId,
          user.account_id,
          [TASK_STATUS.FINISHED],
        );
        if (!task) {
          throw new Error(
            `task not found. audioFileId: ${audioFileId}. account_id: ${user.account_id}`,
          );
        }

        // author情報の取得
        if (!task.file?.author_id) {
          throw new Error(
            `author_id not found. audioFileId: ${audioFileId}. account_id: ${user.account_id}`,
          );
        }
        const { external_id: authorExternalId } =
          await this.usersRepository.findUserByAuthorId(
            context,
            task.file.author_id,
            user.account_id,
          );

        // プライマリ管理者を取得
        const { external_id: primaryAdminExternalId } =
          await this.getPrimaryAdminUser(context, user.account_id);

        // ADB2C情報を取得する
        const usersInfo = await this.adB2cService.getUsers(context, [
          externalId,
          authorExternalId,
          primaryAdminExternalId,
        ]);

        // メール送信に必要な情報を取得
        const author = usersInfo.find((x) => x.id === authorExternalId);
        if (!author) {
          throw new Error(`author not found. id=${authorExternalId}`);
        }
        const { displayName: authorName, emailAddress: authorEmail } =
          getUserNameAndMailAddress(author);
        if (!authorEmail) {
          throw new Error(`author email not found. id=${authorExternalId}`);
        }

        const typist = usersInfo.find((x) => x.id === externalId);
        if (!typist) {
          throw new Error(`typist not found. id=${externalId}`);
        }
        const { displayName: typistName, emailAddress: typistEmail } =
          getUserNameAndMailAddress(typist);
        if (!typistEmail) {
          throw new Error(`typist email not found. id=${externalId}`);
        }

        const primaryAdmin = usersInfo.find(
          (x) => x.id === primaryAdminExternalId,
        );
        if (!primaryAdmin) {
          throw new Error(
            `primary admin not found. id=${primaryAdminExternalId}`,
          );
        }
        const { displayName: primaryAdminName } =
          getUserNameAndMailAddress(primaryAdmin);

        // メール送信
        this.sendgridService.sendMailWithU117(
          context,
          authorEmail,
          typistEmail,
          authorName,
          task.file.file_name.replace('.zip', ''),
          typistName,
          primaryAdminName,
        );
      } 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 TasksNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010603'),
              HttpStatus.NOT_FOUND,
            );
          case StatusNotMatchError:
          case TypistUserNotMatchError:
            throw new HttpException(
              makeErrorResponse('E010601'),
              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.checkin.name}`,
      );
    }
  }

  private async getPrimaryAdminUser(
    context: Context,
    accountId: number,
  ): Promise<User> {
    const accountInfo = await this.accountsRepository.findAccountById(
      context,
      accountId,
    );
    if (!accountInfo || !accountInfo.primary_admin_user_id) {
      throw new Error(`account or primary admin not found. id=${accountId}`);
    }

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

    return primaryAdmin;
  }
  /**
   * 指定した音声ファイルに紐づくタスクをキャンセルする
   * @param audioFileId
   * @param externalId
   * @param role
   * @returns cancel
   */
  async cancel(
    context: Context,
    audioFileId: number,
    externalId: string,
    role: Roles[],
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.cancel.name
      } | params: { audioFileId: ${audioFileId}, externalId: ${externalId}, role: ${role} };`,
    );
    let user: User;
    try {
      // ユーザー取得
      user = await this.usersRepository.findUserByExternalId(
        context,
        externalId,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      this.logger.log(`[OUT] [${context.getTrackingId()}] ${this.cancel.name}`);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    try {
      // roleにAdminが含まれていれば、文字起こし担当でなくてもキャンセルできるため、ユーザーIDは指定しない
      await this.taskRepository.cancel(
        context,
        audioFileId,
        [TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING],
        user.account_id,
        role.includes(ADMIN_ROLES.ADMIN) ? undefined : user.id,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case TasksNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010603'),
              HttpStatus.NOT_FOUND,
            );
          case StatusNotMatchError:
          case TypistUserNotMatchError:
            throw new HttpException(
              makeErrorResponse('E010601'),
              HttpStatus.BAD_REQUEST,
            );
          default:
            throw new HttpException(
              makeErrorResponse('E009999'),
              HttpStatus.INTERNAL_SERVER_ERROR,
            );
        }
      }
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    try {
      // キャンセルしたタスクに自動ルーティングを行う
      const { typistGroupIds, typistIds } =
        await this.taskRepository.autoRouting(
          context,
          audioFileId,
          user.account_id,
          user.author_id ?? undefined,
        );

      // 通知を送信する
      await this.sendNotify(
        context,
        typistIds,
        typistGroupIds,
        audioFileId,
        user.account_id,
      );
    } catch (e) {
      // 処理の本筋はタスクキャンセルのため自動ルーティングに失敗してもエラーにしない
      this.logger.error(
        `[${context.getTrackingId()}] Automatic routing or notification failed.`,
      );
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
    } finally {
      this.logger.log(`[OUT] [${context.getTrackingId()}] ${this.cancel.name}`);
    }
  }

  /**
   * 指定した音声ファイルに紐づくタスクをsuspendする
   * @param audioFileId
   * @param externalId
   * @returns suspend
   */
  async suspend(
    context: Context,
    audioFileId: number,
    externalId: string,
  ): Promise<void> {
    try {
      this.logger.log(
        `[IN] [${context.getTrackingId()}] ${
          this.suspend.name
        } | params: { audioFileId: ${audioFileId}, externalId: ${externalId} };`,
      );
      const { id } = await this.usersRepository.findUserByExternalId(
        context,
        externalId,
      );

      return await this.taskRepository.suspend(
        context,
        audioFileId,
        id,
        TASK_STATUS.IN_PROGRESS,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case TasksNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010603'),
              HttpStatus.NOT_FOUND,
            );
          case StatusNotMatchError:
          case TypistUserNotMatchError:
            throw new HttpException(
              makeErrorResponse('E010601'),
              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.suspend.name}`,
      );
    }
  }

  /**
   * 指定した音声ファイルに紐づくタスクをbackupする
   * @param context
   * @param audioFileId
   * @param externalId
   * @returns backup
   */
  async backup(
    context: Context,
    audioFileId: number,
    externalId: string,
  ): Promise<void> {
    try {
      this.logger.log(
        `[IN] [${context.getTrackingId()}] ${
          this.backup.name
        } | params: { audioFileId: ${audioFileId}, externalId: ${externalId} };`,
      );
      const { account_id: accountId } =
        await this.usersRepository.findUserByExternalId(context, externalId);

      await this.taskRepository.backup(context, accountId, audioFileId, [
        TASK_STATUS.FINISHED,
        TASK_STATUS.BACKUP,
      ]);
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case TasksNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010603'),
              HttpStatus.NOT_FOUND,
            );
          case StatusNotMatchError:
            throw new HttpException(
              makeErrorResponse('E010601'),
              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.backup.name}`);
    }
  }

  private async getB2cUsers(
    context: Context,
    tasks: TaskEntity[],
    permissions: CheckoutPermission[],
  ): Promise<AdB2cUser[]> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getB2cUsers.name
      } | params: { tasks: ${tasks}, permissions: ${permissions} };`,
    );
    // 割り当て候補の外部IDを列挙
    const assigneesExternalIds = permissions.flatMap((permission) =>
      permission.user ? [permission.user.external_id] : [],
    );
    // 割り当てられているタイピストの外部IDを列挙
    const typistExternalIds = tasks.flatMap((task) =>
      task.typist_user ? [task.typist_user.external_id] : [],
    );

    //重複をなくす
    const distinctedExternalIds = [
      ...new Set(assigneesExternalIds.concat(typistExternalIds)),
    ];

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

    // B2Cからユーザー名を取得する
    return await this.adB2cService.getUsers(context, distinctedExternalIds);
  }
  /**
   * 文字起こし候補を変更する
   * @param audioFileId
   * @param assignees
   * @returns checkout permission
   */
  async changeCheckoutPermission(
    context: Context,
    audioFileId: number,
    assignees: Assignee[],
    externalId: string,
    role: Roles[],
  ): Promise<void> {
    try {
      this.logger.log(
        `[IN] [${context.getTrackingId()}] ${
          this.changeCheckoutPermission.name
        } | params: { audioFileId: ${audioFileId}, assignees: ${assignees}, externalId: ${externalId}, role: ${role} };`,
      );
      const { author_id, account_id } =
        await this.usersRepository.findUserByExternalId(context, externalId);
      // RoleがAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする
      if (role.includes(USER_ROLES.AUTHOR) && !author_id) {
        throw new Error('AuthorID not found');
      }

      await this.taskRepository.changeCheckoutPermission(
        context,
        audioFileId,
        author_id ?? undefined,
        account_id,
        role,
        assignees,
      );

      // すべての割り当て候補ユーザーを取得する
      const assigneesGroupIds = assignees
        .filter((assignee) => assignee.typistGroupId)
        .flatMap((assignee) =>
          assignee.typistGroupId ? [assignee.typistGroupId] : [],
        );

      const assigneesUserIds = assignees
        .filter((assignee) => assignee.typistUserId)
        .flatMap((assignee) =>
          assignee.typistUserId ? [assignee.typistUserId] : [],
        );
      // 通知を送信する
      await this.sendNotify(
        context,
        assigneesUserIds,
        assigneesGroupIds,
        audioFileId,
        account_id,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case TypistUserNotFoundError:
          case TypistUserGroupNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010204'),
              HttpStatus.BAD_REQUEST,
            );
          case TasksNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010601'),
              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.changeCheckoutPermission.name
        }`,
      );
    }
  }

  // 通知を送信するプライベートメソッド
  private async sendNotify(
    context: Context,
    typistUserIds: number[],
    typistGroupIds: number[],
    audioFileId: number,
    accountId: number,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${this.sendNotify.name} | params: { ` +
        `typistUserIds: ${typistUserIds}, ` +
        `typistGroupIds: ${typistGroupIds}, ` +
        `audioFileId: ${audioFileId}, ` +
        `accountId: ${accountId} };`,
    );
    const groupMembers =
      await this.userGroupsRepositoryService.getGroupMembersFromGroupIds(
        context,
        typistGroupIds,
      );

    // 重複のない割り当て候補ユーザーID一覧を取得する
    const distinctUserIds = [
      ...new Set([...typistUserIds, ...groupMembers.map((x) => x.user_id)]),
    ];

    // 割り当てられたユーザーがいない場合は通知不要
    if (distinctUserIds.length === 0) {
      this.logger.log(`[${context.getTrackingId()}] No user assigned.`);
      return;
    }

    // タグを生成
    const tags = distinctUserIds.map((x) => `user_${x}`);
    this.logger.log(`[${context.getTrackingId()}] tags: ${tags}`);

    // 通知内容に含む音声ファイル情報を取得
    const { file } = await this.taskRepository.getTaskAndAudioFile(
      context,
      audioFileId,
      accountId,
      [TASK_STATUS.UPLOADED],
    );
    if (!file) {
      throw new Error('audio file not found');
    }

    // タグ対象に通知送信
    await this.notificationhubService.notify(context, tags, {
      authorId: file.author_id,
      filename: file.file_name.replace('.zip', ''),
      priority: file.priority === '00' ? 'Normal' : 'High',
      uploadedAt: file.uploaded_at.toISOString(),
    });
    this.logger.log(
      `[OUT] [${context.getTrackingId()}] ${this.sendNotify.name}`,
    );
  }
}
