import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
import {
  AdB2cService,
  ConflictError,
  isConflictError,
} from '../../gateways/adb2c/adb2c.service';
import { Account } from '../../repositories/accounts/entity/account.entity';
import { User } from '../../repositories/users/entity/user.entity';
import {
  LICENSE_EXPIRATION_THRESHOLD_DAYS,
  TIERS,
  USER_ROLES,
} from '../../constants';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import {
  TypistGroup,
  GetPartnerLicensesResponse,
  PartnerLicenseInfo,
  GetOrderHistoriesResponce,
  LicenseOrder,
} from './types/types';
import { DateWithZeroTime } from '../licenses/types/types';
import { GetLicenseSummaryResponse, Typist } from './types/types';
import { AccessToken } from '../../common/token';
import { UserNotFoundError } from '../../repositories/users/errors/types';
import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service';
import { makePassword } from '../../common/password';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
@Injectable()
export class AccountsService {
  constructor(
    private readonly accountRepository: AccountsRepositoryService,
    private readonly licensesRepository: LicensesRepositoryService,
    private readonly usersRepository: UsersRepositoryService,
    private readonly userGroupsRepository: UserGroupsRepositoryService,
    private readonly adB2cService: AdB2cService,
    private readonly sendgridService: SendGridService,
    private readonly configService: ConfigService,
  ) {}
  private readonly logger = new Logger(AccountsService.name);
  /**
   * 第五階層用のライセンス情報を取得する
   * @param accountId
   * @returns LicenseSummary
   */
  async getLicenseSummary(
    accountId: number,
  ): Promise<GetLicenseSummaryResponse> {
    this.logger.log(`[IN] ${this.getLicenseSummary.name}`);

    try {
      const currentDate = new DateWithZeroTime();

      const expiringSoonDate = new Date(currentDate.getTime());
      expiringSoonDate.setDate(
        currentDate.getDate() + LICENSE_EXPIRATION_THRESHOLD_DAYS,
      );
      // システム上有効期限日付の23時59分59秒999ミリ秒までライセンスは有効なため、各値最大値をセット
      expiringSoonDate.setHours(23, 59, 59, 999);

      const { licenseSummary, isStorageAvailable } =
        await this.accountRepository.getLicenseSummaryInfo(
          accountId,
          currentDate,
          expiringSoonDate,
        );

      const {
        allocatableLicenseWithMargin,
        expiringSoonLicense,
        totalLicense,
        allocatedLicense,
        reusableLicense,
        freeLicense,
        issueRequesting,
        numberOfRequesting,
      } = licenseSummary;

      let shortage = allocatableLicenseWithMargin - expiringSoonLicense;
      shortage = shortage >= 0 ? 0 : Math.abs(shortage);

      const licenseSummaryResponse: GetLicenseSummaryResponse = {
        totalLicense,
        allocatedLicense,
        reusableLicense,
        freeLicense,
        expiringWithin14daysLicense: expiringSoonLicense,
        issueRequesting,
        numberOfRequesting,
        storageSize: 0, // XXX PBI1201対象外
        usedSize: 0, // XXX PBI1201対象外
        shortage,
        isStorageAvailable,
      };
      return licenseSummaryResponse;
    } catch (e) {
      console.log(e);
      console.log('get licenseSummary failed');
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
  }
  /**
   * アカウント情報をDBに作成する
   * @param companyName
   * @param country
   * @param [dealerAccountId]
   * @returns account
   */
  async createAccount(
    companyName: string,
    country: string,
    dealerAccountId: number | null,
    email: string,
    password: string,
    username: string,
    role: string,
    acceptedTermsVersion: string,
  ): Promise<{ accountId: number; userId: number; externalUserId: string }> {
    let externalUser: { sub: string } | ConflictError;
    try {
      // idpにユーザーを作成
      externalUser = await this.adB2cService.createUser(
        email,
        password,
        username,
      );
    } catch (e) {
      console.log(e);
      console.log('create externalUser failed');

      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    // メールアドレス重複エラー
    if (isConflictError(externalUser)) {
      throw new HttpException(
        makeErrorResponse('E010301'),
        HttpStatus.BAD_REQUEST,
      );
    }

    let account: Account;
    let user: User;
    try {
      // アカウントと管理者をセットで作成
      const { newAccount, adminUser } =
        await this.accountRepository.createAccount(
          companyName,
          country,
          dealerAccountId,
          TIERS.TIER5,
          externalUser.sub,
          role,
          acceptedTermsVersion,
        );
      account = newAccount;
      user = adminUser;
    } catch (e) {
      console.log('create account failed');
      console.log(
        `[NOT IMPLEMENT] [RECOVER] delete account: ${externalUser.sub}`,
      );
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    try {
      // メールの送信元を取得
      const from = this.configService.get<string>('MAIL_FROM') ?? '';

      // メールの内容を構成
      const { subject, text, html } =
        await this.sendgridService.createMailContentFromEmailConfirm(
          account.id,
          user.id,
          email,
        );

      // メールを送信
      await this.sendgridService.sendMail(email, from, subject, text, html);
    } catch (e) {
      console.log('create user failed');
      console.log(`[NOT IMPLEMENT] [RECOVER] delete account: ${account.id}`);
      console.log(
        `[NOT IMPLEMENT] [RECOVER] delete externalUser: ${externalUser.sub}`,
      );
      console.log(`[NOT IMPLEMENT] [RECOVER] delete user: ${user.id}`);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    return {
      accountId: account.id,
      userId: user.id,
      externalUserId: user.external_id,
    };
  }

  /**
   * アクセストークンからアカウント情報を取得する
   * @param token
   * @returns accountId
   */
  async getMyAccountInfo(token: AccessToken): Promise<number> {
    this.logger.log(`[IN] ${this.getMyAccountInfo.name}`);

    let userInfo: User;
    try {
      userInfo = await this.usersRepository.findUserByExternalId(token.userId);
    } catch (e) {
      switch (e.constructor) {
        case UserNotFoundError:
          throw new HttpException(
            makeErrorResponse('E010204'),
            HttpStatus.BAD_REQUEST,
          );
        default:
          throw new HttpException(
            makeErrorResponse('E009999'),
            HttpStatus.INTERNAL_SERVER_ERROR,
          );
      }
    }

    this.logger.log(`[OUT] ${this.getMyAccountInfo.name}`);
    return userInfo.account_id;
  }

  async getTypistGroups(externalId: string): Promise<TypistGroup[]> {
    this.logger.log(`[IN] ${this.getTypistGroups.name}`);

    // TypistGroup取得
    try {
      const user = await this.usersRepository.findUserByExternalId(externalId);
      const userGroups = await this.userGroupsRepository.getUserGroups(
        user.account_id,
      );

      return userGroups.map((x) => ({ id: x.id, name: x.name }));
    } catch (e) {
      this.logger.error(e);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(`[OUT] ${this.getTypistGroups.name}`);
    }
  }

  /**
   * Gets typists
   * @param externalId
   * @returns typists
   */
  async getTypists(externalId: string): Promise<Typist[]> {
    this.logger.log(`[IN] ${this.getTypists.name}`);

    // Typist取得
    try {
      const typistUsers = await this.usersRepository.findTypistUsers(
        externalId,
      );
      const externalIds = typistUsers.map((x) => x.external_id);

      // B2Cからユーザー名を取得する
      const adb2cUsers = await this.adB2cService.getUsers(
        // TODO: 外部連携以外のログ強化時に、ContollerからContextを取得するように修正する
        { trackingId: 'dummy' },
        externalIds,
      );

      const typists = typistUsers.map((x) => {
        const user = adb2cUsers.find((adb2c) => adb2c.id === x.external_id);
        return {
          id: x.id,
          name: user.displayName,
        };
      });

      return typists;
    } catch (e) {
      this.logger.error(e);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(`[OUT] ${this.getTypists.name}`);
    }
  }

  /**
   * パートナーを追加する
   * @param companyName
   * @param country
   * @param email
   * @param adminName
   * @param userId
   * @param tier
   */
  async createPartnerAccount(
    companyName: string,
    country: string,
    email: string,
    adminName: string,
    userId: string,
    tier: number,
  ): Promise<void> {
    this.logger.log(`[IN] ${this.createPartnerAccount.name}`);

    let myAccountId: number;

    try {
      // アクセストークンからユーザーIDを取得する
      myAccountId = (await this.usersRepository.findUserByExternalId(userId))
        .account_id;
    } catch (e) {
      this.logger.error(`error=${e}`);
      if (e instanceof UserNotFoundError) {
        throw new HttpException(
          makeErrorResponse('E010204'),
          HttpStatus.BAD_REQUEST,
        );
      } else {
        throw new HttpException(
          makeErrorResponse('E009999'),
          HttpStatus.INTERNAL_SERVER_ERROR,
        );
      }
    }

    const ramdomPassword = makePassword();

    let externalUser: { sub: string } | ConflictError;

    try {
      // 管理者ユーザを作成し、AzureADB2C IDを取得する
      externalUser = await this.adB2cService.createUser(
        email,
        ramdomPassword,
        adminName,
      );
    } catch (e) {
      this.logger.error(`error=${e}`);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
    // メールアドレスが重複していた場合はエラーを返す
    if (isConflictError(externalUser)) {
      throw new HttpException(
        makeErrorResponse('E010301'),
        HttpStatus.BAD_REQUEST,
      );
    }

    try {
      // アカウントと管理者をセットで作成
      const { newAccount, adminUser } =
        await this.accountRepository.createAccount(
          companyName,
          country,
          myAccountId,
          tier + 1,
          externalUser.sub,
          USER_ROLES.NONE,
          null,
        );

      const from = this.configService.get<string>('MAIL_FROM') || '';
      const { subject, text, html } =
        await this.sendgridService.createMailContentFromEmailConfirmForNormalUser(
          newAccount.id,
          adminUser.id,
          email,
        );
      await this.sendgridService.sendMail(email, from, subject, text, html);
    } catch (e) {
      this.logger.error(`error=${e}`);
      this.logger.error('create partner account failed');
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
  }

  /**
   * パートナーライセンス情報を取得する
   * @param limit
   * @param offset
   * @param accountId
   * @returns getPartnerLicensesResponse
   */
  async getPartnerLicenses(
    limit: number,
    offset: number,
    accountId: number,
  ): Promise<GetPartnerLicensesResponse> {
    this.logger.log(`[IN] ${this.getPartnerLicenses.name}`);

    try {
      const currentDate = new DateWithZeroTime();

      const getPartnerLicenseResult =
        await this.accountRepository.getPartnerLicense(
          accountId,
          currentDate,
          offset,
          limit,
        );

      // 自アカウントのShortageを算出してreturn用の変数にマージする
      let ownShortage =
        getPartnerLicenseResult.ownPartnerLicenseFromRepository.stockLicense -
        getPartnerLicenseResult.ownPartnerLicenseFromRepository.issuedRequested;
      // 「不足している値」を取得するため、負数の場合は絶対値とし、0以上の場合は0とする
      ownShortage = ownShortage >= 0 ? 0 : Math.abs(ownShortage);

      // return用の型にリポジトリから取得した型をマージし、不足項目（shortage）を設定する
      const ownPartnerLicense: PartnerLicenseInfo = Object.assign(
        {},
        getPartnerLicenseResult.ownPartnerLicenseFromRepository,
        {
          shortage: ownShortage,
        },
      );

      // 第五階層のshortage算出に使用する日付情報
      // 「有効期限が現在日付からしきい値以内のライセンス数」を取得するため、しきい値となる日付を作成する
      const expiringSoonDate = new Date(currentDate.getTime());
      expiringSoonDate.setDate(
        currentDate.getDate() + LICENSE_EXPIRATION_THRESHOLD_DAYS,
      );
      expiringSoonDate.setHours(23, 59, 59, 999);

      // 各子アカウントのShortageを算出してreturn用の変数にマージする
      const childrenPartnerLicenses: PartnerLicenseInfo[] = [];
      for (const childPartnerLicenseFromRepository of getPartnerLicenseResult.childPartnerLicensesFromRepository) {
        let childShortage;
        if (childPartnerLicenseFromRepository.tier === TIERS.TIER5) {
          // 第五階層の場合計算式が異なるため、別途値を取得する
          const { expiringSoonLicense, allocatableLicenseWithMargin } =
            await this.accountRepository.getLicenseCountForShortage(
              childPartnerLicenseFromRepository.accountId,
              currentDate,
              expiringSoonDate,
            );
          childShortage = allocatableLicenseWithMargin - expiringSoonLicense;
        } else {
          childShortage =
            childPartnerLicenseFromRepository.stockLicense -
            childPartnerLicenseFromRepository.issuedRequested;
        }
        // 「不足している値」を取得するため、負数の場合は絶対値とし、0以上の場合は0とする
        childShortage = childShortage >= 0 ? 0 : Math.abs(childShortage);

        const childPartnerLicense: PartnerLicenseInfo = Object.assign(
          {},
          childPartnerLicenseFromRepository,
          {
            shortage: childShortage,
          },
        );

        childrenPartnerLicenses.push(childPartnerLicense);
      }

      const getPartnerLicensesResponse: GetPartnerLicensesResponse = {
        total: getPartnerLicenseResult.total,
        ownPartnerLicense: ownPartnerLicense,
        childrenPartnerLicenses: childrenPartnerLicenses,
      };

      return getPartnerLicensesResponse;
    } catch (e) {
      this.logger.error(e);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(`[OUT] ${this.getPartnerLicenses.name}`);
    }
  }
  /**
   * 注文履歴情報を取得する
   * @param limit
   * @param offset
   * @param accountId
   * @returns getOrderHistoriesResponce
   */
  async getOrderHistories(
    limit: number,
    offset: number,
    accountId: number,
  ): Promise<GetOrderHistoriesResponce> {
    this.logger.log(`[IN] ${this.getOrderHistories.name}`);

    try {
      const licenseHistoryInfo =
        await this.licensesRepository.getLicenseOrderHistoryInfo(
          accountId,
          offset,
          limit,
        );

      // 戻り値用に配列の詰めなおしを行う
      const orderHistories: LicenseOrder[] = [];
      for (const licenseOrder of licenseHistoryInfo.licenseOrders) {
        const returnLicenseOrder: LicenseOrder = {
          issueDate:
            licenseOrder.issued_at !== null
              ? new Date(licenseOrder.issued_at)
                  .toISOString()
                  .substr(0, 10)
                  .replace(/-/g, '/')
              : null,
          numberOfOrder: licenseOrder.quantity,
          orderDate: new Date(licenseOrder.ordered_at)
            .toISOString()
            .substr(0, 10)
            .replace(/-/g, '/'),
          poNumber: licenseOrder.po_number,
          status: licenseOrder.status,
        };
        orderHistories.push(returnLicenseOrder);
      }
      const getOrderHistoriesResponse: GetOrderHistoriesResponce = {
        total: licenseHistoryInfo.total,
        orderHistories: orderHistories,
      };
      return getOrderHistoriesResponse;
    } catch (e) {
      this.logger.error(e);
      throw new HttpException(
        makeErrorResponse('E009999'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    } finally {
      this.logger.log(`[OUT] ${this.getOrderHistories.name}`);
    }
  }
}
