import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { WorkflowsRepositoryService } from '../../repositories/workflows/workflows.repository.service';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { Context } from '../../common/log';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { Workflow, WorkflowTypist } from './types/types';
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
import { UserNotFoundError } from '../../repositories/users/errors/types';
import { TypistGroupNotExistError } from '../../repositories/user_groups/errors/types';
import { WorktypeIdNotFoundError } from '../../repositories/worktypes/errors/types';
import { TemplateFileNotExistError } from '../../repositories/template_files/errors/types';
import {
  AuthorIdAndWorktypeIdPairAlreadyExistsError,
  WorkflowNotFoundError,
} from '../../repositories/workflows/errors/types';
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
import { Assignee } from '../tasks/types/types';

@Injectable()
export class WorkflowsService {
  private readonly logger = new Logger(WorkflowsService.name);
  constructor(
    private readonly usersRepository: UsersRepositoryService,
    private readonly workflowsRepository: WorkflowsRepositoryService,
    private readonly adB2cService: AdB2cService,
  ) {}
  /**
   * ワークフロー一覧を取得する
   * @param context
   * @param externalId
   * @returns workflows
   */
  async getWorkflows(
    context: Context,
    externalId: string,
  ): Promise<Workflow[]> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getWorkflows.name
      } | params: { externalId: ${externalId} };`,
    );
    try {
      const { account_id: accountId } =
        await this.usersRepository.findUserByExternalId(context, externalId);

      // DBからワークフロー一覧を取得
      const workflowRecords = await this.workflowsRepository.getWorkflows(
        context,
        accountId,
      );

      // ワークフロー一覧からtypistのexternalIdを取得
      const externalIds = workflowRecords.flatMap((workflow) => {
        const workflowTypists = workflow.workflowTypists?.flatMap(
          (workflowTypist) => {
            const { typist } = workflowTypist;
            return typist ? [typist.external_id] : [];
          },
        );
        return workflowTypists;
      });
      // externalIdsからundefinedを除外
      const filteredExternalIds = externalIds.flatMap((externalId) =>
        externalId ? [externalId] : [],
      );
      // externalIdsから重複を除外
      const distinctedExternalIds = [...new Set(filteredExternalIds)];

      // ADB2Cからユーザー一覧を取得
      const adb2cUsers = await this.adB2cService.getUsers(
        context,
        distinctedExternalIds,
      );

      // DBから取得したワークフロー一覧を整形
      const workflows = workflowRecords.map((workflow): Workflow => {
        const { id, author, worktype, template, workflowTypists } = workflow;
        if (!author || !author.id || !author.author_id) {
          throw new Error('author is undefined');
        }

        const authorId = { id: author.id, authorId: author.author_id };
        const worktypeId = worktype
          ? { id: worktype.id, worktypeId: worktype.custom_worktype_id }
          : undefined;
        const templateId = template
          ? { id: template.id, fileName: template.file_name }
          : undefined;

        if (!workflowTypists) {
          throw new Error('workflowTypists is undefined');
        }

        // ルーティング候補を整形
        const typists = workflowTypists.map((workflowTypist): Assignee => {
          const { typist, typistGroup } = workflowTypist;

          // typistがユーザーの場合はADB2Cからユーザー名を取得
          const typistName = typist
            ? adb2cUsers.find(
                (adb2cUser) => adb2cUser.id === typist.external_id,
              )?.displayName
            : typistGroup?.name;

          if (!typistName) {
            throw new Error('typistName is undefined');
          }

          return {
            typistUserId: typist?.id,
            typistGroupId: typistGroup?.id,
            typistName,
          };
        });

        return {
          id,
          author: authorId,
          worktype: worktypeId,
          template: templateId,
          typists,
        };
      });

      return workflows;
    } 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.getWorkflows.name}`,
      );
    }
  }

  /**
   * ワークフローを作成する
   * @param context
   * @param externalId
   * @param authorId
   * @param worktypeId
   * @param templateId
   * @param typists
   * @returns workflow
   */
  async createWorkflow(
    context: Context,
    externalId: string,
    authorId: number,
    typists: WorkflowTypist[],
    worktypeId?: number | undefined,
    templateId?: number | undefined,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.createWorkflow.name
      } | params: { ` +
        `externalId: ${externalId}, ` +
        `authorId: ${authorId}, ` +
        `worktypeId: ${worktypeId}, ` +
        `templateId: ${templateId}, ` +
        `typists: ${JSON.stringify(typists)} };`,
    );
    try {
      const { account_id: accountId } =
        await this.usersRepository.findUserByExternalId(context, externalId);

      await this.workflowsRepository.createtWorkflows(
        context,
        accountId,
        authorId,
        typists,
        worktypeId,
        templateId,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case UserNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010204'),
              HttpStatus.BAD_REQUEST,
            );
          case TypistGroupNotExistError:
            throw new HttpException(
              makeErrorResponse('E010908'),
              HttpStatus.BAD_REQUEST,
            );
          case WorktypeIdNotFoundError:
            throw new HttpException(
              makeErrorResponse('E011003'),
              HttpStatus.BAD_REQUEST,
            );
          case TemplateFileNotExistError:
            throw new HttpException(
              makeErrorResponse('E012001'),
              HttpStatus.BAD_REQUEST,
            );
          case AuthorIdAndWorktypeIdPairAlreadyExistsError:
            throw new HttpException(
              makeErrorResponse('E013001'),
              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.createWorkflow.name}`,
      );
    }
  }
  /**
   * アカウント内のワークフローを更新する
   * @param context
   * @param externalId
   * @param workflowId
   * @param authorId
   * @param [worktypeId]
   * @param [templateId]
   * @param [typists]
   * @returns workflow
   */
  async updateWorkflow(
    context: Context,
    externalId: string,
    workflowId: number,
    authorId: number,
    typists: WorkflowTypist[],
    worktypeId?: number | undefined,
    templateId?: number | undefined,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.updateWorkflow.name
      } | params: { ` +
        `externalId: ${externalId}, ` +
        `workflowId: ${workflowId}, ` +
        `authorId: ${authorId}, ` +
        `worktypeId: ${worktypeId}, ` +
        `templateId: ${templateId}, ` +
        `typists: ${JSON.stringify(typists)} };`,
    );
    try {
      const { account_id: accountId } =
        await this.usersRepository.findUserByExternalId(context, externalId);

      await this.workflowsRepository.updatetWorkflow(
        context,
        accountId,
        workflowId,
        authorId,
        typists,
        worktypeId,
        templateId,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case WorkflowNotFoundError:
            throw new HttpException(
              makeErrorResponse('E013002'),
              HttpStatus.BAD_REQUEST,
            );
          case UserNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010204'),
              HttpStatus.BAD_REQUEST,
            );
          case TypistGroupNotExistError:
            throw new HttpException(
              makeErrorResponse('E010908'),
              HttpStatus.BAD_REQUEST,
            );
          case WorktypeIdNotFoundError:
            throw new HttpException(
              makeErrorResponse('E011003'),
              HttpStatus.BAD_REQUEST,
            );
          case TemplateFileNotExistError:
            throw new HttpException(
              makeErrorResponse('E012001'),
              HttpStatus.BAD_REQUEST,
            );
          case AuthorIdAndWorktypeIdPairAlreadyExistsError:
            throw new HttpException(
              makeErrorResponse('E013001'),
              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.updateWorkflow.name}`,
      );
    }
  }

  /**
   * ワークフローを削除する
   * @param context
   * @param externalId
   * @param workflowId
   * @returns workflow
   */
  async deleteWorkflow(
    context: Context,
    externalId: string,
    workflowId: number,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.deleteWorkflow.name
      } | params: { ` +
        `externalId: ${externalId}, ` +
        `workflowId: ${workflowId} };`,
    );
    try {
      const { account } = await this.usersRepository.findUserByExternalId(
        context,
        externalId,
      );

      if (!account) {
        throw new AccountNotFoundError(
          `account not found. externalId: ${externalId}`,
        );
      }

      await this.workflowsRepository.deleteWorkflow(
        context,
        account.id,
        workflowId,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      if (e instanceof Error) {
        switch (e.constructor) {
          case UserNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010204'),
              HttpStatus.BAD_REQUEST,
            );
          case AccountNotFoundError:
            throw new HttpException(
              makeErrorResponse('E010501'),
              HttpStatus.BAD_REQUEST,
            );
          case WorkflowNotFoundError:
            throw new HttpException(
              makeErrorResponse('E013002'),
              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.deleteWorkflow.name}`,
      );
    }
  }
}
