import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { AccessToken } from '../../common/token';
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,
  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';

@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 blobStorageService: BlobstorageService,
  ) {}

  /**
   * 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.trackingId}] ${this.uploadFinished.name} | params: { ` +
        `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(
          `param createdDate is invalid format:[createdDate=${createdDate}]`,
        );
      }
      if (isInvalidFinishedDate) {
        this.logger.error(
          `param finishedDate is invalid format:[finishedDate=${finishedDate}]`,
        );
      }
      if (isInvalidUploadedDate) {
        this.logger.error(
          `param uploadedDate is invalid format:[uploadedDate=${uploadedDate}]`,
        );
      }
      this.logger.log(
        `[OUT] [${context.trackingId}] ${this.uploadFinished.name}`,
      );

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

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

    let user: User;
    try {
      // ユーザー取得
      user = await this.usersRepository.findUserByExternalId(userId);
    } catch (e) {
      this.logger.error(`error=${e}`);
      this.logger.log(
        `[OUT] [${context.trackingId}] ${this.uploadFinished.name}`,
      );
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

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

      // 文字起こしタスク追加（音声ファイルとオプションアイテムも同時に追加）
      // 追加時に末尾のJOBナンバーにインクリメントする
      const task = await this.tasksRepositoryService.create(
        user.account_id,
        user.id,
        priority,
        fileUrl,
        fileName,
        authorId,
        workType,
        formattedCreatedDate,
        duration,
        formattedFinishedDate,
        formattedUploadedDate,
        fileSize,
        audioFormat,
        comment,
        isEncrypted,
        optionItemList,
      );
      return { jobNumber: task.job_number };
    } catch (e) {
      this.logger.error(`error=${e}`);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(
        `[OUT] [${context.trackingId}] ${this.uploadFinished.name}`,
      );
    }
  }

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

    //DBから国情報とアカウントIDを取得する
    let accountId: number;
    let country: string;
    let userId: number;
    try {
      const user = await this.usersRepository.findUserByExternalId(
        token.userId,
      );
      accountId = user.account.id;
      userId = user.id;
      country = user.account.country;
    } catch (e) {
      this.logger.error(`error=${e}`);
      this.logger.log(
        `[OUT] [${context.trackingId}] ${this.publishUploadSas.name}`,
      );
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    try {
      // 国に応じたリージョンのBlobストレージにコンテナが存在するか確認
      const isContainerExist = await this.blobStorageService.containerExists(
        context,
        accountId,
        country,
      );
      //TODO [Task2241] コンテナが無ければ作成しているが、アカウント登録時に作成するので本処理は削除予定。
      if (!isContainerExist) {
        await this.blobStorageService.createContainer(accountId, country);
      }
    } catch (e) {
      this.logger.error(`error=${e}`);
      this.logger.log(
        `[OUT] [${context.trackingId}] ${this.publishUploadSas.name}`,
      );
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    try {
      // SASトークン発行
      const url = await this.blobStorageService.publishUploadSas(
        context,
        accountId,
        userId,
        country,
      );
      return url;
    } catch (e) {
      this.logger.error(`error=${e}`);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(
        `[OUT] [${context.trackingId}] ${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.trackingId}] ${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;
    try {
      const user = await this.usersRepository.findUserByExternalId(externalId);
      accountId = user.account.id;
      userId = user.id;
      userId = user.id;
      country = user.account.country;
      isTypist = user.role === USER_ROLES.TYPIST;
      authorId = user.author_id;
    } catch (e) {
      this.logger.error(`error=${e}`);

      this.logger.log(
        `[OUT] [${context.trackingId}] ${this.publishAudioFileDownloadSas.name}`,
      );
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    try {
      const status = isTypist
        ? [TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING]
        : Object.values(TASK_STATUS);

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

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

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

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

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

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

      if (!isFileExist) {
        this.logger.log(`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(`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.trackingId}] ${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.trackingId}] ${this.publishTemplateFileDownloadSas.name} | params: { externalId: ${externalId}, audioFileId: ${audioFileId} };`,
    );

    //DBから国情報とアカウントID,ユーザーIDを取得する
    let accountId: number;
    let userId: number;
    let country: string;
    let isTypist: boolean;
    let authorId: string;
    try {
      const user = await this.usersRepository.findUserByExternalId(externalId);
      accountId = user.account.id;
      userId = user.id;
      country = user.account.country;
      isTypist = user.role === USER_ROLES.TYPIST;
      authorId = user.author_id;
    } catch (e) {
      this.logger.error(`error=${e}`);
      this.logger.log(
        `[OUT] [${context.trackingId}] ${this.publishTemplateFileDownloadSas.name}`,
      );
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    try {
      const status = isTypist
        ? [TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING]
        : Object.values(TASK_STATUS);

      const task = await this.tasksRepository.getTaskAndAudioFile(
        audioFileId,
        accountId,
        status,
      );

      const template_file = task.template_file;

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

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

      // ユーザーがTypistの場合、自身が担当したタスクでない場合はエラー
      if (isTypist && task.typist_user_id !== userId) {
        throw new AuthorUserNotMatchError(
          `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(`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 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.trackingId}] ${this.publishTemplateFileDownloadSas.name}`,
      );
    }
  }
}
