import { app, InvocationContext, Timer } from "@azure/functions";
import { Between, DataSource, In, MoreThan, Repository } from "typeorm";
import { User } from "../entity/user.entity";
import { Account } from "../entity/account.entity";
import { License, LicenseAllocationHistory } from "../entity/license.entity";
import * as dotenv from "dotenv";
import {
  LICENSE_ALLOCATED_STATUS,
  LICENSE_TYPE,
  SWITCH_FROM_TYPE,
  TIERS,
  USER_ROLES,
} from "../constants";
import {
  DateWithDayEndTime,
  DateWithZeroTime,
  NewAllocatedLicenseExpirationDate,
} from "../common/types/types";

export async function licenseAutoAllocationProcessing(
  context: InvocationContext,
  datasource: DataSource,
  dateToTrigger?: Date
): Promise<void> {
  try {
    context.log("[IN]licenseAutoAllocationProcessing");

    // ライセンスの有効期間判定用
    let currentDateZeroTime = new DateWithZeroTime();
    let currentDateEndTime = new DateWithDayEndTime();
    if (dateToTrigger) {
      currentDateZeroTime = new DateWithZeroTime(dateToTrigger);
      currentDateEndTime = new DateWithDayEndTime(dateToTrigger);
    }
    // 自動更新対象の候補となるアカウントを取得
    const accountRepository = datasource.getRepository(Account);
    const targetAccounts = await accountRepository.find({
      where: {
        tier: TIERS.TIER5,
      },
    });

    // 自動更新対象となるアカウント・ユーザーを取得
    const autoAllocationLists = await findTargetUser(
      context,
      datasource,
      targetAccounts,
      currentDateZeroTime,
      currentDateEndTime
    );

    // 対象となるアカウント数分ループ
    for (const autoAllocationList of autoAllocationLists) {
      // ライセンスを割り当てる
      await allocateLicense(
        context,
        datasource,
        autoAllocationList,
        currentDateZeroTime,
        currentDateEndTime
      );
    }
  } catch (e) {
    context.log("licenseAutoAllocationProcessing failed.");
    context.error(e);
    throw e;
  } finally {
    context.log("[OUT]licenseAutoAllocationProcessing");
  }
}

export async function licenseAutoAllocation(
  myTimer: Timer,
  context: InvocationContext
): Promise<void> {
  try {
    context.log("[IN]licenseAutoAllocation");
    dotenv.config({ path: ".env" });
    dotenv.config({ path: ".env.local", override: true });
    let datasource: DataSource;
    try {
      datasource = new DataSource({
        type: "mysql",
        host: process.env.DB_HOST,
        port: Number(process.env.DB_PORT),
        username: process.env.DB_USERNAME,
        password: process.env.DB_PASSWORD,
        database: process.env.DB_NAME,
        entities: [User, Account, License, LicenseAllocationHistory],
      });
      await datasource.initialize();
    } catch (e) {
      context.log("database initialize failed.");
      context.error(e);
      throw e;
    }

    await licenseAutoAllocationProcessing(context, datasource);
  } catch (e) {
    context.log("licenseAutoAllocation failed.");
    context.error(e);
    throw e;
  } finally {
    context.log("[OUT]licenseAutoAllocation");
  }
}

/**
 * 自動更新対象のアカウント・ユーザーを取得する
 * @param context
 * @param datasource
 * @param targetAccounts 自動更新対象候補のアカウント
 * @param currentDateZeroTime
 * @param currentDateEndTime
 * @returns autoAllocationList[] 自動更新対象のアカウント・ユーザーのIDリスト
 */
export async function findTargetUser(
  context: InvocationContext,
  datasource: DataSource,
  targetAccounts: Account[],
  currentDateZeroTime: DateWithZeroTime,
  currentDateEndTime: DateWithDayEndTime
): Promise<autoAllocationList[]> {
  try {
    context.log("[IN]findTargetUser");

    const autoAllocationList = [] as autoAllocationList[];
    // ライセンス期限が今日で自動更新対象のユーザーを取得
    const userRepository = datasource.getRepository(User);
    for (const account of targetAccounts) {
      // Author→Typist→Noneの優先度で割り当てたいので、roleごとに個別で取得
      const targetAuthorUsers = await userRepository.find({
        where: {
          account_id: account.id,
          auto_renew: true,
          role: USER_ROLES.AUTHOR,
          license: {
            expiry_date: Between(currentDateZeroTime, currentDateEndTime),
          },
        },
        relations: {
          license: true,
        },
      });

      const targetTypistUsers = await userRepository.find({
        where: {
          account_id: account.id,
          auto_renew: true,
          role: USER_ROLES.TYPIST,
          license: {
            expiry_date: Between(currentDateZeroTime, currentDateEndTime),
          },
        },
        relations: {
          license: true,
        },
      });

      const targetNoneUsers = await userRepository.find({
        where: {
          account_id: account.id,
          auto_renew: true,
          role: USER_ROLES.NONE,
          license: {
            expiry_date: Between(currentDateZeroTime, currentDateEndTime),
          },
        },
        relations: {
          license: true,
        },
      });
      // Author→Typist→Noneの順で配列に格納
      const userIds = [] as number[];
      for (const user of targetAuthorUsers) {
        userIds.push(Number(user.id));
      }
      for (const user of targetTypistUsers) {
        userIds.push(Number(user.id));
      }
      for (const user of targetNoneUsers) {
        userIds.push(Number(user.id));
      }
      // 対象ユーザーが0件なら自動更新リストには含めない
      if (userIds.length !== 0) {
        autoAllocationList.push({
          accountId: account.id,
          userIds: userIds,
        });
      }
    }
    return autoAllocationList;
  } catch (e) {
    context.error(e);
    context.log("findTargetUser failed.");
    throw e;
  } finally {
    context.log("[OUT]findTargetUser");
  }
}

/**
 * 割り当て可能なライセンスを取得する
 * @param context
 * @param licenseRepository
 * @param accountId 自動更新対象のアカウントID
 * @returns License 割り当て可能なライセンス
 */
export async function getAutoAllocatableLicense(
  context: InvocationContext,
  licenseRepository: Repository<License>,
  accountId: number,
  currentDateEndTime: DateWithDayEndTime
): Promise<License | undefined> {
  try {
    context.log("[IN]getAutoAllocatableLicense");
    // 割り当て可能なライセンスを取得
    const license = await licenseRepository.findOne({
      where: {
        account_id: accountId,
        status: In([
          LICENSE_ALLOCATED_STATUS.REUSABLE,
          LICENSE_ALLOCATED_STATUS.UNALLOCATED,
        ]),
        expiry_date: MoreThan(currentDateEndTime) || null,
      },
      order: {
        expiry_date: "ASC",
      },
    });
    if (!license) {
      // 割り当て可能なライセンスが存在しない場合でもエラーとはしたくないので、undifinedを返却する
      return undefined;
    }
    return license;
  } catch (e) {
    context.error(e);
    context.log("getAutoAllocatableLicense failed.");
    throw e;
  } finally {
    context.log("[OUT]getAutoAllocatableLicense");
  }
}

/**
 * ライセンスを割り当てる
 * @param context
 * @param datasource
 * @param account アカウント・ユーザーID
 * @param currentDateZeroTime
 * @param currentDateEndTime
 */
export async function allocateLicense(
  context: InvocationContext,
  datasource: DataSource,
  autoAllocationList: autoAllocationList,
  currentDateZeroTime: DateWithZeroTime,
  currentDateEndTime: DateWithDayEndTime
): Promise<void> {
  try {
    context.log("[IN]allocateLicense");

    // 自動更新対象ユーザーにライセンスを割り当てる
    let hasAllocatebleLicense = true;
    for (const userId of autoAllocationList.userIds) {
      await datasource.transaction(async (entityManager) => {
        const licenseRepository = entityManager.getRepository(License);
        const licenseAllocationHistoryRepo = entityManager.getRepository(
          LicenseAllocationHistory
        );
        // 割り当て可能なライセンスを取得する（自動割り当て用）
        const autoAllocatableLicense = await getAutoAllocatableLicense(
          context,
          licenseRepository,
          autoAllocationList.accountId,
          currentDateEndTime
        );

        // 割り当て可能なライセンスが存在しなければreturnし、その後ループ終了
        if (!autoAllocatableLicense) {
          context.log(`allocatable license not exist.`);
          hasAllocatebleLicense = false;
          return;
        }

        // ライセンスが直前で手動割り当てされていたら、自動割り当てしない
        const allocatedLicense = await licenseRepository.findOne({
          where: {
            allocated_user_id: userId,
            expiry_date: Between(currentDateZeroTime, currentDateEndTime),
          },
        });
        if (!allocatedLicense) {
          context.log(`skip auto allocation. userID:${userId}`);
          return;
        }

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

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

        // 新規ライセンス割り当て
        autoAllocatableLicense.status = LICENSE_ALLOCATED_STATUS.ALLOCATED;
        autoAllocatableLicense.allocated_user_id = userId;
        // 有効期限が未設定なら365日後に設定
        if (!autoAllocatableLicense.expiry_date) {
          autoAllocatableLicense.expiry_date =
            new NewAllocatedLicenseExpirationDate();
        }
        await licenseRepository.save(autoAllocatableLicense);
        context.log(
          `license allocated. userID:${userId}, licenseID:${autoAllocatableLicense.id}`
        );

        // ライセンス割り当て履歴テーブルを更新するための処理
        // 直近割り当てたライセンス種別を取得
        const oldLicenseType = await licenseAllocationHistoryRepo.findOne({
          relations: {
            license: true,
          },
          where: { user_id: userId, is_allocated: true },
          order: { executed_at: "DESC" },
        });

        let switchFromType = "";
        if (oldLicenseType && oldLicenseType.license) {
          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 = autoAllocatableLicense.id;
        allocationHistory.account_id = autoAllocationList.accountId;
        allocationHistory.is_allocated = true;
        allocationHistory.executed_at = new Date();
        // TODO switchFromTypeの値については「PBI1234: 第一階層として、ライセンス数推移情報をCSV出力したい」で正式対応
        allocationHistory.switch_from_type = switchFromType;
        await licenseAllocationHistoryRepo.save(allocationHistory);
      });
      // 割り当て可能なライセンスが存在しなければループ終了
      if (!hasAllocatebleLicense) {
        break;
      }
    }
  } catch (e) {
    // エラーが発生しても次のアカウントへの処理は継続させるため、例外をthrowせずにreturnだけする
    context.error(e);
    context.log("allocateLicense failed.");
    return;
  } finally {
    context.log("[OUT]allocateLicense");
  }
}

app.timer("licenseAutoAllocation", {
  schedule: "0 0 16 * * *",
  handler: licenseAutoAllocation,
});

class autoAllocationList {
  accountId: number;
  userIds: number[];
}
