import { Injectable, Logger } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import {
  LicenseOrder,
  License,
  CardLicenseIssue,
  CardLicense,
} from './entity/license.entity';
import {
  CARD_LICENSE_LENGTH,
  LICENSE_ALLOCATED_STATUS,
  LICENSE_STATUS_ISSUE_REQUESTING,
  LICENSE_STATUS_ISSUED,
  LICENSE_TYPE,
} from '../../constants';
import {
  PoNumberAlreadyExistError,
  LicenseNotExistError,
  LicenseKeyAlreadyActivatedError,
} from './errors/types';

@Injectable()
export class LicensesRepositoryService {
  constructor(private dataSource: DataSource) {}
  private readonly logger = new Logger(LicensesRepositoryService.name);

  async order(
    poNumber: string,
    fromAccountId: number,
    toAccountId: number,
    quantity: number,
  ): Promise<LicenseOrder> {
    const licenseOrder = new LicenseOrder();
    licenseOrder.po_number = poNumber;
    licenseOrder.from_account_id = fromAccountId;
    licenseOrder.to_account_id = toAccountId;
    licenseOrder.quantity = quantity;
    licenseOrder.status = LICENSE_STATUS_ISSUE_REQUESTING;

    // ライセンス注文テーブルに登録する
    const createdEntity = await this.dataSource.transaction(
      async (entityManager) => {
        //poNumberの重複チェックを行う
        const isPoNumberDuplicated = await entityManager
          .getRepository(LicenseOrder)
          .findOne({
            where: [
              {
                po_number: poNumber,
                from_account_id: fromAccountId,
                status: LICENSE_STATUS_ISSUED,
              },
              {
                po_number: poNumber,
                from_account_id: fromAccountId,
                status: LICENSE_STATUS_ISSUE_REQUESTING,
              },
            ],
          });
        // 重複があった場合はエラーを返却する
        if (isPoNumberDuplicated) {
          throw new PoNumberAlreadyExistError();
        }

        const repo = entityManager.getRepository(LicenseOrder);
        const newLicenseOrder = repo.create(licenseOrder);
        const persisted = await repo.save(newLicenseOrder);
        return persisted;
      },
    );
    return createdEntity;
  }

  /**
   * カードライセンスを発行する
   * @param accountId
   * @param count
   * @returns string[] カードライセンスキーの配列
   */
  async createCardLicenses(
    accountId: number,
    count: number,
  ): Promise<string[]> {
    const licenseKeys: string[] = [];

    await this.dataSource.transaction(async (entityManager) => {
      const licensesRepo = entityManager.getRepository(License);
      const cardLicenseRepo = entityManager.getRepository(CardLicense);
      const cardLicenseIssueRepo =
        entityManager.getRepository(CardLicenseIssue);

      const licenses = [];
      // ライセンステーブルを作成する（BULK INSERT)
      for (let i = 0; i < count; i++) {
        const license = new License();
        license.account_id = accountId;
        license.status = LICENSE_ALLOCATED_STATUS.UNALLOCATED;
        license.type = LICENSE_TYPE.CARD;
        licenses.push(license);
      }
      const savedLicenses = await licensesRepo.save(licenses);

      // カードライセンス発行テーブルを作成する
      const cardLicenseIssue = new CardLicenseIssue();
      cardLicenseIssue.issued_at = new Date();
      const newCardLicenseIssue = cardLicenseIssueRepo.create(cardLicenseIssue);
      const savedCardLicensesIssue = await cardLicenseIssueRepo.save(
        newCardLicenseIssue,
      );

      let isDuplicateKeysExist = true;
      let generateCount = count;
      while (isDuplicateKeysExist) {
        const generateKeys = await this.generateLicenseKeys(
          generateCount,
          licenseKeys,
        );
        // licenseKeysが既にカードライセンステーブルに存在するかチェック
        const existingCardLicenses = await cardLicenseRepo.find({
          where: {
            card_license_key: In(generateKeys),
          },
        });
        if (existingCardLicenses.length > 0) {
          // 重複分を配列から削除
          existingCardLicenses.forEach((existKey) => {
            generateKeys.splice(
              generateKeys.indexOf(existKey.card_license_key),
              1,
            );
          });
          // 重複がなかったものを格納
          generateKeys.forEach((keys) => {
            licenseKeys.push(keys);
          });
          // 重複分の再生成を行う
          generateCount = existingCardLicenses.length;
          continue;
        }
        // 重複がない場合は本ループで作成したkeyをすべて格納
        generateKeys.forEach((keys) => {
          licenseKeys.push(keys);
        });
        // 重複がない場合はループを終了
        isDuplicateKeysExist = false;
      }

      const cardLicenses = [];
      //  カードライセンステーブルを作成する（BULK INSERT)
      for (let i = 0; i < count; i++) {
        const cardLicense = new CardLicense();
        cardLicense.license_id = savedLicenses[i].id; // Licenseテーブルの自動採番されたIDを挿入
        cardLicense.issue_id = savedCardLicensesIssue.id; // CardLicenseIssueテーブルの自動採番されたIDを挿入
        cardLicense.card_license_key = licenseKeys[i];
        cardLicenses.push(cardLicense);
      }
      await cardLicenseRepo.save(cardLicenses);
    });
    return licenseKeys;
  }

  /**
   * ランダム（大文字英数字）の一意のライセンスキーを作成する。
   * @param count
   * @param existingLicenseKeys 既に作成されたライセンスキーの配列
   * @returns licenseKeys
   */
  async generateLicenseKeys(
    count: number,
    existingLicenseKeys: string[],
  ): Promise<string[]> {
    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    const licenseKeys: string[] = [];

    while (licenseKeys.length < count) {
      let licenseKey = '';
      for (let i = 0; i < CARD_LICENSE_LENGTH; i++) {
        const randomIndex = Math.floor(Math.random() * characters.length);
        licenseKey += characters[randomIndex];
      }

      // 重複しない一意のライセンスキーを生成するまで繰り返す
      if (
        !licenseKeys.includes(licenseKey) &&
        !existingLicenseKeys.includes(licenseKey)
      ) {
        licenseKeys.push(licenseKey);
      }
    }

    return licenseKeys;
  }

  /**
   * カードライセンスを取り込む
   * @param accountId
   * @param licenseKey
   * @returns void
   */
  async activateCardLicense(
    accountId: number,
    licenseKey: string,
  ): Promise<void> {
    await this.dataSource.transaction(async (entityManager) => {
      const cardLicenseRepo = entityManager.getRepository(CardLicense);

      // カードライセンステーブルを検索
      const targetCardLicense = await cardLicenseRepo.findOne({
        where: {
          card_license_key: licenseKey,
        },
      });
      // カードライセンスが存在しなければエラー
      if (!targetCardLicense) {
        this.logger.error(
          `card license key not exist. card_licence_key: ${licenseKey}`,
        );
        throw new LicenseNotExistError();
      }
      // 既に取り込み済みならエラー
      if (targetCardLicense.activated_at) {
        this.logger.error(
          `card license already activated. card_licence_key: ${licenseKey}`,
        );
        throw new LicenseKeyAlreadyActivatedError();
      }

      const licensesRepo = entityManager.getRepository(License);

      // ライセンステーブルを検索
      const targetLicense = await licensesRepo.findOne({
        where: {
          id: targetCardLicense.license_id,
        },
      });
      // ライセンスが存在しなければエラー
      if (!targetLicense) {
        this.logger.error(
          `license not exist. licence_id: ${targetCardLicense.license_id}`,
        );
        throw new LicenseNotExistError();
      }

      // ライセンステーブルを更新する
      targetLicense.account_id = accountId;
      await licensesRepo.save(targetLicense);

      // カードライセンステーブルを更新する
      targetCardLicense.activated_at = new Date();
      await cardLicenseRepo.save(targetCardLicense);

      this.logger.log(
        `activate success. licence_id: ${targetCardLicense.license_id}`,
      );
    });
    return;
  }

  /**
   * アカウントIDに紐づく注文履歴情報を取得する
   * @param accountId
   * @param offset
   * @param limit
   * @returns total
   * @returns licenseOrders
   */
  async getLicenseOrderHistoryInfo(
    accountId: number,
    offset: number,
    limit: number,
  ): Promise<{
    total: number;
    licenseOrders: LicenseOrder[];
  }> {
    return await this.dataSource.transaction(async (entityManager) => {
      const licenseOrder = entityManager.getRepository(LicenseOrder);
      // limit/offsetによらない総件数を取得する
      const total = await licenseOrder.count({
        where: {
          from_account_id: accountId,
        },
      });
      const licenseOrders = await licenseOrder.find({
        where: {
          from_account_id: accountId,
        },
        order: {
          ordered_at: 'DESC',
        },
        take: limit,
        skip: offset,
      });
      return {
        total: total,
        licenseOrders: licenseOrders,
      };
    });
  }
}
