import { Injectable, Logger } from '@nestjs/common';
import { DataSource, In, IsNull, MoreThanOrEqual } from 'typeorm';
import {
  LicenseOrder,
  License,
  CardLicenseIssue,
  CardLicense,
  LicenseAllocationHistory,
} from './entity/license.entity';
import {
  CARD_LICENSE_LENGTH,
  LICENSE_ALLOCATED_STATUS,
  LICENSE_STATUS_ISSUE_REQUESTING,
  LICENSE_STATUS_ISSUED,
  LICENSE_TYPE,
  SWITCH_FROM_TYPE,
  TIERS,
} from '../../constants';
import {
  PoNumberAlreadyExistError,
  LicenseNotExistError,
  LicenseKeyAlreadyActivatedError,
  LicensesShortageError,
  AlreadyIssuedError,
  OrderNotFoundError,
  LicenseExpiredError,
  LicenseUnavailableError,
  LicenseAlreadyDeallocatedError,
} from './errors/types';
import {
  AllocatableLicenseInfo,
  DateWithZeroTime,
} from '../../features/licenses/types/types';
import { NewAllocatedLicenseExpirationDate } from '../../features/licenses/types/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 = [];
      //TODO タスク 2409: カードライセンスのレコード作成がbulkinsertになっていない
      // ライセンステーブルを作成する（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 = [];
      //TODO タスク 2409: カードライセンスのレコード作成がbulkinsertになっていない
      //  カードライセンステーブルを作成する（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,
      };
    });
  }

  /**
   * 対象の注文を発行する
   * @context Context
   * @param orderedAccountId
   * @param myAccountId
   * @param tier
   * @param poNumber
   */
  async issueLicense(
    orderedAccountId: number,
    myAccountId: number,
    tier: number,
    poNumber: string,
  ): Promise<void> {
    const nowDate = new Date();
    await this.dataSource.transaction(async (entityManager) => {
      const licenseOrderRepo = entityManager.getRepository(LicenseOrder);
      const licenseRepo = entityManager.getRepository(License);

      const issuingOrder = await licenseOrderRepo.findOne({
        where: {
          from_account_id: orderedAccountId,
          po_number: poNumber,
        },
      });
      // 注文が存在しない場合、エラー
      if (!issuingOrder) {
        throw new OrderNotFoundError(`No order found for PONumber:${poNumber}`);
      }
      // 既に発行済みの注文の場合、エラー
      if (issuingOrder.status !== LICENSE_STATUS_ISSUE_REQUESTING) {
        throw new AlreadyIssuedError(
          `An order for PONumber:${poNumber} has already been issued.`,
        );
      }

      // ライセンステーブルのレコードを作成する
      const newLicenses = Array.from({ length: issuingOrder.quantity }, () => {
        const license = new License();
        license.account_id = orderedAccountId;
        license.status = LICENSE_ALLOCATED_STATUS.UNALLOCATED;
        license.type = LICENSE_TYPE.NORMAL;
        license.order_id = issuingOrder.id;
        return license;
      });
      // ライセンス注文テーブルを更新（注文元）
      await licenseOrderRepo.update(
        { id: issuingOrder.id },
        {
          issued_at: nowDate,
          status: LICENSE_STATUS_ISSUED,
        },
      );
      // ライセンステーブルを登録（注文元）
      await licenseRepo.save(newLicenses);

      // 第一階層の場合はストックライセンスの概念が存在しないため、ストックライセンス変更処理は行わない
      if (tier !== TIERS.TIER1) {
        const licensesToUpdate = await licenseRepo.find({
          where: {
            account_id: myAccountId,
            status: LICENSE_ALLOCATED_STATUS.UNALLOCATED,
            type: LICENSE_TYPE.NORMAL,
          },
          order: {
            id: 'ASC',
          },
          take: newLicenses.length,
        });

        // 登録したライセンスに対して自身のライセンスが不足していた場合、エラー
        if (newLicenses.length > licensesToUpdate.length) {
          throw new LicensesShortageError(
            `Shortage Licenses.Number of licenses attempted to be issued is ${newLicenses.length}.`,
          );
        }
        for (const licenseToUpdate of licensesToUpdate) {
          licenseToUpdate.status = LICENSE_ALLOCATED_STATUS.DELETED;
          licenseToUpdate.deleted_at = nowDate;
          licenseToUpdate.delete_order_id = issuingOrder.id;
        }
        // 自身のライセンスを削除（論理削除）する
        await licenseRepo.save(licensesToUpdate);
      }
    });
  }
  /**
   * 対象のアカウントの割り当て可能なライセンスを取得する
   * @context Context
   * @param accountId
   * @param tier
   * @return AllocatableLicenseInfo[]
   */
  async getAllocatableLicenses(
    myAccountId: number,
  ): Promise<AllocatableLicenseInfo[]> {
    const nowDate = new DateWithZeroTime();
    const licenseRepo = this.dataSource.getRepository(License);
    const queryBuilder = licenseRepo
      .createQueryBuilder('license')
      .where('license.account_id = :accountId', { accountId: myAccountId })
      .andWhere('license.status IN (:...statuses)', {
        statuses: [
          LICENSE_ALLOCATED_STATUS.UNALLOCATED,
          LICENSE_ALLOCATED_STATUS.REUSABLE,
        ],
      })
      .andWhere(
        '(license.expiry_date >= :nowDate OR license.expiry_date IS NULL)',
        { nowDate },
      )
      .orderBy('license.expiry_date IS NULL', 'DESC')
      .addOrderBy('license.expiry_date', 'DESC')
      .addOrderBy('license.id', 'ASC');
    const allocatableLicenses = await queryBuilder.getMany();
    return allocatableLicenses.map((license) => ({
      licenseId: license.id,
      expiryDate: license.expiry_date,
    }));
  }
  /**
   * ライセンスをユーザーに割り当てる
   * @param userId
   * @param newLicenseId
   */
  async allocateLicense(userId: number, newLicenseId: number): Promise<void> {
    await this.dataSource.transaction(async (entityManager) => {
      const licenseRepo = entityManager.getRepository(License);
      const licenseAllocationHistoryRepo = entityManager.getRepository(
        LicenseAllocationHistory,
      );
      // 割り当て対象のライセンス情報を取得
      const targetLicense = await licenseRepo.findOne({
        where: {
          id: newLicenseId,
        },
      });

      // 期限切れの場合はエラー
      if (targetLicense.expiry_date) {
        const currentDay = new Date();
        currentDay.setHours(23, 59, 59, 999);
        if (targetLicense.expiry_date < currentDay) {
          throw new LicenseExpiredError(
            `License is expired. expiration date: ${targetLicense.expiry_date} current Date: ${currentDay}`,
          );
        }
      }
      // ライセンス状態が「未割当」「再利用可能」以外の場合はエラー
      if (
        targetLicense.status === LICENSE_ALLOCATED_STATUS.ALLOCATED ||
        targetLicense.status === LICENSE_ALLOCATED_STATUS.DELETED
      ) {
        throw new LicenseUnavailableError(
          `License is unavailable. License status: ${targetLicense.status}`,
        );
      }

      // 対象ユーザーのライセンス割り当て状態を取得
      const allocatedLicense = await licenseRepo.findOne({
        where: {
          allocated_user_id: userId,
        },
      });

      // 既にライセンスが割り当てられているなら、割り当てを解除
      if (allocatedLicense) {
        allocatedLicense.status = LICENSE_ALLOCATED_STATUS.REUSABLE;
        allocatedLicense.allocated_user_id = null;

        await licenseRepo.save(allocatedLicense);

        // ライセンス割り当て履歴テーブルへ登録
        const deallocationHistory = new LicenseAllocationHistory();
        deallocationHistory.user_id = userId;
        deallocationHistory.license_id = allocatedLicense.id;
        deallocationHistory.is_allocated = false;
        deallocationHistory.executed_at = new Date();
        deallocationHistory.switch_from_type = SWITCH_FROM_TYPE.NONE;
        await licenseAllocationHistoryRepo.save(deallocationHistory);
      }

      // ライセンス割り当てを実施
      targetLicense.status = LICENSE_ALLOCATED_STATUS.ALLOCATED;
      targetLicense.allocated_user_id = userId;
      // 有効期限が未設定なら365日後に設定
      if (!targetLicense.expiry_date) {
        targetLicense.expiry_date = new NewAllocatedLicenseExpirationDate();
      }
      await licenseRepo.save(targetLicense);

      // 直近割り当てたライセンス種別を取得
      const oldLicenseType = await licenseAllocationHistoryRepo.findOne({
        relations: {
          license: true,
        },
        where: { user_id: userId, is_allocated: true },
        order: { executed_at: 'DESC' },
      });

      let switchFromType = '';
      if (oldLicenseType) {
        switch (oldLicenseType.license.type) {
          case LICENSE_TYPE.CARD:
            switchFromType = SWITCH_FROM_TYPE.CARD;
            break;
          case LICENSE_TYPE.TRIAL:
            switchFromType = SWITCH_FROM_TYPE.TRIAL;
            break;
          default:
            switchFromType = SWITCH_FROM_TYPE.NONE;
            break;
        }
      } else {
        switchFromType = SWITCH_FROM_TYPE.NONE;
      }

      // ライセンス割り当て履歴テーブルへ登録
      const allocationHistory = new LicenseAllocationHistory();
      allocationHistory.user_id = userId;
      allocationHistory.license_id = targetLicense.id;
      allocationHistory.is_allocated = true;
      allocationHistory.executed_at = new Date();
      // TODO switchFromTypeの値については「PBI1234: 第一階層として、ライセンス数推移情報をCSV出力したい」で正式対応
      allocationHistory.switch_from_type = switchFromType;

      await licenseAllocationHistoryRepo.save(allocationHistory);
    });
  }

  /**
   * ユーザーに割り当てられているライセンスを解除する
   * @param userId
   */
  async deallocateLicense(userId: number): Promise<void> {
    await this.dataSource.transaction(async (entityManager) => {
      const licenseRepo = entityManager.getRepository(License);
      const licenseAllocationHistoryRepo = entityManager.getRepository(
        LicenseAllocationHistory,
      );
      // 対象ユーザーのライセンス割り当て状態を取得
      const allocatedLicense = await licenseRepo.findOne({
        where: {
          allocated_user_id: userId,
          status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
        },
      });

      // ライセンスが割り当てられていない場合はエラー
      if (!allocatedLicense) {
        throw new LicenseAlreadyDeallocatedError(
          `License is already deallocated. userId: ${userId}`,
        );
      }

      // ライセンスの割り当てを解除
      allocatedLicense.status = LICENSE_ALLOCATED_STATUS.REUSABLE;
      allocatedLicense.allocated_user_id = null;
      await licenseRepo.save(allocatedLicense);

      // ライセンス割り当て履歴テーブルへ登録
      const deallocationHistory = new LicenseAllocationHistory();
      deallocationHistory.user_id = userId;
      deallocationHistory.license_id = allocatedLicense.id;
      deallocationHistory.is_allocated = false;
      deallocationHistory.executed_at = new Date();
      deallocationHistory.switch_from_type = SWITCH_FROM_TYPE.NONE;
      await licenseAllocationHistoryRepo.save(deallocationHistory);
    });
  }
}
