import { ClientSecretCredential } from '@azure/identity';
import { Client } from '@microsoft/microsoft-graph-client';
import { TokenCredentialAuthenticationProvider } from '@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';
import { Aadb2cUser, B2cMetadata, JwkSignKey } from '../../common/token';
import { AdB2cResponse, AdB2cUser } from './types/types';
import { Context } from '../../common/log';

export type ConflictError = {
  reason: 'email';
  message: string;
};

export class Adb2cTooManyRequestsError extends Error {}

export const isConflictError = (arg: unknown): arg is ConflictError => {
  const value = arg as ConflictError;
  if (value.message === undefined) {
    return false;
  }
  if (value.reason === 'email') {
    return true;
  }
  return false;
};

@Injectable()
export class AdB2cService {
  private readonly logger = new Logger(AdB2cService.name);
  private readonly tenantName = this.configService.get<string>('TENANT_NAME');
  private readonly flowName =
    this.configService.get<string>('SIGNIN_FLOW_NAME');
  private graphClient: Client;

  constructor(private readonly configService: ConfigService) {
    // ADB2Cへの認証情報
    const credential = new ClientSecretCredential(
      this.configService.get('ADB2C_TENANT_ID'),
      this.configService.get('ADB2C_CLIENT_ID'),
      this.configService.get('ADB2C_CLIENT_SECRET'),
    );
    const authProvider = new TokenCredentialAuthenticationProvider(credential, {
      scopes: ['https://graph.microsoft.com/.default'],
    });

    this.graphClient = Client.initWithMiddleware({ authProvider });
  }

  /**
   * Creates user AzureADB2Cにユーザーを追加する
   * @param email 管理ユーザーのメールアドレス
   * @param password 管理ユーザーのパスワード
   * @param username 管理ユーザーの名前
   * @returns user
   */
  async createUser(
    email: string,
    password: string,
    username: string,
  ): Promise<{ sub: string } | ConflictError> {
    this.logger.log(`[IN] ${this.createUser.name}`);
    try {
      // ユーザをADB2Cに登録
      const newUser = await this.graphClient.api('users/').post({
        accountEnabled: true,
        displayName: username,
        passwordPolicies: 'DisableStrongPassword',
        passwordProfile: {
          forceChangePasswordNextSignIn: false,
          password: password,
        },
        identities: [
          {
            signinType: 'emailAddress',
            issuer: `${this.tenantName}.onmicrosoft.com`,
            issuerAssignedId: email,
          },
        ],
      });
      return { sub: newUser.id };
    } catch (e) {
      this.logger.error(e);
      if (e?.statusCode === 400 && e?.body) {
        const error = JSON.parse(e.body);

        // エラーが競合エラーである場合は、メールアドレス重複としてエラーを返す
        if (error?.details?.find((x) => x.code === 'ObjectConflict')) {
          return { reason: 'email', message: 'ObjectConflict' };
        }
      }

      throw e;
    } finally {
      this.logger.log(`[OUT] ${this.createUser.name}`);
    }
  }

  /**
   * ADB2Cのメタデータを取得する
   * @returns meta data
   */
  async getMetaData(): Promise<B2cMetadata> {
    this.logger.log(`[IN] ${this.getMetaData.name}`);
    try {
      // Azure AD B2Cのメタデータを取得する。 以下のURLから取得できる。
      // https://<テナント名>.b2clogin.com/<テナント名>.onmicrosoft.com/<ユーザーフロー名>/v2.0/.well-known/openid-configuration
      const metaData = await axios
        .get(
          `https://${this.tenantName}.b2clogin.com/${this.tenantName}.onmicrosoft.com/${this.flowName}/v2.0/.well-known/openid-configuration`,
        )
        .then((res) => res.data);

      return metaData;
    } catch (e) {
      this.logger.error(`error=${e}`);
      throw e;
    } finally {
      this.logger.log(`[OUT] ${this.getMetaData.name}`);
    }
  }

  /**
   * IDトークンの署名キーセットを取得する
   * @returns sign key sets
   */
  async getSignKeySets(): Promise<JwkSignKey[]> {
    this.logger.log(`[IN] ${this.getSignKeySets.name}`);
    try {
      // 署名キーのキーセット配列を取得する。 以下のURLから取得できる。
      const keySets = await axios
        .get(
          `https://${this.tenantName}.b2clogin.com/${this.tenantName}.onmicrosoft.com/${this.flowName}/discovery/v2.0/keys`,
        )
        .then((res) => res.data.keys);

      return keySets;
    } catch (e) {
      this.logger.error(`error=${e}`);
      throw e;
    } finally {
      this.logger.log(`[OUT] ${this.getSignKeySets.name}`);
    }
  }

  /**
   * change Password 特定のADB2Cのユーザのパスワードを変更する
   * @param externalId ユーザ情報
   * @param password パスワード
   */
  async changePassword(externalId: string, password: string): Promise<void> {
    this.logger.log(`[IN] ${this.changePassword.name}`);
    try {
      // ADB2Cのユーザのパスワードを変更する
      await this.graphClient.api(`/users/${externalId}`).patch({
        passwordProfile: {
          forceChangePasswordNextSignIn: false,
          password: password,
        },
      });
    } catch (e) {
      this.logger.error(e);
      throw e;
    } finally {
      this.logger.log(`[OUT] ${this.changePassword.name}`);
    }
  }

  /**
   * Azure AD B2Cからユーザ情報を取得する
   * @param externalId 外部ユーザーID
   * @returns ユーザ情報
   */
  async getUser(externalId: string): Promise<Aadb2cUser> {
    this.logger.log(`[IN] ${this.getUser.name}`);

    try {
      return await this.graphClient.api(`users/${externalId}`).get();
    } catch (e) {
      this.logger.error(e);
      throw e;
    } finally {
      this.logger.log(`[OUT] ${this.getUser.name}`);
    }
  }
  /**
   * Gets users
   * @param externalIds
   * @returns users
   */
  async getUsers(
    context: Context,
    externalIds: string[],
  ): Promise<AdB2cUser[]> {
    this.logger.log(
      `[IN] [${context.trackingId}] ${
        this.getUsers.name
      } | params: { externalIds:[${externalIds.join(',')}] };`,
    );

    /* 
    TODO [Task2002] 現状の実装だと１リクエストで最大15パラメータまでしか設定できないため、
    別タスクでアカウント単位の検索用パラメータを用いて取得するように修正する。
    タスク 2002: B2Cからの名前取得をより低コストで行えるように修正する
    */
    const chunkExternalIds = splitArrayInChunksOfFifteen(externalIds);

    try {
      const b2cUsers: AdB2cUser[] = [];
      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);
      }

      return b2cUsers;
    } catch (e) {
      this.logger.error(e);
      const { statusCode } = e;
      if (statusCode === 429) {
        throw new Adb2cTooManyRequestsError();
      }

      throw e;
    } finally {
      this.logger.log(`[OUT] [${context.trackingId}] ${this.getUsers.name}`);
    }
  }
}

// TODO [Task2002] 文字列の配列を１５要素ずつ区切る（この処理も別タスクで削除予定）
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;
};
