import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import { AudioOptionItem, AudioUploadFinishedResponse } from './types/types';
import {
  OPTION_ITEM_NUM,
  TASK_STATUS,
  TIERS,
  USER_ROLES,
} from '../../constants/index';
import { User } from '../../repositories/users/entity/user.entity';
import {
  AudioFileNotFoundError,
  AuthorUserNotMatchError,
  TemplateFileNotFoundError,
} from './errors/types';
import {
  AccountNotMatchError,
  StatusNotMatchError,
  TasksNotFoundError,
  TypistUserNotFoundError,
} from '../../repositories/tasks/errors/types';
import { Context } from '../../common/log';
import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service';
import {
  AccountNotFoundError,
  AccountLockedError,
} from '../../repositories/accounts/errors/types';
import { Task } from '../../repositories/tasks/entity/task.entity';
import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service';
import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service';
import {
  LicenseExpiredError,
  LicenseNotAllocatedError,
} from '../../repositories/licenses/errors/types';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';

@Injectable()
export class FilesService {
  private readonly logger = new Logger(FilesService.name);
  constructor(
    private readonly usersRepository: UsersRepositoryService,
    private readonly tasksRepository: TasksRepositoryService,
    private readonly tasksRepositoryService: TasksRepositoryService,
    private readonly templateFilesRepository: TemplateFilesRepositoryService,
    private readonly blobStorageService: BlobstorageService,
    private readonly userGroupsRepositoryService: UserGroupsRepositoryService,
    private readonly notificationhubService: NotificationhubService,
    private readonly licensesRepository: LicensesRepositoryService,
  ) {}

  /**
   * Uploads finished
   * @param url アップロード先Blob Storage(ファイル名含む)
   * @param authorId 自分自身(ログイン認証)したAuthorID
   * @param fileName 音声ファイル名
   * @param duration 音声ファイルの録音時間（ミリ秒の整数値）
   * @param createdDate 音声ファイルの録音作成日時(開始日時)（yyyy-mm-ddThh:mm:ss.sss）'
   * @param finishedDate 音声ファイルの録音作成終了日時（yyyy-mm-ddThh:mm:ss.sss）
   * @param uploadedDate 音声ファイルのアップロード日時（yyyy-mm-ddThh:mm:ss.sss）
   * @param fileSize 音声ファイルのファイルサイズ（Byte）
   * @param priority 優先度 "00":Normal / "01":High
   * @param audioFormat 録音形式: DSS/DS2(SP)/DS2(QP)
   * @param comment コメント
   * @param workType WorkType
   * @param optionItemList オプションアイテム（音声メタデータ）10個固定
   * @param isEncrypted 暗号化されているか
   * @returns finished
   */
  async uploadFinished(
    context: Context,
    userId: string,
    url: string,
    authorId: string,
    fileName: string,
    duration: string,
    createdDate: string,
    finishedDate: string,
    uploadedDate: string,
    fileSize: number,
    priority: string,
    audioFormat: string,
    comment: string,
    workType: string,
    optionItemList: AudioOptionItem[],
    isEncrypted: boolean,
  ): Promise<AudioUploadFinishedResponse> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.uploadFinished.name
      } | params: { ` +
        `userId: ${userId}, ` +
        `url: ${url}, ` +
        `authorId: ${authorId}, ` +
        `fileName: ${fileName}, ` +
        `duration: ${duration}, ` +
        `createdDate: ${createdDate}, ` +
        `finishedDate: ${finishedDate}, ` +
        `uploadedDate: ${uploadedDate}, ` +
        `fileSize: ${fileSize}, ` +
        `priority: ${priority}, ` +
        `audioFormat: ${audioFormat}, ` +
        `comment: ${comment}, ` +
        `workType: ${workType}, ` +
        `optionItemList: ${JSON.stringify(optionItemList)}, ` +
        `isEncrypted: ${isEncrypted} };`,
    );

    const formattedCreatedDate = new Date(createdDate);
    const formattedFinishedDate = new Date(finishedDate);
    const formattedUploadedDate = new Date(uploadedDate);

    const isInvalidCreatedDate = isNaN(formattedCreatedDate.getTime());
    const isInvalidFinishedDate = isNaN(formattedFinishedDate.getTime());
    const isInvalidUploadedDate = isNaN(formattedUploadedDate.getTime());

    // 日付フォーマットが不正ならパラメータ不正
    if (
      isInvalidCreatedDate ||
      isInvalidFinishedDate ||
      isInvalidUploadedDate
    ) {
      if (isInvalidCreatedDate) {
        this.logger.error(
          `[${context.getTrackingId()}] param createdDate is invalid format:[createdDate=${createdDate}]`,
        );
      }
      if (isInvalidFinishedDate) {
        this.logger.error(
          `[${context.getTrackingId()}] param finishedDate is invalid format:[finishedDate=${finishedDate}]`,
        );
      }
      if (isInvalidUploadedDate) {
        this.logger.error(
          `[${context.getTrackingId()}] param uploadedDate is invalid format:[uploadedDate=${uploadedDate}]`,
        );
      }
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.uploadFinished.name}`,
      );

      throw new HttpException(
        makeErrorResponse('E010001'),
        HttpStatus.BAD_REQUEST,
      );
    }

    // オプションアイテムが10個ない場合はパラメータ不正
    if (optionItemList.length !== OPTION_ITEM_NUM) {
      this.logger.error(
        `[${context.getTrackingId()}] param optionItemList expects ${OPTION_ITEM_NUM} items, but has ${
          optionItemList.length
        } items`,
      );
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.uploadFinished.name}`,
      );
      throw new HttpException(
        makeErrorResponse('E010001'),
        HttpStatus.BAD_REQUEST,
      );
    }

    let user: User;
    try {
      // ユーザー取得
      user = await this.usersRepository.findUserByExternalId(context, userId);
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.uploadFinished.name}`,
      );
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
    let task: Task;
    try {
      // URLにSASトークンがついている場合は取り除く
      const urlObj = new URL(url);
      urlObj.search = '';
      const fileUrl = urlObj.toString();
      this.logger.log(
        `[${context.getTrackingId()}] Request URL: ${url}, Without param URL${fileUrl}`,
      );

      // 文字起こしタスク追加（音声ファイルとオプションアイテムも同時に追加）
      // 追加時に末尾のJOBナンバーにインクリメントする
      task = await this.tasksRepositoryService.create(
        context,
        user.account_id,
        user.id,
        priority,
        fileUrl,
        fileName,
        authorId,
        workType,
        formattedCreatedDate,
        duration,
        formattedFinishedDate,
        formattedUploadedDate,
        fileSize,
        audioFormat,
        comment,
        isEncrypted,
        optionItemList,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
    try {
      // ルーティング設定に従い、チェックアウト権限を付与する
      const { typistGroupIds, typistIds } =
        await this.tasksRepositoryService.autoRouting(
          context,
          task.audio_file_id,
          user.account_id,
          user.author_id ?? undefined,
        );

      const groupMembers =
        await this.userGroupsRepositoryService.getGroupMembersFromGroupIds(
          context,
          typistGroupIds,
        );

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

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

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

      // タグ対象に通知送信
      await this.notificationhubService.notify(context, tags, {
        authorId: authorId,
        filename: fileName.replace('.zip', ''),
        priority: priority === '00' ? 'Normal' : 'High',
        uploadedAt: uploadedDate,
      });

      // 追加したタスクのJOBナンバーを返却
      return { jobNumber: task.job_number };
    } catch (error) {
      // 処理の本筋はタスク生成のため自動ルーティングに失敗してもエラーにしない
      this.logger.error(
        `[${context.getTrackingId()}] Automatic routing or notification failed.`,
      );
      this.logger.error(`[${context.getTrackingId()}] error=${error}`);
      return { jobNumber: task.job_number };
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.uploadFinished.name}`,
      );
    }
  }

  /**
   * Publishs upload sas
   * @param companyName
   * @returns upload sas
   */
  async publishUploadSas(
    context: Context,
    externalId: string,
  ): Promise<string> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.publishUploadSas.name
      } | params: { externalId: ${externalId} };`,
    );

    //DBから国情報とアカウントIDを取得する
    try {
      const user = await this.usersRepository.findUserByExternalId(
        context,
        externalId,
      );
      if (!user.account) {
        throw new AccountNotFoundError('account not found.');
      }
      const accountId = user.account_id;
      const country = user.account.country;
      // 第五階層のみチェック
      if (user.account.tier === TIERS.TIER5) {
        // アカウントがロックされている場合、エラー
        if (user.account.locked) {
          throw new AccountLockedError('account is locked.');
        }

        // ライセンスが有効でない場合、エラー
        const { state } = await this.licensesRepository.getLicenseState(
          context,
          user.id,
        );
        if (state === 'expired') {
          throw new LicenseExpiredError('license is expired.');
        }
        if (state === 'inallocated') {
          throw new LicenseNotAllocatedError('license is not allocated.');
        }
      }
      // 国に応じたリージョンのBlobストレージにコンテナが存在するか確認
      await this.blobStorageService.containerExists(
        context,
        accountId,
        country,
      );

      // SASトークン発行
      const url = await this.blobStorageService.publishUploadSas(
        context,
        accountId,
        country,
      );
      return url;
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      switch (e.constructor) {
        case AccountLockedError:
          throw new HttpException(
            makeErrorResponse('E010504'),
            HttpStatus.BAD_REQUEST,
          );
        case LicenseExpiredError:
          throw new HttpException(
            makeErrorResponse('E010805'),
            HttpStatus.BAD_REQUEST,
          );
        case LicenseNotAllocatedError:
          throw new HttpException(
            makeErrorResponse('E010812'),
            HttpStatus.BAD_REQUEST,
          );
        default:
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
      }
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.publishUploadSas.name}`,
      );
    }
  }

  /**
   * 指定したIDの音声ファイルのダウンロードURLを取得する
   * @param externalId
   * @param audioFileId
   * @returns audio file download sas
   */
  async publishAudioFileDownloadSas(
    context: Context,
    externalId: string,
    audioFileId: number,
  ): Promise<string> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.publishAudioFileDownloadSas.name
      } | params: { externalId: ${externalId}, audioFileId: ${audioFileId} };`,
    );

    //DBから国情報とアカウントID,ユーザーIDを取得する
    let accountId: number;
    let userId: number;
    let country: string;
    let isTypist: boolean;
    let authorId: string | undefined;
    let isAdmin: boolean;
    try {
      const user = await this.usersRepository.findUserByExternalId(
        context,
        externalId,
      );
      if (!user.account) {
        throw new AccountNotFoundError('account not found.');
      }
      // 第五階層のみチェック
      if (user.account.tier === TIERS.TIER5) {
        // ライセンスが有効でない場合、エラー
        const { state } = await this.licensesRepository.getLicenseState(
          context,
          user.id,
        );
        if (state === 'expired') {
          throw new LicenseExpiredError('license is expired.');
        }
        if (state === 'inallocated') {
          throw new LicenseNotAllocatedError('license is not allocated.');
        }
      }
      accountId = user.account.id;
      userId = user.id;
      country = user.account.country;
      isTypist = user.role === USER_ROLES.TYPIST;
      authorId = user.author_id ?? undefined;
      isAdmin =
        user.account.primary_admin_user_id === user.id ||
        user.account.secondary_admin_user_id === user.id;
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${
          this.publishAudioFileDownloadSas.name
        }`,
      );
      switch (e.constructor) {
        case LicenseExpiredError:
          throw new HttpException(
            makeErrorResponse('E010805'),
            HttpStatus.BAD_REQUEST,
          );
        case LicenseNotAllocatedError:
          throw new HttpException(
            makeErrorResponse('E010812'),
            HttpStatus.BAD_REQUEST,
          );
        default:
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
      }
    }

    try {
      // ユーザーが管理者でないTypistの場合はステータスがIN_PROGRESSかPENDINGのタスクのみ取得できる
      const status =
        !isAdmin && isTypist
          ? [TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING]
          : Object.values(TASK_STATUS);

      const task = await this.tasksRepository.getTaskAndAudioFile(
        context,
        audioFileId,
        accountId,
        status,
      );
      const { file } = task;

      // タスクに紐づく音声ファイルだけが消される場合がある。
      // その場合はダウンロード不可なので不在エラーとして扱う
      if (!file) {
        throw new AudioFileNotFoundError(
          `Audio file is not exists in DB. audio_file_id:${audioFileId}`,
        );
      }

      // ユーザーがAdminの場合は特にチェックしない
      if (!isAdmin) {
        // ユーザーがAuthorの場合、自身が追加したタスクでない場合はエラー
        if (!isTypist && file.author_id !== authorId) {
          throw new AuthorUserNotMatchError(
            `task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${file.author_id}, authorId:${authorId}`,
          );
        }

        // ユーザーがTypistの場合、自身が担当したタスクでない場合はエラー
        if (isTypist && task.typist_user_id !== userId) {
          throw new TypistUserNotFoundError(
            `task typist is not match. audio_file_id:${audioFileId}, task.typist_user_id:${task.typist_user_id}, userId:${userId}`,
          );
        }
      }

      const filePath = `${file.file_name}`;

      const isFileExist = await this.blobStorageService.fileExists(
        context,
        accountId,
        country,
        filePath,
      );

      if (!isFileExist) {
        this.logger.log(`[${context.getTrackingId()}] filePath:${filePath}`);
        throw new AudioFileNotFoundError(
          `Audio file is not exists in blob storage. audio_file_id:${audioFileId}, url:${file.url}, fileName:${file.file_name}`,
        );
      }

      // SASトークン発行
      const url = await this.blobStorageService.publishDownloadSas(
        context,
        accountId,
        country,
        filePath,
      );
      return url;
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case TasksNotFoundError:
          case AccountNotMatchError:
          case StatusNotMatchError:
          case AuthorUserNotMatchError:
          case TypistUserNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010603'),
              HttpStatus.BAD_REQUEST,
            );
          case AudioFileNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010701'),
              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.publishAudioFileDownloadSas.name
        }`,
      );
    }
  }

  /**
   * 指定したIDの音声ファイルに紐づいた文字起こしテンプレートファイルのダウンロードURLを取得する
   * @param externalId
   * @param audioFileId
   * @returns template file download sas
   */
  async publishTemplateFileDownloadSas(
    context: Context,
    externalId: string,
    audioFileId: number,
  ): Promise<string> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.publishTemplateFileDownloadSas.name
      } | params: { externalId: ${externalId}, audioFileId: ${audioFileId} };`,
    );

    //DBから国情報とアカウントID,ユーザーIDを取得する
    let accountId: number;
    let userId: number;
    let country: string;
    try {
      const user = await this.usersRepository.findUserByExternalId(
        context,
        externalId,
      );
      if (!user.account) {
        throw new AccountNotFoundError('account not found.');
      }
      // 第五階層のみチェック
      if (user.account.tier === TIERS.TIER5) {
        // ライセンスが有効でない場合、エラー
        const { state } = await this.licensesRepository.getLicenseState(
          context,
          user.id,
        );
        if (state === 'expired') {
          throw new LicenseExpiredError('license is expired.');
        }
        if (state === 'inallocated') {
          throw new LicenseNotAllocatedError('license is not allocated.');
        }
      }
      accountId = user.account_id;
      userId = user.id;
      country = user.account.country;
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${
          this.publishTemplateFileDownloadSas.name
        }`,
      );
      switch (e.constructor) {
        case LicenseExpiredError:
          throw new HttpException(
            makeErrorResponse('E010805'),
            HttpStatus.BAD_REQUEST,
          );
        case LicenseNotAllocatedError:
          throw new HttpException(
            makeErrorResponse('E010812'),
            HttpStatus.BAD_REQUEST,
          );
        default:
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
      }
    }

    try {
      const task = await this.tasksRepository.getTaskAndAudioFile(
        context,
        audioFileId,
        accountId,
        [TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING],
      );
      const { template_file } = task;

      // タスクに紐づくテンプレートファイルがない場合がある。
      // その場合はダウンロード不可なので不在エラーとして扱う
      if (!template_file) {
        throw new TemplateFileNotFoundError(
          `Template file is not exists in DB. audio_file_id:${audioFileId}`,
        );
      }

      // ユーザー自身が担当したタスクでない場合はエラー
      if (task.typist_user_id !== userId) {
        throw new TypistUserNotFoundError(
          `task typist is not match. audio_file_id:${audioFileId}, task.typist_user_id:${task.typist_user_id}, userId:${userId}`,
        );
      }

      const filePath = `Templates/${template_file.file_name}`;

      const isFileExist = await this.blobStorageService.fileExists(
        context,
        accountId,
        country,
        filePath,
      );

      if (!isFileExist) {
        throw new TemplateFileNotFoundError(
          `Template file is not exists in blob storage. audio_file_id:${audioFileId}, url:${template_file.url}, fileName:${template_file.file_name}`,
        );
      }

      // SASトークン発行
      const url = await this.blobStorageService.publishDownloadSas(
        context,
        accountId,
        country,
        filePath,
      );
      return url;
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case TasksNotFoundError:
          case AccountNotMatchError:
          case StatusNotMatchError:
          case TypistUserNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010603'),
              HttpStatus.BAD_REQUEST,
            );
          case AudioFileNotFoundError:
          case TemplateFileNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010701'),
              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.publishTemplateFileDownloadSas.name
        }`,
      );
    }
  }

  /**
   * ログインユーザーアカウントのテンプレートファイルのアップロードURLを取得する
   * @param context
   * @param externalId
   * @returns template file upload sas
   */
  async publishTemplateFileUploadSas(
    context: Context,
    externalId: string,
  ): Promise<string> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.publishTemplateFileUploadSas.name
      } | params: { externalId: ${externalId} };`,
    );
    try {
      const { account } = await this.usersRepository.findUserByExternalId(
        context,
        externalId,
      );
      if (!account) {
        throw new AccountNotFoundError('account not found.');
      }

      // 国に応じたリージョンのBlobストレージにコンテナが存在するか確認
      const isContainerExists = await this.blobStorageService.containerExists(
        context,
        account.id,
        account.country,
      );
      if (!isContainerExists) {
        throw new Error('container not found.');
      }

      // SASトークン発行
      const url = await this.blobStorageService.publishTemplateUploadSas(
        context,
        account.id,
        account.country,
      );

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

  /**
   * テンプレートファイルのアップロード後にDBにテンプレートファイル情報を登録する
   * @param context
   * @param externalId
   * @param url
   * @param fileName
   * @returns upload finished
   */
  async templateUploadFinished(
    context: Context,
    externalId: string,
    url: string,
    fileName: string,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.templateUploadFinished.name
      } | params: { externalId: ${externalId}, url: ${url}, fileName: ${fileName} };`,
    );

    try {
      // ユーザー取得
      const { account_id: accountId } =
        await this.usersRepository.findUserByExternalId(context, externalId);

      // URLにSASトークンがついている場合は取り除く;
      const urlObj = new URL(url);
      urlObj.search = '';
      const fileUrl = urlObj.toString();
      this.logger.log(
        `[${context.getTrackingId()}] Request URL: ${url}, Without param URL${fileUrl}`,
      );

      // テンプレートファイル情報をDBに登録
      await this.templateFilesRepository.upsertTemplateFile(
        context,
        accountId,
        fileName,
        fileUrl,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${
          this.templateUploadFinished.name
        }`,
      );
    }
  }
}
