import { Injectable } from '@nestjs/common';
import { DataSource, Not } from 'typeorm';
import { Worktype } from './entity/worktype.entity';
import {
  OPTION_ITEM_NUM,
  OPTION_ITEM_VALUE_TYPE,
  WORKTYPE_MAX_COUNT,
} from '../../constants';
import {
  WorktypeIdAlreadyExistsError,
  WorktypeIdInUseError,
  WorktypeIdMaxCountError,
  WorktypeIdNotFoundError,
} from './errors/types';
import { OptionItem } from './entity/option_item.entity';
import { PostWorktypeOptionItem } from '../../features/accounts/types/types';
import { AccountNotFoundError } from '../accounts/errors/types';
import { Account } from '../accounts/entity/account.entity';
import { Workflow } from '../workflows/entity/workflow.entity';
import {
  insertEntities,
  insertEntity,
  updateEntity,
  deleteEntity,
} from '../../common/repository';
import { Context } from '../../common/log';

@Injectable()
export class WorktypesRepositoryService {
  // クエリログにコメントを出力するかどうか
  private readonly isCommentOut = process.env.STAGE !== 'local';

  constructor(private dataSource: DataSource) {}

  /**
   * ワークタイプ一覧とActiveWorktypeIDを取得する
   * @param externalId
   * @returns worktypes and active worktype id
   */
  async getWorktypes(
    context: Context,
    accountId: number,
  ): Promise<{
    worktypes: Worktype[];
    active_worktype_id?: number | undefined;
  }> {
    return await this.dataSource.transaction(async (entityManager) => {
      const WorktypeRepo = entityManager.getRepository(Worktype);
      const accountRepo = entityManager.getRepository(Account);

      const account = await accountRepo.findOne({
        where: { id: accountId },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      // 運用上アカウントがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
      if (!account) {
        throw new AccountNotFoundError('Account is Not Found.');
      }

      const worktypes = await WorktypeRepo.find({
        where: { account_id: account.id },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      return {
        worktypes,
        active_worktype_id: account.active_worktype_id ?? undefined,
      };
    });
  }
  /**
   * ワークタイプを作成する
   * @param accountId
   * @param worktypeId
   * @param [description]
   */
  async createWorktype(
    context: Context,
    accountId: number,
    worktypeId: string,
    description?: string,
  ): Promise<void> {
    await this.dataSource.transaction(async (entityManager) => {
      const worktypeRepo = entityManager.getRepository(Worktype);
      const optionItemRepo = entityManager.getRepository(OptionItem);

      const duplicatedWorktype = await worktypeRepo.findOne({
        where: { account_id: accountId, custom_worktype_id: worktypeId },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      // ワークタイプIDが重複している場合はエラー
      if (duplicatedWorktype) {
        throw new WorktypeIdAlreadyExistsError(
          `WorktypeID is already exists. WorktypeID: ${worktypeId}`,
        );
      }

      const worktypeCount = await worktypeRepo.count({
        where: { account_id: accountId },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      // ワークタイプの登録数が上限に達している場合はエラー
      if (worktypeCount >= WORKTYPE_MAX_COUNT) {
        throw new WorktypeIdMaxCountError(
          `Number of worktype is exceeded the limit. MAX_COUNT: ${WORKTYPE_MAX_COUNT}, currentCount: ${worktypeCount}`,
        );
      }

      // ワークタイプを作成
      const worktype = await insertEntity(
        Worktype,
        worktypeRepo,
        {
          account_id: accountId,
          custom_worktype_id: worktypeId,
          description: description ?? null,
        },
        this.isCommentOut,
        context,
      );

      // ワークタイプに紐づくオプションアイテムを10件作成
      const newOptionItems = Array.from({ length: OPTION_ITEM_NUM }, () => {
        const optionItem = new OptionItem();
        optionItem.worktype_id = worktype.id;
        optionItem.item_label = '';
        optionItem.default_value_type = OPTION_ITEM_VALUE_TYPE.DEFAULT;
        optionItem.initial_value = '';

        return optionItem;
      });

      await insertEntities(
        OptionItem,
        optionItemRepo,
        newOptionItems,
        this.isCommentOut,
        context,
      );
    });
  }

  /**
   * ワークタイプを更新する
   * @param accountId
   * @param id ワークタイプの内部ID
   * @param worktypeId ユーザーが設定するワークタイプ名
   * @param [description]
   */
  async updateWorktype(
    context: Context,
    accountId: number,
    id: number,
    worktypeId: string,
    description?: string,
  ): Promise<void> {
    await this.dataSource.transaction(async (entityManager) => {
      const worktypeRepo = entityManager.getRepository(Worktype);

      const worktype = await worktypeRepo.findOne({
        where: { account_id: accountId, id: id },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

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

      const duplicatedWorktype = await worktypeRepo.findOne({
        where: {
          account_id: accountId,
          custom_worktype_id: worktypeId,
          id: Not(id),
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      // ワークタイプIDが重複している場合はエラー
      if (duplicatedWorktype) {
        throw new WorktypeIdAlreadyExistsError(
          `WorktypeID is already exists. WorktypeID: ${worktypeId}`,
        );
      }

      // ワークタイプを更新
      worktype.custom_worktype_id = worktypeId;
      worktype.description = description ?? null;
      await updateEntity(
        worktypeRepo,
        { id: id },
        { custom_worktype_id: worktypeId, description: description ?? null },
        this.isCommentOut,
        context,
      );
    });
  }

  /**
   * ワークタイプを削除する
   * @param accountId
   * @param id
   * @returns worktype
   */
  async deleteWorktype(
    context: Context,
    accountId: number,
    id: number,
  ): Promise<void> {
    await this.dataSource.transaction(async (entityManager) => {
      const worktypeRepo = entityManager.getRepository(Worktype);

      const worktype = await worktypeRepo.findOne({
        where: { account_id: accountId, id: id },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      // ワークタイプが存在しない場合はエラー
      if (!worktype) {
        throw new WorktypeIdNotFoundError(`Worktype is not found. id: ${id}`);
      }

      // アカウントのActiveWorktypeIDが削除対象のワークタイプIDの場合はActiveWorktypeIDをnullに更新
      const accountRepo = entityManager.getRepository(Account);
      const account = await accountRepo.findOne({
        where: { id: accountId },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      if (account?.active_worktype_id === id) {
        await updateEntity(
          accountRepo,
          { id: accountId },
          { active_worktype_id: null },
          this.isCommentOut,
          context,
        );
      }

      // ワークタイプがワークフローに紐づいている場合はエラー
      const workflowRepo = entityManager.getRepository(Workflow);
      const workflows = await workflowRepo.find({
        where: { account_id: accountId, worktype_id: id },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      if (workflows.length > 0) {
        const workflowIds = workflows.map((workflow) => workflow.id);
        throw new WorktypeIdInUseError(
          `Worktype is in use by workflow. worktype id: ${id}, workflow ids: [${workflowIds}]`,
        );
      }

      // ワークタイプに紐づくオプションアイテムを削除
      const optionItemRepo = entityManager.getRepository(OptionItem);
      await deleteEntity(
        optionItemRepo,
        { worktype_id: id },
        this.isCommentOut,
        context,
      );

      // ワークタイプを削除
      await deleteEntity(worktypeRepo, { id: id }, this.isCommentOut, context);
    });
  }

  /**
   * オプションアイテム一覧を取得する
   * @param accountId
   * @param worktypeId worktypeの内部ID
   * @returns option items
   */
  async getOptionItems(
    context: Context,
    accountId: number,
    worktypeId: number,
  ): Promise<OptionItem[]> {
    return await this.dataSource.transaction(async (entityManager) => {
      const repoWorktype = entityManager.getRepository(Worktype);
      const repoOptionItem = entityManager.getRepository(OptionItem);

      const worktype = await repoWorktype.findOne({
        where: { account_id: accountId, id: worktypeId },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

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

      const optionItems = await repoOptionItem.find({
        where: { worktype_id: worktypeId },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      return optionItems;
    });
  }

  /**
   * オプションアイテムを更新する
   * @param accountId
   * @param id ワークタイプの内部ID
   * @param optionItems
   * @returns option items
   */
  async updateOptionItems(
    context: Context,
    accountId: number,
    worktypeId: number,
    worktypeOptionItems: PostWorktypeOptionItem[],
  ): Promise<void> {
    await this.dataSource.transaction(async (entityManager) => {
      const worktypeRepo = entityManager.getRepository(Worktype);
      const optionItemRepo = entityManager.getRepository(OptionItem);

      const worktype = await worktypeRepo.findOne({
        where: { account_id: accountId, id: worktypeId },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      // ワークタイプが存在しない場合はエラー
      if (!worktype) {
        throw new WorktypeIdNotFoundError(
          `Worktype is not found. id: ${worktypeId}`,
        );
      }

      // 引数で渡されたオプションアイテムをDB保存用オプションアイテムに変換
      const optionItems = worktypeOptionItems.map((item) => {
        const optionItem = new OptionItem();
        optionItem.worktype_id = worktype.id;
        optionItem.item_label = item.itemLabel;
        optionItem.default_value_type = item.defaultValueType;
        optionItem.initial_value = item.initialValue;

        return optionItem;
      });

      // ワークタイプに紐づくオプションアイテムを削除してから再登録
      await deleteEntity(
        optionItemRepo,
        { worktype_id: worktypeId },
        this.isCommentOut,
        context,
      );
      await insertEntities(
        OptionItem,
        optionItemRepo,
        optionItems,
        this.isCommentOut,
        context,
      );
    });
  }
}
