import { Injectable } from '@nestjs/common';
import {
  Between,
  DataSource,
  In,
  IsNull,
  MoreThan,
  MoreThanOrEqual,
  Not,
  UpdateResult,
  EntityManager,
} from 'typeorm';
import { User } from '../users/entity/user.entity';
import { Account } from './entity/account.entity';
import { License, LicenseOrder } from '../licenses/entity/license.entity';
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
import {
  getDirection,
  getTaskListSortableAttribute,
} from '../../common/types/sort/util';
import {
  LICENSE_ALLOCATED_STATUS,
  LICENSE_STATUS_ISSUE_REQUESTING,
} from '../../constants';
import {
  LicenseSummaryInfo,
  PartnerLicenseInfoForRepository,
} from '../../features/accounts/types/types';
import { AccountNotFoundError } from './errors/types';

@Injectable()
export class AccountsRepositoryService {
  constructor(private dataSource: DataSource) {}

  /**
   * 管理ユーザー無しでアカウントを作成する
   * @param companyName
   * @param country
   * @param dealerAccountId
   * @param tier
   * @returns create
   */
  async create(
    companyName: string,
    country: string,
    dealerAccountId: number | null,
    tier: number,
  ): Promise<Account> {
    const account = new Account();
    {
      account.parent_account_id = dealerAccountId;
      account.company_name = companyName;
      account.country = country;
      account.tier = tier;
    }

    const createdEntity = await this.dataSource.transaction(
      async (entityManager) => {
        const repo = entityManager.getRepository(Account);
        const newAccount = repo.create(account);
        const persisted = await repo.save(newAccount);
        return persisted;
      },
    );
    return createdEntity;
  }

  /**
   * 特定の情報でアカウントを更新する
   * @param account
   * @returns update
   */
  async update(account: Account): Promise<UpdateResult> {
    return await this.dataSource.transaction(async (entityManager) => {
      const repo = entityManager.getRepository(Account);
      return await repo.update({ id: account.id }, account);
    });
  }

  /**
   * プライマリ管理者とアカウントを同時に作成する
   * @param companyName
   * @param country
   * @param dealerAccountId
   * @param tier
   * @param adminExternalUserId
   * @param adminUserRole
   * @param adminUserAcceptedTermsVersion
   * @returns account/admin user
   */
  async createAccount(
    companyName: string,
    country: string,
    dealerAccountId: number | null,
    tier: number,
    adminExternalUserId: string,
    adminUserRole: string,
    adminUserAcceptedTermsVersion: string,
  ): Promise<{ newAccount: Account; adminUser: User }> {
    return await this.dataSource.transaction(async (entityManager) => {
      const account = new Account();
      {
        account.parent_account_id = dealerAccountId;
        account.company_name = companyName;
        account.country = country;
        account.tier = tier;
      }
      const accountsRepo = entityManager.getRepository(Account);
      const newAccount = accountsRepo.create(account);
      const persistedAccount = await accountsRepo.save(newAccount);

      // 作成されたAccountのIDを使用してユーザーを作成
      const user = new User();
      {
        user.account_id = persistedAccount.id;
        user.external_id = adminExternalUserId;
        user.role = adminUserRole;
        user.accepted_terms_version = adminUserAcceptedTermsVersion;
      }
      const usersRepo = entityManager.getRepository(User);
      const newUser = usersRepo.create(user);
      const persistedUser = await usersRepo.save(newUser);

      // アカウントに管理者を設定して更新
      persistedAccount.primary_admin_user_id = persistedUser.id;

      const result = await accountsRepo.update(
        { id: persistedAccount.id },
        persistedAccount,
      );

      // 想定外の更新が行われた場合はロールバックを行った上でエラー送出
      if (result.affected !== 1) {
        throw new Error(`invalid update. result.affected=${result.affected}`);
      }

      // ユーザーのタスクソート条件を作成
      const sortCriteria = new SortCriteria();
      {
        sortCriteria.parameter = getTaskListSortableAttribute('JOB_NUMBER');
        sortCriteria.direction = getDirection('ASC');
        sortCriteria.user_id = persistedUser.id;
      }
      const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
      const newSortCriteria = sortCriteriaRepo.create(sortCriteria);
      await sortCriteriaRepo.save(newSortCriteria);

      return { newAccount: persistedAccount, adminUser: persistedUser };
    });
  }

  /**
   * アカウントIDからアカウント情報を取得する
   * @param id
   * @returns account
   */
  async findAccountById(id: number): Promise<Account> {
    const account = await this.dataSource.getRepository(Account).findOne({
      where: {
        id: id,
      },
    });

    if (!account) {
      throw new AccountNotFoundError();
    }
    return account;
  }

  /**
   * ※サブルーチンとして、別途トランザクション開始された処理から呼び出されることを想定
   * 有効期限が現在日付からしきい値以内のライセンス数を取得する
   * @param entityManager
   * @param id
   * @param currentDate
   * @param expiringSoonDate
   * @returns expiringSoonLicense
   */
  private async getExpiringSoonLicense(
    entityManager: EntityManager,
    id: number,
    currentDate: Date,
    expiringSoonDate: Date,
  ): Promise<number> {
    const license = entityManager.getRepository(License);

    // 有効期限が現在日付からしきい値以内のライセンス数を取得する
    const expiringSoonLicense = await license.count({
      where: {
        account_id: id,
        expiry_date: Between(currentDate, expiringSoonDate),
        status: Not(LICENSE_ALLOCATED_STATUS.DELETED),
      },
    });

    return expiringSoonLicense;
  }

  /**
   * ※サブルーチンとして、別途トランザクション開始された処理から呼び出されることを想定
   * 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う
   * @param entityManager
   * @param id
   * @param expiringSoonDate
   * @returns allocatableLicenseWithMargin
   */
  private async getAllocatableLicenseWithMargin(
    entityManager: EntityManager,
    id: number,
    expiringSoonDate: Date,
  ): Promise<number> {
    const license = entityManager.getRepository(License);

    // 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う
    const allocatableLicenseWithMargin = await license.count({
      where: [
        {
          account_id: id,
          status: In([
            LICENSE_ALLOCATED_STATUS.UNALLOCATED,
            LICENSE_ALLOCATED_STATUS.REUSABLE,
          ]),
          expiry_date: MoreThan(expiringSoonDate),
        },
        {
          account_id: id,
          status: In([
            LICENSE_ALLOCATED_STATUS.UNALLOCATED,
            LICENSE_ALLOCATED_STATUS.REUSABLE,
          ]),
          expiry_date: IsNull(),
        },
      ],
    });

    return allocatableLicenseWithMargin;
  }

  /**
   * アカウントIDからライセンス情報を取得する
   * @param id
   * @param currentDate
   * @param expiringSoonDate
   * @returns licenseSummary
   */
  async getLicenseSummaryInfo(
    id: number,
    currentDate: Date,
    expiringSoonDate: Date,
  ): Promise<{
    licenseSummary: LicenseSummaryInfo;
    isStorageAvailable: boolean;
  }> {
    return await this.dataSource.transaction(async (entityManager) => {
      const license = entityManager.getRepository(License);
      const licenseOrder = entityManager.getRepository(LicenseOrder);

      // 有効な総ライセンス数を取得する
      const totalLicense = await license.count({
        where: [
          {
            account_id: id,
            expiry_date: MoreThanOrEqual(currentDate),
            status: Not(LICENSE_ALLOCATED_STATUS.DELETED),
          },
          {
            account_id: id,
            expiry_date: IsNull(),
            status: Not(LICENSE_ALLOCATED_STATUS.DELETED),
          },
        ],
      });

      // 有効な総ライセンス数のうち、ユーザーに割り当て済みのライセンス数を取得する
      const allocatedLicense = await license.count({
        where: {
          account_id: id,
          allocated_user_id: Not(IsNull()),
          status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
        },
      });

      // 総ライセンス数のうち、ユーザーに割り当てたことがあるが、現在は割り当て解除され誰にも割り当たっていないライセンス数を取得する
      const reusableLicense = await license.count({
        where: {
          account_id: id,
          status: LICENSE_ALLOCATED_STATUS.REUSABLE,
        },
      });

      // 総ライセンス数のうち、一度もユーザーに割り当てたことのないライセンス数を取得する
      const freeLicense = await license.count({
        where: {
          account_id: id,
          status: LICENSE_ALLOCATED_STATUS.UNALLOCATED,
        },
      });

      // 有効期限が現在日付からしきい値以内のライセンス数を取得する
      const expiringSoonLicense = await this.getExpiringSoonLicense(
        entityManager,
        id,
        currentDate,
        expiringSoonDate,
      );

      // 未発行状態あるいは発行キャンセルされた注文数を取得する
      const numberOfRequesting = await licenseOrder.count({
        where: {
          from_account_id: id,
          status: LICENSE_STATUS_ISSUE_REQUESTING,
        },
      });

      // 未発行状態あるいは発行キャンセルされた注文の総ライセンス数を取得する
      const result = await licenseOrder
        .createQueryBuilder('license_orders')
        .select('SUM(license_orders.quantity)', 'sum')
        .where('license_orders.from_account_id = :id', { id })
        .andWhere('license_orders.status = :status', {
          status: LICENSE_STATUS_ISSUE_REQUESTING,
        })
        .getRawOne();
      const issueRequesting = parseInt(result.sum, 10) || 0;

      // 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う
      const allocatableLicenseWithMargin =
        await this.getAllocatableLicenseWithMargin(
          entityManager,
          id,
          expiringSoonDate,
        );

      // アカウントのロック状態を取得する
      const isStorageAvailable = (await this.findAccountById(id)).locked;

      let licenseSummary = new LicenseSummaryInfo();
      licenseSummary = {
        totalLicense: totalLicense,
        allocatedLicense: allocatedLicense,
        reusableLicense: reusableLicense,
        freeLicense: freeLicense,
        expiringSoonLicense: expiringSoonLicense,
        allocatableLicenseWithMargin: allocatableLicenseWithMargin,
        issueRequesting: issueRequesting,
        numberOfRequesting: numberOfRequesting,
      };
      return { licenseSummary: licenseSummary, isStorageAvailable };
    });
  }

  /**
   * ※サブルーチンとして、別途トランザクション開始された処理から呼び出されることを想定
   * アカウントIDをもとに、ライセンスの注文状況を取得する
   * @param id
   * @param currentDate
   * @param entityManager
   * @returns stockLicense
   * @returns issuedRequested
   * @returns issueRequesting
   */
  private async getAccountLicenseOrderStatus(
    id: number,
    currentDate: Date,
    entityManager: EntityManager,
  ): Promise<{
    stockLicense: number;
    issuedRequested: number;
    issueRequesting: number;
  }> {
    const license = entityManager.getRepository(License);
    const licenseOrder = entityManager.getRepository(LicenseOrder);

    // 有効な総ライセンス数を取得する
    const stockLicense = await license.count({
      where: [
        {
          account_id: id,
          expiry_date: MoreThanOrEqual(currentDate),
          status: Not(LICENSE_ALLOCATED_STATUS.DELETED),
        },
        {
          account_id: id,
          expiry_date: IsNull(),
          status: Not(LICENSE_ALLOCATED_STATUS.DELETED),
        },
      ],
    });

    // 子アカウントからの、未発行状態あるいは発行キャンセルされた注文の総ライセンス数を取得する
    const issuedRequestedSqlResult = await licenseOrder
      .createQueryBuilder('license_orders')
      .select('SUM(license_orders.quantity)', 'sum')
      .where('license_orders.to_account_id = :id', { id })
      .andWhere('license_orders.status = :status', {
        status: LICENSE_STATUS_ISSUE_REQUESTING,
      })
      .getRawOne();
    const issuedRequested = parseInt(issuedRequestedSqlResult.sum, 10) || 0;

    // 未発行状態あるいは発行キャンセルされた注文の総ライセンス数を取得する
    const issuedRequestingSqlResult = await licenseOrder
      .createQueryBuilder('license_orders')
      .select('SUM(license_orders.quantity)', 'sum')
      .where('license_orders.from_account_id = :id', { id })
      .andWhere('license_orders.status = :status', {
        status: LICENSE_STATUS_ISSUE_REQUESTING,
      })
      .getRawOne();
    const issuedRequesting = parseInt(issuedRequestingSqlResult.sum, 10) || 0;

    return {
      stockLicense: stockLicense,
      issuedRequested: issuedRequested,
      issueRequesting: issuedRequesting,
    };
  }

  /**
   * アカウントIDをもとに、自分と子アカウントのライセンス情報を取得する
   * @param id
   * @param offset
   * @param limit
   * @returns total: 総件数
   * @returns ownPartnerLicenseFromRepository: リポジトリから取得した自アカウントのライセンス情報
   * @returns childrenPartnerLicensesFromRepository: リポジトリから取得した子アカウントのライセンス情報
   */
  async getPartnerLicense(
    id: number,
    currentDate: Date,
    offset: number,
    limit: number,
  ): Promise<{
    total: number;
    ownPartnerLicenseFromRepository: PartnerLicenseInfoForRepository;
    childPartnerLicensesFromRepository: PartnerLicenseInfoForRepository[];
  }> {
    return await this.dataSource.transaction(async (entityManager) => {
      const account = entityManager.getRepository(Account);

      // 自アカウントの情報を取得する
      const ownAccount = await account.findOne({
        where: {
          id: id,
        },
      });

      // 自アカウントのライセンス注文状況を取得する
      const ownLicenseOrderStatus = await this.getAccountLicenseOrderStatus(
        id,
        currentDate,
        entityManager,
      );

      // 自アカウントの戻り値を設定する
      const ownPartnerLicenseFromRepository: PartnerLicenseInfoForRepository = {
        accountId: id,
        tier: ownAccount.tier,
        companyName: ownAccount.company_name,
        stockLicense: ownLicenseOrderStatus.stockLicense,
        issuedRequested: ownLicenseOrderStatus.issuedRequested,
        issueRequesting: ownLicenseOrderStatus.issueRequesting,
      };

      // 子アカウントのアカウント情報を取得する
      const childAccounts = await account.find({
        where: {
          parent_account_id: id,
        },
        order: {
          company_name: 'ASC',
        },
        take: limit,
        skip: offset,
      });

      // 各子アカウントのライセンス注文状況を取得する
      const childPartnerLicensesFromRepository: PartnerLicenseInfoForRepository[] =
        [];
      for (const childAccount of childAccounts) {
        // ライセンス注文状況を取得する
        const childLicenseOrderStatus = await this.getAccountLicenseOrderStatus(
          childAccount.id,
          currentDate,
          entityManager,
        );

        // 戻り値用の値を設定
        const childPartnerLicenseFromRepository: PartnerLicenseInfoForRepository =
          {
            accountId: childAccount.id,
            tier: childAccount.tier,
            companyName: childAccount.company_name,
            stockLicense: childLicenseOrderStatus.stockLicense,
            issuedRequested: childLicenseOrderStatus.issuedRequested,
            issueRequesting: childLicenseOrderStatus.issueRequesting,
          };

        childPartnerLicensesFromRepository.push(
          childPartnerLicenseFromRepository,
        );
      }

      // limit/offsetによらない総件数を取得する
      const total = await account.count({
        where: {
          parent_account_id: id,
        },
      });

      return {
        total: total,
        ownPartnerLicenseFromRepository: ownPartnerLicenseFromRepository,
        childPartnerLicensesFromRepository: childPartnerLicensesFromRepository,
      };
    });
  }

  /**
   * 不足数を算出するためのライセンス数情報を取得する
   * @param id
   * @param currentDate
   * @param expiringSoonDate
   * @returns expiringSoonLicense
   * @returns expiringSoonDate
   */
  async getLicenseCountForShortage(
    id: number,
    currentDate: Date,
    expiringSoonDate: Date,
  ): Promise<{
    expiringSoonLicense: number;
    allocatableLicenseWithMargin: number;
  }> {
    return await this.dataSource.transaction(async (entityManager) => {
      const expiringSoonLicense = await this.getExpiringSoonLicense(
        entityManager,
        id,
        currentDate,
        expiringSoonDate,
      );
      const allocatableLicenseWithMargin =
        await this.getAllocatableLicenseWithMargin(
          entityManager,
          id,
          expiringSoonDate,
        );
      return {
        expiringSoonLicense: expiringSoonLicense,
        allocatableLicenseWithMargin: allocatableLicenseWithMargin,
      };
    });
  }
}
