import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
import {
  PoNumberAlreadyExistError,
  LicenseNotExistError,
  LicenseKeyAlreadyActivatedError,
  CancelOrderFailedError,
} from '../../repositories/licenses/errors/types';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
import { UserNotFoundError } from '../../repositories/users/errors/types';
import {
  GetAllocatableLicensesResponse,
  IssueCardLicensesResponse,
} from './types/types';
import { Context } from '../../common/log';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils';
import { LICENSE_ISSUE_STATUS } from '../../constants';

@Injectable()
export class LicensesService {
  constructor(
    private readonly usersRepository: UsersRepositoryService,
    private readonly accountsRepository: AccountsRepositoryService,
    private readonly licensesRepository: LicensesRepositoryService,
    private readonly adb2cService: AdB2cService,
    private readonly sendgridService: SendGridService,
  ) {}
  private readonly logger = new Logger(LicensesService.name);

  /**
   * license Orders
   * @param token
   * @param body
   */
  async licenseOrders(
    context: Context,
    externalId: string,
    poNumber: string,
    orderCount: number,
  ): Promise<void> {
    //アクセストークンからユーザーIDを取得する
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.licenseOrders.name
      } | params: { externalId: ${externalId}, poNumber: ${poNumber}, orderCount: ${orderCount} };`,
    );
    let myAccountId: number;
    let parentAccountId: number | undefined;

    // ユーザIDからアカウントIDを取得する
    try {
      myAccountId = (
        await this.usersRepository.findUserByExternalId(context, externalId)
      ).account_id;
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      switch (e.constructor) {
        case UserNotFoundError:
          throw new HttpException(
            makeErrorResponse('E010204'),
            HttpStatus.BAD_REQUEST,
          );
        default:
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
      }
    }

    // 親アカウントIDを取得
    try {
      parentAccountId =
        (await this.accountsRepository.findAccountById(context, myAccountId))
          .parent_account_id ?? undefined;
      // 親アカウントIDが取得できない場合はエラー
      if (parentAccountId === undefined) {
        throw new Error('parent account id is undefined');
      }
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      switch (e.constructor) {
        case AccountNotFoundError:
          throw new HttpException(
            makeErrorResponse('E010501'),
            HttpStatus.BAD_REQUEST,
          );
        default:
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
      }
    }

    try {
      await this.licensesRepository.order(
        context,
        poNumber,
        myAccountId,
        parentAccountId,
        orderCount,
      );

      // メール送信処理
      try {
        const customer = await this.getAccountInformation(context, myAccountId);
        const dealer = await this.getAccountInformation(
          context,
          parentAccountId,
        );

        this.sendgridService.sendMailWithU105(
          context,
          customer.adminEmails,
          customer.companyName,
          orderCount,
          poNumber,
          dealer.adminEmails,
          dealer.companyName,
        );
      } catch (e) {
        this.logger.error(`[${context.getTrackingId()}] error=${e}`);
        // メール送信に関する例外はログだけ出して握りつぶす
      }
    } catch (e) {
      switch (e.constructor) {
        case PoNumberAlreadyExistError:
          throw new HttpException(
            makeErrorResponse('E010401'),
            HttpStatus.BAD_REQUEST,
          );
        default:
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
      }
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.licenseOrders.name}`,
      );
    }
  }
  async issueCardLicenseKeys(
    context: Context,
    externalId: string,
    createCount: number,
  ): Promise<IssueCardLicensesResponse> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.issueCardLicenseKeys.name
      } | params: { externalId: ${externalId}, createCount: ${createCount} };`,
    );
    const issueCardLicensesResponse = new IssueCardLicensesResponse();
    let myAccountId: number;

    // ユーザIDからアカウントIDを取得する
    try {
      myAccountId = (
        await this.usersRepository.findUserByExternalId(context, externalId)
      ).account_id;
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      switch (e.constructor) {
        case UserNotFoundError:
          throw new HttpException(
            makeErrorResponse('E010204'),
            HttpStatus.BAD_REQUEST,
          );
        default:
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
      }
    }
    try {
      const licenseKeys = await this.licensesRepository.createCardLicenses(
        context,
        myAccountId,
        createCount,
      );
      issueCardLicensesResponse.cardLicenseKeys = licenseKeys;
      return issueCardLicensesResponse;
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      this.logger.error(
        `[${context.getTrackingId()}] get cardlicensekeys failed`,
      );
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.issueCardLicenseKeys.name}`,
      );
    }
  }

  /**
   * card license activate
   * @param externalId
   * @param cardLicenseKey
   */
  async activateCardLicenseKey(
    context: Context,
    externalId: string,
    cardLicenseKey: string,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.activateCardLicenseKey.name
      } | params: { externalId: ${externalId}, argCardLicenseKey: ${cardLicenseKey} };`,
    );
    let myAccountId: number;

    // ユーザIDからアカウントIDを取得する
    try {
      myAccountId = (
        await this.usersRepository.findUserByExternalId(context, externalId)
      ).account_id;
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      switch (e.constructor) {
        case UserNotFoundError:
          throw new HttpException(
            makeErrorResponse('E010204'),
            HttpStatus.BAD_REQUEST,
          );
        default:
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
      }
    }

    // カードライセンスを取り込む
    try {
      await this.licensesRepository.activateCardLicense(
        context,
        myAccountId,
        cardLicenseKey,
      );
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      this.logger.error(
        `[${context.getTrackingId()}] cardLicenseKey activate failed`,
      );

      switch (e.constructor) {
        case LicenseNotExistError:
          throw new HttpException(
            makeErrorResponse('E010801'),
            HttpStatus.BAD_REQUEST,
          );
        case LicenseKeyAlreadyActivatedError:
          throw new HttpException(
            makeErrorResponse('E010802'),
            HttpStatus.BAD_REQUEST,
          );
        default:
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
      }
    }
    this.logger.log(
      `[OUT] [${context.getTrackingId()}] ${this.activateCardLicenseKey.name}`,
    );
    return;
  }

  /**
   * get allocatable lisences
   * @param context
   * @param userId
   * @@returns AllocatableLicenseInfo[]
   */
  async getAllocatableLicenses(
    context: Context,
    userId: string,
  ): Promise<GetAllocatableLicensesResponse> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.getAllocatableLicenses.name
      } | params: { ` + `userId: ${userId}, `,
    );
    // ユーザIDからアカウントIDを取得する
    try {
      const myAccountId = (
        await this.usersRepository.findUserByExternalId(context, userId)
      ).account_id;
      // 割り当て可能なライセンスを取得する
      const allocatableLicenses =
        await this.licensesRepository.getAllocatableLicenses(
          context,
          myAccountId,
        );

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

  /**
   * ライセンス注文をキャンセルする
   * @param context
   * @param externalId
   * @param poNumber
   */
  async cancelOrder(
    context: Context,
    externalId: string,
    poNumber: string,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${
        this.cancelOrder.name
      } | params: { ` +
        `externalId: ${externalId}, ` +
        `poNumber: ${poNumber}, };`,
    );
    let myAccountId: number;

    try {
      // ユーザIDからアカウントIDを取得する
      myAccountId = (
        await this.usersRepository.findUserByExternalId(context, externalId)
      ).account_id;
      // 注文キャンセル処理
      const { canceledOrderId } = await this.licensesRepository.cancelOrder(
        context,
        myAccountId,
        poNumber,
      );

      // メール送信処理
      try {
        // 注文キャンセルを実行したカスタマー企業の管理者情報を取得する
        const customerAccountId = myAccountId;

        // カスタマー企業の企業名と管理者情報を取得する
        const {
          companyName: customerCompanyName,
          adminEmails: customerAdminEmails,
        } = await this.getAccountInformation(context, customerAccountId);

        // ディーラー企業を特定する
        const { parent_account_id: dealerAccountId } =
          await this.accountsRepository.findAccountById(
            context,
            customerAccountId,
          );
        // ライセンス発行が行われているので、ディーラーは必ず存在するはず
        if (dealerAccountId == null) {
          throw new Error('dealerAccountId is null');
        }

        // ディーラー企業の企業名と管理者情報を取得する
        const {
          companyName: dealerCompanyName,
          adminEmails: dealerAdminEmails,
        } = await this.getAccountInformation(context, dealerAccountId);

        // キャンセル済みの注文をID指定して取得する
        const order = await this.licensesRepository.getLicenseOrder(
          context,
          customerAccountId,
          poNumber,
          canceledOrderId,
        );

        if (order == null) {
          throw new Error(
            `cancel target order not found. fromAccountId: ${customerAccountId}, poNumber:${poNumber}`,
          );
        }
        if (order.status !== LICENSE_ISSUE_STATUS.CANCELED) {
          throw new Error(
            `target order is not canceled. fromAccountId: ${order.from_account_id}, poNumber:${order.po_number}, status:${order.status}, id:${order.id}`,
          );
        }

        const { quantity } = order;

        // 注文キャンセルが成功した旨をメール送信する
        await this.sendgridService.sendMailWithU106(
          context,
          customerAdminEmails,
          customerCompanyName,
          quantity,
          poNumber,
          dealerAdminEmails,
          dealerCompanyName,
        );
      } catch (e) {
        this.logger.error(`[${context.getTrackingId()}] error=${e}`);
        // メール送信に関する例外はログだけ出して握りつぶす
      }
    } catch (e) {
      this.logger.error(`[${context.getTrackingId()}] error=${e}`);
      switch (e.constructor) {
        case UserNotFoundError:
          throw new HttpException(
            makeErrorResponse('E010204'),
            HttpStatus.BAD_REQUEST,
          );
        case CancelOrderFailedError:
          throw new HttpException(
            makeErrorResponse('E010808'),
            HttpStatus.BAD_REQUEST,
          );
        default:
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
      }
    } finally {
      this.logger.log(
        `[OUT] [${context.getTrackingId()}] ${this.cancelOrder.name}`,
      );
    }
    return;
  }

  /**
   * アカウントIDを指定して、アカウント情報と管理者情報を取得する
   * @param context
   * @param accountId 対象アカウントID
   * @returns 企業名/管理者メールアドレス
   */
  private async getAccountInformation(
    context: Context,
    accountId: number,
  ): Promise<{
    companyName: string;
    adminEmails: string[];
  }> {
    // アカウントIDから企業名を取得する
    const { company_name } = await this.accountsRepository.findAccountById(
      context,
      accountId,
    );

    // 管理者一覧を取得
    const admins = await this.usersRepository.findAdminUsers(
      context,
      accountId,
    );
    const adminExternalIDs = admins.map((x) => x.external_id);

    // ADB2Cから管理者IDを元にメールアドレスを取得する
    const usersInfo = await this.adb2cService.getUsers(
      context,
      adminExternalIDs,
    );

    // 生のAzure AD B2Cのユーザー情報からメールアドレスを抽出する
    const adminEmails = usersInfo.map((x) => {
      const { emailAddress } = getUserNameAndMailAddress(x);
      if (emailAddress == null) {
        throw new Error('dealer admin email-address is not found');
      }
      return emailAddress;
    });

    return {
      companyName: company_name,
      adminEmails: adminEmails,
    };
  }
}
