import { ClientSecretCredential } from "@azure/identity";
import { Client } from "@microsoft/microsoft-graph-client";
import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials";
import { AdB2cResponse, AdB2cUser } from "./types/types";
import { error } from "console";
import { makeADB2CKey, restoreAdB2cID } from "../common/cache";
import { promisify } from "util";
import { InvocationContext } from "@azure/functions";
import { RedisClient } from "redis";

export class Adb2cTooManyRequestsError extends Error {}

export class AdB2cService {
  private graphClient: Client;

  constructor() {
    // ADB2Cへの認証情報
    if (
      !process.env.ADB2C_TENANT_ID ||
      !process.env.ADB2C_CLIENT_ID ||
      !process.env.ADB2C_CLIENT_SECRET ||
      !process.env.ADB2C_CACHE_TTL
    ) {
      throw error;
    }
    try {
      const credential = new ClientSecretCredential(
        process.env.ADB2C_TENANT_ID,
        process.env.ADB2C_CLIENT_ID,
        process.env.ADB2C_CLIENT_SECRET
      );
      const authProvider = new TokenCredentialAuthenticationProvider(
        credential,
        {
          scopes: ["https://graph.microsoft.com/.default"],
        }
      );

      this.graphClient = Client.initWithMiddleware({ authProvider });
    } catch (error) {
      console.log(error);
      throw error;
    }
  }

  /**
   * Azure AD B2Cからユーザ情報を取得する
   * @param externalIds 外部ユーザーID
   * @returns ユーザ情報
   */
  async getUsers(
    context: InvocationContext,
    redisClient: RedisClient,
    externalIds: string[]
  ): Promise<AdB2cUser[]> {
    try {
      const b2cUsers: AdB2cUser[] = [];
      const keys = externalIds.map((externalId) => makeADB2CKey(externalId));

      // 取得対象が0件だとmgetがエラーとなるので、1件以上のときに処理
      if (keys.length !== 0) {
        // redisからキャッシュされているユーザーを取得
        let cacheUserObjects: {
          key: string;
          value: AdB2cUser;
        }[] = [];
        const mgetAsync = promisify(redisClient.mget).bind(redisClient);
        try {
          const values = await mgetAsync(...keys);
          cacheUserObjects = values.map((value, index) => ({
            key: keys[index],
            value: value ? JSON.parse(value) : null,
          }));
          context.log("mget Result:", cacheUserObjects);
        } catch (error) {
          context.error("mget Error:", error);
        }

        // キャッシュ上に存在していれば、キャッシュから取得する
        const cachedUsers = cacheUserObjects.flatMap((x) =>
          x.value ? [x.value] : []
        );
        if (cachedUsers.length > 0) {
          context.log(
            `[CACHE HIT] ids: ${cachedUsers.map((x) => x.id).join(",")}`
          );
        }

        // キャッシュ上に存在していなければ、ADB2Cから取得する
        const queryExternalIds = cacheUserObjects
          .filter((x) => x.value === null)
          .map((x) => restoreAdB2cID(x.key));
        const chunkExternalIds = splitArrayInChunksOfFifteen(queryExternalIds);

        for (let index = 0; index < chunkExternalIds.length; index++) {
          const element = chunkExternalIds[index];
          const res: AdB2cResponse = await this.graphClient
            .api(`users/`)
            .select(["id", "displayName", "identities"])
            .filter(`id in (${element.map((y) => `'${y}'`).join(",")})`)
            .get();

          b2cUsers.push(...res.value);

          // 取得したユーザーをキャッシュに保存する
          const users = res.value.map((user) => {
            const key = makeADB2CKey(user.id);
            return {
              key: key,
              value: user,
            };
          });
          try {
            const setexAsync = promisify(redisClient.setex).bind(redisClient);
            const ttl = process.env.ADB2C_CACHE_TTL;
            users.map(async (x) => {
              await setexAsync(x.key, ttl, JSON.stringify(x.value));
              context.log(
                "setex Result:",
                `key:${x.key},ttl:${ttl},value:${JSON.stringify(x.value)}`
              );
            });
          } catch (error) {
            context.error("setex Error:", error);
          }

          context.log(
            `[ADB2C GET] externalIds: ${res.value?.map((x) => x.id).join(",")}`
          );
        }

        return [...cachedUsers, ...b2cUsers];
      } else {
        return b2cUsers;
      }
    } catch (e) {
      const { statusCode } = e;
      if (statusCode === 429) {
        throw new Adb2cTooManyRequestsError();
      }
      throw e;
    } finally {
    }
  }
}

const splitArrayInChunksOfFifteen = (arr: string[]): string[][] => {
  const result: string[][] = [];
  const chunkSize = 15; // SDKの制限数
  for (let i = 0; i < arr.length; i += chunkSize) {
    result.push(arr.slice(i, i + chunkSize));
  }
  return result;
};
