import { Injectable } from '@nestjs/common';
import { DataSource, In, IsNull } from 'typeorm';
import { Workflow } from './entity/workflow.entity';
import { WorkflowTypist as DbWorkflowTypist } from './entity/workflow_typists.entity';
import { User } from '../users/entity/user.entity';
import { WorkflowTypist } from '../../features/workflows/types/types';
import { Worktype } from '../worktypes/entity/worktype.entity';
import { TemplateFile } from '../template_files/entity/template_file.entity';
import { UserGroup } from '../user_groups/entity/user_group.entity';
import { TypistGroupNotExistError } from '../user_groups/errors/types';
import { UserNotFoundError } from '../users/errors/types';
import { WorktypeIdNotFoundError } from '../worktypes/errors/types';
import { TemplateFileNotExistError } from '../template_files/errors/types';
import {
  AuthorIdAndWorktypeIdPairAlreadyExistsError,
  WorkflowNotFoundError,
} from './errors/types';
import {
  insertEntities,
  insertEntity,
  deleteEntity,
} from '../../common/repository';
import { Context } from '../../common/log';

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

  constructor(private dataSource: DataSource) {}

  /**
   * ワークフロー一を取得する
   * @param externalId
   * @returns worktypes and active worktype id
   */
  async getWorkflows(context: Context, accountId: number): Promise<Workflow[]> {
    return await this.dataSource.transaction(async (entityManager) => {
      const workflowRepo = entityManager.getRepository(Workflow);

      const workflows = await workflowRepo.find({
        where: { account_id: accountId },
        relations: {
          author: true,
          worktype: true,
          template: true,
          workflowTypists: {
            typist: true,
            typistGroup: true,
          },
        },
        order: {
          id: 'ASC',
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });

      return workflows;
    });
  }

  /**
   * ワークフローを作成する
   * @param accountId
   * @param authorId
   * @param worktypeId
   * @param templateId
   * @param typists
   * @returns workflows
   */
  async createtWorkflows(
    context: Context,
    accountId: number,
    authorId: number,
    typists: WorkflowTypist[],
    worktypeId?: number | undefined,
    templateId?: number | undefined,
  ): Promise<void> {
    return await this.dataSource.transaction(async (entityManager) => {
      // authorの存在確認
      const userRepo = entityManager.getRepository(User);
      const author = await userRepo.findOne({
        where: { account_id: accountId, id: authorId, email_verified: true },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      if (!author) {
        throw new UserNotFoundError(
          `author not found or email not verified. id: ${authorId}`,
        );
      }

      // worktypeの存在確認
      if (worktypeId !== undefined) {
        const worktypeRepo = entityManager.getRepository(Worktype);
        const worktypes = await worktypeRepo.find({
          where: { account_id: accountId, id: worktypeId },
          comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
        });
        if (worktypes.length === 0) {
          throw new WorktypeIdNotFoundError(
            `worktype not found. id: ${worktypeId}`,
          );
        }
      }

      // templateの存在確認
      if (templateId !== undefined) {
        const templateRepo = entityManager.getRepository(TemplateFile);
        const template = await templateRepo.findOne({
          where: { account_id: accountId, id: templateId },
          comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
        });
        if (!template) {
          throw new TemplateFileNotExistError('template not found.');
        }
      }

      // ルーティング候補ユーザーの存在確認
      const typistIds = typists.flatMap((typist) =>
        typist.typistId ? [typist.typistId] : [],
      );
      const typistUsers = await userRepo.find({
        where: {
          account_id: accountId,
          id: In(typistIds),
          email_verified: true,
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      if (typistUsers.length !== typistIds.length) {
        throw new UserNotFoundError(
          `typist not found or email not verified. ids: ${typistIds}`,
        );
      }

      // ルーティング候補ユーザーグループの存在確認
      const groupIds = typists.flatMap((typist) => {
        return typist.typistGroupId ? [typist.typistGroupId] : [];
      });
      const userGroupRepo = entityManager.getRepository(UserGroup);
      const typistGroups = await userGroupRepo.find({
        where: { account_id: accountId, id: In(groupIds) },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      if (typistGroups.length !== groupIds.length) {
        throw new TypistGroupNotExistError(
          `typist group not found. ids: ${groupIds}`,
        );
      }

      const workflowRepo = entityManager.getRepository(Workflow);

      // ワークフローの重複確認
      const workflow = await workflowRepo.find({
        where: {
          account_id: accountId,
          author_id: authorId,
          worktype_id: worktypeId !== undefined ? worktypeId : IsNull(),
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      if (workflow.length !== 0) {
        throw new AuthorIdAndWorktypeIdPairAlreadyExistsError(
          'workflow already exists.',
        );
      }

      // ワークフローのデータ作成
      const newWorkflow = this.makeWorkflow(
        accountId,
        authorId,
        worktypeId,
        templateId,
      );

      await insertEntity(
        Workflow,
        workflowRepo,
        newWorkflow,
        this.isCommentOut,
        context,
      );

      // ルーティング候補のデータ作成
      const workflowTypists = typists.map((typist) =>
        this.makeWorkflowTypist(
          newWorkflow.id,
          typist.typistId,
          typist.typistGroupId,
        ),
      );

      const workflowTypistsRepo = entityManager.getRepository(DbWorkflowTypist);
      await insertEntities(
        DbWorkflowTypist,
        workflowTypistsRepo,
        workflowTypists,
        this.isCommentOut,
        context,
      );
    });
  }

  /**
   * ワークフローを更新する
   * @param accountId
   * @param workflowId
   * @param authorId
   * @param [worktypeId]
   * @param [templateId]
   * @param [typists]
   * @returns workflow
   */
  async updatetWorkflow(
    context: Context,
    accountId: number,
    workflowId: number,
    authorId: number,
    typists: WorkflowTypist[],
    worktypeId?: number | undefined,
    templateId?: number | undefined,
  ): Promise<void> {
    return await this.dataSource.transaction(async (entityManager) => {
      const workflowRepo = entityManager.getRepository(Workflow);

      // ワークフローの存在確認
      const targetWorkflow = await workflowRepo.findOne({
        where: { account_id: accountId, id: workflowId },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      if (!targetWorkflow) {
        throw new WorkflowNotFoundError(
          `workflow not found. id: ${workflowId}`,
        );
      }

      // authorの存在確認
      const userRepo = entityManager.getRepository(User);
      const author = await userRepo.findOne({
        where: { account_id: accountId, id: authorId, email_verified: true },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      if (!author) {
        throw new UserNotFoundError(
          `author not found  or email not verified. id: ${authorId}`,
        );
      }

      // worktypeの存在確認
      if (worktypeId !== undefined) {
        const worktypeRepo = entityManager.getRepository(Worktype);
        const worktypes = await worktypeRepo.find({
          where: { account_id: accountId, id: worktypeId },
          comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
        });
        if (worktypes.length === 0) {
          throw new WorktypeIdNotFoundError(
            `worktype not found. id: ${worktypeId}`,
          );
        }
      }

      // templateの存在確認
      if (templateId !== undefined) {
        const templateRepo = entityManager.getRepository(TemplateFile);
        const template = await templateRepo.findOne({
          where: { account_id: accountId, id: templateId },
          comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
        });
        if (!template) {
          throw new TemplateFileNotExistError(
            `template not found. id: ${templateId}`,
          );
        }
      }

      // ルーティング候補ユーザーの存在確認
      const typistIds = typists.flatMap((typist) =>
        typist.typistId ? [typist.typistId] : [],
      );
      const typistUsers = await userRepo.find({
        where: {
          account_id: accountId,
          id: In(typistIds),
          email_verified: true,
        },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      if (typistUsers.length !== typistIds.length) {
        throw new UserNotFoundError(
          `typist not found or email not verified. ids: ${typistIds}`,
        );
      }

      // ルーティング候補ユーザーグループの存在確認
      const groupIds = typists.flatMap((typist) => {
        return typist.typistGroupId ? [typist.typistGroupId] : [];
      });
      const userGroupRepo = entityManager.getRepository(UserGroup);
      const typistGroups = await userGroupRepo.find({
        where: { account_id: accountId, id: In(groupIds) },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      if (typistGroups.length !== groupIds.length) {
        throw new TypistGroupNotExistError(
          `typist group not found. ids: ${groupIds}`,
        );
      }

      const workflowTypistsRepo = entityManager.getRepository(DbWorkflowTypist);

      // 既存データの削除
      await deleteEntity(
        workflowTypistsRepo,
        { workflow_id: workflowId },
        this.isCommentOut,
        context,
      );
      await deleteEntity(
        workflowRepo,
        { id: workflowId },
        this.isCommentOut,
        context,
      );

      {
        // ワークフローの重複確認
        const duplicateWorkflow = await workflowRepo.find({
          where: {
            account_id: accountId,
            author_id: authorId,
            worktype_id: worktypeId !== undefined ? worktypeId : IsNull(),
          },
          comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
        });
        if (duplicateWorkflow.length !== 0) {
          throw new AuthorIdAndWorktypeIdPairAlreadyExistsError(
            'workflow already exists.',
          );
        }
      }

      // ワークフローのデータ作成
      const newWorkflow = this.makeWorkflow(
        accountId,
        authorId,
        worktypeId,
        templateId,
      );

      await insertEntity(
        Workflow,
        workflowRepo,
        newWorkflow,
        this.isCommentOut,
        context,
      );

      // ルーティング候補のデータ作成
      const workflowTypists = typists.map((typist) =>
        this.makeWorkflowTypist(
          newWorkflow.id,
          typist.typistId,
          typist.typistGroupId,
        ),
      );

      await insertEntities(
        DbWorkflowTypist,
        workflowTypistsRepo,
        workflowTypists,
        this.isCommentOut,
        context,
      );
    });
  }

  /**
   * ワークフローを削除する
   * @param accountId
   * @param workflowId
   * @returns workflow
   */
  async deleteWorkflow(
    context: Context,
    accountId: number,
    workflowId: number,
  ): Promise<void> {
    return await this.dataSource.transaction(async (entityManager) => {
      const workflowRepo = entityManager.getRepository(Workflow);
      const workflowTypistsRepo = entityManager.getRepository(DbWorkflowTypist);

      // ワークフローの存在確認
      const workflow = await workflowRepo.findOne({
        where: { account_id: accountId, id: workflowId },
        comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
      });
      if (!workflow) {
        throw new WorkflowNotFoundError(
          `workflow not found. id: ${workflowId}`,
        );
      }
      await deleteEntity(
        workflowTypistsRepo,
        { workflow_id: workflowId },
        this.isCommentOut,
        context,
      );
      await deleteEntity(
        workflowRepo,
        { id: workflowId },
        this.isCommentOut,
        context,
      );
    });
  }

  /**
   * DBに保存するワークフローデータを作成する
   * @param accountId
   * @param authorId
   * @param worktypeId
   * @param templateId
   * @returns workflow
   */
  private makeWorkflow(
    accountId: number,
    authorId: number,
    worktypeId?: number,
    templateId?: number,
  ): Workflow {
    const workflow = new Workflow();
    workflow.account_id = accountId;
    workflow.author_id = authorId;
    workflow.worktype_id = worktypeId ?? null;
    workflow.template_id = templateId ?? null;

    return workflow;
  }

  /**
   * DBに保存するルーティング候補データを作成する
   * @param workflowId
   * @param typistId
   * @param typistGroupId
   * @returns workflow typist
   */
  private makeWorkflowTypist(
    workflowId: number,
    typistId?: number,
    typistGroupId?: number,
  ): DbWorkflowTypist {
    const workflowTypist = new DbWorkflowTypist();
    workflowTypist.workflow_id = workflowId;
    workflowTypist.typist_id = typistId ?? null;
    workflowTypist.typist_group_id = typistGroupId ?? null;

    return workflowTypist;
  }
}
