import { app, InvocationContext, Timer } from "@azure/functions";
import { Between, DataSource, In, IsNull, MoreThan, Not } from "typeorm";
import { User } from "../entity/user.entity";
import { Account } from "../entity/account.entity";
import {
  ADB2C_SIGN_IN_TYPE,
  LICENSE_ALLOCATED_STATUS,
  TIERS,
} from "../constants";
import * as dotenv from "dotenv";
import { License } from "../entity/license.entity";
import {
  DateWithDayEndTime,
  DateWithZeroTime,
  ExpirationThresholdDate,
} from "../common/types/types";
import { createMailContentOfLicenseShortage } from "../sendgrid/mailContents/U103ShortageAlert";
import { createMailContentOfLicenseExpiringSoon } from "../sendgrid/mailContents/U104ExpiringSoonAlert";
import { AdB2cService } from "../adb2c/adb2c";
import { SendGridService } from "../sendgrid/sendgrid";
import { getMailFrom } from "../common/getEnv/getEnv";
import { createRedisClient } from "../redis/redis";
import { RedisClient } from "redis";
import { promisify } from "util";
import { makeSendCompKey } from "../common/cache";
import {
  MAIL_U103,
  MAIL_U104,
  SEND_COMPLETE_PREFIX,
  DONE,
} from "../common/cache/constants";

export async function licenseAlertProcessing(
  context: InvocationContext,
  datasource: DataSource,
  redisClient: RedisClient,
  sendgrid: SendGridService,
  adb2c: AdB2cService
) {
  try {
    context.log("[IN]licenseAlertProcessing");

    // redisのキー用
    const currentDate = new DateWithZeroTime();
    const formattedDate = `${currentDate.getFullYear()}-${(
      currentDate.getMonth() + 1
    ).toString()}-${currentDate.getDate().toString()}`;
    const keysAsync = promisify(redisClient.keys).bind(redisClient);

    // メール送信対象のアカウント情報を取得
    const sendTargetAccounts = await getAlertMailTargetAccount(
      context,
      datasource
    );

    // adb2cからメールアドレスを取得し、上記で取得したアカウントにマージする
    const sendTargetAccountsMargedAdb2c = await createAccountInfo(
      context,
      redisClient,
      adb2c,
      sendTargetAccounts
    );

    // メール送信
    await sendAlertMail(
      context,
      redisClient,
      sendgrid,
      sendTargetAccountsMargedAdb2c,
      formattedDate
    );

    // 最後まで処理が正常に通ったら、redisに保存した送信情報を削除する
    try {
      const delAsync = promisify(redisClient.del).bind(redisClient);

      const keys = await keysAsync(`${SEND_COMPLETE_PREFIX}${formattedDate}*`);
      console.log(`delete terget:${keys}`);
      if (keys.length > 0) {
        const delResult = await delAsync(...keys);
        console.log(`delete number:${delResult}`);
      }
    } catch (e) {
      context.log("redis delete failed");
      throw e;
    }
  } catch (e) {
    throw e;
  } finally {
    context.log("[OUT]licenseAlertProcessing");
  }
}

export async function licenseAlert(
  myTimer: Timer,
  context: InvocationContext
): Promise<void> {
  context.log("[IN]licenseAlert");

  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],
    });
    await datasource.initialize();
  } catch (e) {
    context.log("database initialize failed.");
    context.error(e);
    throw e;
  }

  let redisClient: RedisClient;
  try {
    // redis接続
    redisClient = createRedisClient();
  } catch (e) {
    context.log("redis client create failed.");
    context.error(e);
    throw e;
  }

  try {
    const adb2c = new AdB2cService();
    const sendgrid = new SendGridService();
    await licenseAlertProcessing(
      context,
      datasource,
      redisClient,
      sendgrid,
      adb2c
    );
  } catch (e) {
    context.log("licenseAlertProcessing failed.");
    context.error(e);
    throw e;
  } finally {
    await datasource.destroy();
    redisClient.quit;
    context.log("[OUT]licenseAlert");
  }
}

/**
 * アラートメールを送信する対象のアカウントを取得する
 * @param context
 * @param datasource
 * @returns accountInfo[] メール送信対象のアカウント情報
 */
async function getAlertMailTargetAccount(
  context: InvocationContext,
  datasource: DataSource
): Promise<accountInfo[]> {
  try {
    context.log("[IN]getAlertMailTargetAccount");
    const currentDate = new DateWithZeroTime();
    const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime());
    const currentDateWithZeroTime = new DateWithZeroTime();
    const currentDateWithDayEndTime = new DateWithDayEndTime();

    // 第五のアカウントを取得
    const accountRepository = datasource.getRepository(Account);
    const accounts = await accountRepository.find({
      where: {
        tier: TIERS.TIER5,
      },
      relations: {
        primaryAdminUser: true,
        secondaryAdminUser: true,
      },
    });

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

      // 有効期限が現在日付からしきい値以内のライセンス数を取得する
      const expiringSoonLicense = await licenseRepository.count({
        where: {
          account_id: account.id,
          expiry_date: Between(currentDate, expiringSoonDate),
          status: Not(LICENSE_ALLOCATED_STATUS.DELETED),
        },
      });
      // shortage算出
      let shortage = allocatableLicenseWithMargin - expiringSoonLicense;
      shortage = shortage >= 0 ? 0 : Math.abs(shortage);

      // AutoRenewが未チェックかつ、有効期限当日のライセンスが割り当てられているユーザー数を取得
      const userCount = await licenseRepository.count({
        where: [
          {
            account_id: account.id,
            expiry_date: Between(
              currentDateWithZeroTime,
              currentDateWithDayEndTime
            ),
            status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
            user: {
              auto_renew: false,
            },
          },
        ],
        relations: {
          user: true,
        },
      });

      // 上で取得したshortageとユーザー数のどちらかが1以上ならプライマリ、セカンダリ管理者、親企業名を保持する
      // （shortageとユーザー数のどちらかが1以上 = アラートメールを送る必要がある）
      let primaryAdminExternalId: string | undefined;
      let secondaryAdminExternalId: string | undefined;
      let parentCompanyName: string | undefined;

      if (shortage !== 0 || userCount !== 0) {
        primaryAdminExternalId = account.primaryAdminUser
          ? account.primaryAdminUser.external_id
          : undefined;
        secondaryAdminExternalId = account.secondaryAdminUser
          ? account.secondaryAdminUser.external_id
          : undefined;
        // 第五のアカウントを取得
        // strictNullChecks対応
        if (account.parent_account_id) {
          const parent = await accountRepository.findOne({
            where: {
              id: account.parent_account_id,
            },
          });
          parentCompanyName = parent?.company_name;
        }
      } else {
        primaryAdminExternalId = undefined;
        secondaryAdminExternalId = undefined;
        parentCompanyName = undefined;
      }
      sendTargetAccounts.push({
        accountId: account.id,
        companyName: account.company_name,
        parentCompanyName: parentCompanyName,
        shortage: shortage,
        userCountOfLicenseExpiringSoon: userCount,
        primaryAdminExternalId: primaryAdminExternalId,
        secondaryAdminExternalId: secondaryAdminExternalId,
        primaryAdminEmail: undefined,
        secondaryAdminEmail: undefined,
      });
    }
    return sendTargetAccounts;
  } catch (e) {
    context.error(e);
    context.log("getAlertMailTargetAccount failed.");
    throw e;
  } finally {
    context.log("[OUT]getAlertMailTargetAccount");
  }
}

/**
 * Azure AD B2Cからユーザ情報を取得し、アカウント情報を作成する
 * @param context
 * @param redisClient
 * @param adb2c
 * @param sendTargetAccounts RDBから取得したアカウント情報
 * @returns accountInfo[]  メール送信対象のアカウント情報
 */
async function createAccountInfo(
  context: InvocationContext,
  redisClient: RedisClient,
  adb2c: AdB2cService,
  sendTargetAccounts: accountInfo[]
): Promise<accountInfo[]> {
  // ADB2Cからユーザーを取得する用の外部ID配列を作成
  const externalIds = [] as string[];
  sendTargetAccounts.forEach((x) => {
    if (x.primaryAdminExternalId) {
      externalIds.push(x.primaryAdminExternalId);
    }
    if (x.secondaryAdminExternalId) {
      externalIds.push(x.secondaryAdminExternalId);
    }
  });
  const adb2cUsers = await adb2c.getUsers(context, redisClient, externalIds);
  if (adb2cUsers.length === 0) {
    context.log("Target user not found");
    return [];
  }
  // ADB2Cから取得したメールアドレスをRDBから取得した情報にマージ
  sendTargetAccounts.map((info) => {
    const primaryAdminUser = adb2cUsers.find(
      (adb2c) => info.primaryAdminExternalId === adb2c.id
    );
    if (primaryAdminUser) {
      const primaryAdminMail = primaryAdminUser.identities?.find(
        (identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EMAILADDRESS
      )?.issuerAssignedId;
      if (primaryAdminMail) {
        info.primaryAdminEmail = primaryAdminMail;
      }

      const secondaryAdminUser = adb2cUsers.find(
        (adb2c) => info.secondaryAdminExternalId === adb2c.id
      );
      if (secondaryAdminUser) {
        const secondaryAdminMail = secondaryAdminUser.identities?.find(
          (identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EMAILADDRESS
        )?.issuerAssignedId;
        if (secondaryAdminMail) {
          info.secondaryAdminEmail = secondaryAdminMail;
        }
      }
    }
  });
  return sendTargetAccounts;
}

/**
 * アラートメールを送信する
 * @param context
 * @param redisClient
 * @param sendgrid
 * @param sendTargetAccounts メール送信対象のアカウント情報
 * @param formattedDate redisのキーに使用する日付
 * @returns ユーザ情報
 */
async function sendAlertMail(
  context: InvocationContext,
  redisClient: RedisClient,
  sendgrid: SendGridService,
  sendTargetAccounts: accountInfo[],
  formattedDate: string
): Promise<void> {
  try {
    context.log("[IN]sendAlertMail");

    // redis用
    const getAsync = promisify(redisClient.get).bind(redisClient);
    const setexAsync = promisify(redisClient.setex).bind(redisClient);
    const ttl = process.env.ADB2C_CACHE_TTL;
    const mailFrom = getMailFrom();

    for (const targetAccount of sendTargetAccounts) {
      // プライマリ管理者が入っているかチェック
      // 入っていない場合は、アラートメールを送信する必要が無いため、何も処理をせず次のループへ
      if (targetAccount.primaryAdminExternalId) {
        // メール送信
        // strictNullChecks対応
        if (!targetAccount.primaryAdminEmail) {
          continue;
        }
        // ライセンス不足メール
        if (targetAccount.shortage !== 0) {
          // redisに送信履歴がない場合のみ送信する
          const mailResult = await getAsync(
            makeSendCompKey(
              formattedDate,
              targetAccount.primaryAdminExternalId,
              MAIL_U103
            )
          );
          if (mailResult !== DONE) {
            const { subject, text, html } =
              await createMailContentOfLicenseShortage(
                targetAccount.companyName,
                targetAccount.shortage,
                targetAccount.parentCompanyName
              );
            // メールを送信
            try {
              await sendgrid.sendMail(
                targetAccount.primaryAdminEmail,
                mailFrom,
                subject,
                text,
                html
              );
              context.log(
                `Shortage mail send success. mail to :${targetAccount.primaryAdminEmail}`
              );
              // 送信成功時、成功履歴をredisに保存
              try {
                const key = makeSendCompKey(
                  formattedDate,
                  targetAccount.primaryAdminExternalId,
                  MAIL_U103
                );
                await setexAsync(key, ttl, DONE);
                context.log(
                  "setex Result:",
                  `key:${key},ttl:${ttl},value:Done`
                );
              } catch (e) {
                context.error(e);
                context.log(
                  "setex failed.",
                  `target: ${targetAccount.primaryAdminEmail}`
                );
              }
            } catch (e) {
              context.error(e);
              context.log(
                `Shortage mail send failed. mail to :${targetAccount.primaryAdminEmail}`
              );
              throw e;
            }
          }

          // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
          if (
            targetAccount.secondaryAdminEmail &&
            targetAccount.secondaryAdminExternalId
          ) {
            // redisに送信履歴がない場合のみ送信する
            const mailResult = await getAsync(
              makeSendCompKey(
                formattedDate,
                targetAccount.secondaryAdminExternalId,
                MAIL_U103
              )
            );
            if (mailResult !== DONE) {
              const { subject, text, html } =
                await createMailContentOfLicenseShortage(
                  targetAccount.companyName,
                  targetAccount.shortage,
                  targetAccount.parentCompanyName
                );
              // メールを送信
              try {
                await sendgrid.sendMail(
                  targetAccount.secondaryAdminEmail,
                  mailFrom,
                  subject,
                  text,
                  html
                );
                context.log(
                  `Shortage mail send success. mail to :${targetAccount.secondaryAdminEmail}`
                );
                // 送信成功時、成功履歴をredisに保存
                try {
                  const key = makeSendCompKey(
                    formattedDate,
                    targetAccount.secondaryAdminExternalId,
                    MAIL_U103
                  );
                  await setexAsync(key, ttl, DONE);
                  context.log(
                    "setex Result:",
                    `key:${key},ttl:${ttl},value:Done`
                  );
                } catch (e) {
                  context.error(e);
                  context.log(
                    "setex failed.",
                    `target: ${targetAccount.secondaryAdminEmail}`
                  );
                }
              } catch (e) {
                context.error(e);
                context.log(
                  `Shortage mail send failed. mail to :${targetAccount.secondaryAdminEmail}`
                );
                throw e;
              }
            }
          }
        }

        // ライセンス失効警告メール
        if (targetAccount.userCountOfLicenseExpiringSoon !== 0) {
          // redisに送信履歴がない場合のみ送信する
          const mailResult = await getAsync(
            makeSendCompKey(
              formattedDate,
              targetAccount.primaryAdminExternalId,
              MAIL_U104
            )
          );
          if (mailResult !== DONE) {
            const { subject, text, html } =
              await createMailContentOfLicenseExpiringSoon(
                targetAccount.companyName,
                targetAccount.userCountOfLicenseExpiringSoon,
                targetAccount.parentCompanyName
              );
            // メールを送信
            try {
              await sendgrid.sendMail(
                targetAccount.primaryAdminEmail,
                mailFrom,
                subject,
                text,
                html
              );
              context.log(
                `Expiring soon mail send success. mail to :${targetAccount.primaryAdminEmail}`
              );
              // 送信成功時、成功履歴をredisに保存
              try {
                const key = makeSendCompKey(
                  formattedDate,
                  targetAccount.primaryAdminExternalId,
                  MAIL_U104
                );
                await setexAsync(key, ttl, DONE);
                context.log(
                  "setex Result:",
                  `key:${key},ttl:${ttl},value:Done`
                );
              } catch (e) {
                context.error(e);
                context.log(
                  "setex failed.",
                  `target: ${targetAccount.primaryAdminEmail}`
                );
              }
            } catch (e) {
              context.error(e);
              context.log(
                `Expiring soon mail send failed. mail to :${targetAccount.primaryAdminEmail}`
              );
              throw e;
            }
          }

          // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
          if (
            targetAccount.secondaryAdminEmail &&
            targetAccount.secondaryAdminExternalId
          ) {
            // redisに送信履歴がない場合のみ送信する
            const mailResult = makeSendCompKey(
              formattedDate,
              targetAccount.secondaryAdminExternalId,
              MAIL_U104
            );
            if (mailResult !== DONE) {
              const { subject, text, html } =
                await createMailContentOfLicenseExpiringSoon(
                  targetAccount.companyName,
                  targetAccount.userCountOfLicenseExpiringSoon,
                  targetAccount.parentCompanyName
                );
              // メールを送信
              try {
                await sendgrid.sendMail(
                  targetAccount.secondaryAdminEmail,
                  mailFrom,
                  subject,
                  text,
                  html
                );
                context.log(
                  `Expiring soon mail send success. mail to :${targetAccount.secondaryAdminEmail}`
                );
                try {
                  const key = makeSendCompKey(
                    formattedDate,
                    targetAccount.secondaryAdminExternalId,
                    MAIL_U104
                  );
                  await setexAsync(key, ttl, DONE);
                  context.log(
                    "setex Result:",
                    `key:${key},ttl:${ttl},value:Done`
                  );
                } catch (e) {
                  context.error(e);
                  context.log(
                    "setex failed.",
                    `target: ${targetAccount.secondaryAdminEmail}`
                  );
                }
              } catch (e) {
                context.error(e);
                context.log(
                  `Expiring soon mail send failed. mail to :${targetAccount.secondaryAdminEmail}`
                );
                throw e;
              }
            }
          }
        }
      }
    }
  } catch (e) {
    context.log("sendAlertMail failed.");
    throw e;
  } finally {
    context.log("[OUT]sendAlertMail");
  }
}

app.timer("licenseAlert", {
  schedule: "0 0 1 * * *",
  handler: licenseAlert,
});

class accountInfo {
  accountId: number;
  companyName: string;
  parentCompanyName: string | undefined;
  shortage: number;
  userCountOfLicenseExpiringSoon: number;
  primaryAdminExternalId: string | undefined;
  secondaryAdminExternalId: string | undefined;
  primaryAdminEmail: string | undefined;
  secondaryAdminEmail: string | undefined;
}
