/*
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */

import * as Constants from "../../utils/Constants.js";
import { Authority } from "../../authority/Authority.js";
import { ICrypto } from "../../crypto/ICrypto.js";
import { ClientInfo, buildClientInfo } from "../../account/ClientInfo.js";
import {
    AccountInfo,
    TenantProfile,
    buildTenantProfile,
    DataBoundary,
} from "../../account/AccountInfo.js";
import {
    createClientAuthError,
    ClientAuthErrorCodes,
} from "../../error/ClientAuthError.js";
import { AuthorityType } from "../../authority/AuthorityType.js";
import { Logger } from "../../logger/Logger.js";
import {
    TokenClaims,
    getTenantIdFromIdTokenClaims,
} from "../../account/TokenClaims.js";
import { ProtocolMode } from "../../authority/ProtocolMode.js";
import { AccountEntity } from "../entities/AccountEntity.js";

/**
 * Generate Account Id key component as per the schema: <home_account_id>-<environment>
 * @internal
 */
export function generateAccountId(accountEntity: AccountEntity): string {
    const accountId: Array<string> = [
        accountEntity.homeAccountId,
        accountEntity.environment,
    ];
    return accountId.join(Constants.CACHE_KEY_SEPARATOR).toLowerCase();
}

/**
 * Returns the AccountInfo interface for this account.
 * @internal
 */
export function getAccountInfo(accountEntity: AccountEntity): AccountInfo {
    const tenantProfiles = accountEntity.tenantProfiles || [];
    // Ensure at least the home tenant profile exists
    if (
        tenantProfiles.length === 0 &&
        accountEntity.realm &&
        accountEntity.localAccountId
    ) {
        tenantProfiles.push(
            buildTenantProfile(
                accountEntity.homeAccountId,
                accountEntity.localAccountId,
                accountEntity.realm,
                accountEntity.nativeAccountId
            )
        );
    }
    // Resolve nativeAccountId from the home tenant profile first, fall back to top-level (deprecated) for old cache entries
    const homeTenantProfile = tenantProfiles.find(
        (tp) => tp.tenantId === accountEntity.realm
    );
    const nativeAccountId =
        homeTenantProfile?.nativeAccountId || accountEntity.nativeAccountId;

    return {
        homeAccountId: accountEntity.homeAccountId,
        environment: accountEntity.environment,
        tenantId: accountEntity.realm,
        username: accountEntity.username,
        localAccountId: accountEntity.localAccountId,
        loginHint: accountEntity.loginHint,
        name: accountEntity.name,
        nativeAccountId: nativeAccountId,
        authorityType: accountEntity.authorityType,
        // Deserialize tenant profiles array into a Map
        tenantProfiles: new Map(
            tenantProfiles.map((tenantProfile) => {
                return [tenantProfile.tenantId, tenantProfile];
            })
        ),
        dataBoundary: accountEntity.dataBoundary,
    };
}

/**
 * Returns true if the account entity is in single tenant format (outdated), false otherwise
 * @internal
 */
export function isSingleTenant(accountEntity: AccountEntity): boolean {
    return !accountEntity.tenantProfiles;
}

/**
 * Build Account cache from IdToken, clientInfo and authority/policy. Associated with AAD.
 * @param accountDetails
 * @internal
 */
export function createAccountEntity(
    accountDetails: {
        homeAccountId: string;
        idTokenClaims?: TokenClaims;
        clientInfo?: string;
        cloudGraphHostName?: string;
        msGraphHost?: string;
        environment?: string;
        nativeAccountId?: string;
        tenantProfiles?: Array<TenantProfile>;
    },
    authority: Authority,
    correlationId: string,
    base64Decode?: (input: string) => string
): AccountEntity {
    let authorityType;
    if (authority.authorityType === AuthorityType.Adfs) {
        authorityType = Constants.CACHE_ACCOUNT_TYPE_ADFS;
    } else if (authority.protocolMode === ProtocolMode.OIDC) {
        authorityType = Constants.CACHE_ACCOUNT_TYPE_GENERIC;
    } else {
        authorityType = Constants.CACHE_ACCOUNT_TYPE_MSSTS;
    }

    let clientInfo: ClientInfo | undefined;
    let dataBoundary: DataBoundary | undefined;

    if (accountDetails.clientInfo && base64Decode) {
        clientInfo = buildClientInfo(accountDetails.clientInfo, base64Decode);
        if (clientInfo.xms_tdbr) {
            dataBoundary = clientInfo.xms_tdbr === "EU" ? "EU" : "None";
        }
    }

    const env =
        accountDetails.environment ||
        (authority && authority.getPreferredCache());

    if (!env) {
        throw createClientAuthError(
            ClientAuthErrorCodes.invalidCacheEnvironment,
            correlationId
        );
    }

    /*
     * In B2C scenarios the emails claim is used instead of preferred_username and it is an array.
     * In most cases it will contain a single email. This field should not be relied upon if a custom
     * policy is configured to return more than 1 email.
     */
    const preferredUsername =
        accountDetails.idTokenClaims?.preferred_username ||
        accountDetails.idTokenClaims?.upn;
    const email = accountDetails.idTokenClaims?.emails
        ? accountDetails.idTokenClaims.emails[0]
        : null;

    const username = preferredUsername || email || "";

    const loginHint = accountDetails.idTokenClaims?.login_hint;

    const realm =
        clientInfo?.utid ||
        getTenantIdFromIdTokenClaims(accountDetails.idTokenClaims) ||
        ""; // non-AAD scenarios can have empty realm
    // How do you account for MSA CID here?
    const localAccountId =
        clientInfo?.uid ||
        accountDetails.idTokenClaims?.oid ||
        accountDetails.idTokenClaims?.sub ||
        "";
    let tenantProfiles;
    if (accountDetails.tenantProfiles) {
        tenantProfiles = accountDetails.tenantProfiles;
    } else {
        const tenantProfile = buildTenantProfile(
            accountDetails.homeAccountId,
            localAccountId,
            realm,
            accountDetails.nativeAccountId,
            accountDetails.idTokenClaims
        );
        tenantProfiles = [tenantProfile];
    }

    return {
        homeAccountId: accountDetails.homeAccountId,
        environment: env,
        realm: realm,
        localAccountId: localAccountId,
        username: username,
        authorityType: authorityType,
        loginHint: loginHint,
        clientInfo: accountDetails.clientInfo,
        name: accountDetails.idTokenClaims?.name || "",
        lastModificationTime: undefined,
        lastModificationApp: undefined,
        cloudGraphHostName: accountDetails.cloudGraphHostName,
        msGraphHost: accountDetails.msGraphHost,
        nativeAccountId: accountDetails.nativeAccountId,
        tenantProfiles: tenantProfiles,
        dataBoundary,
    } as AccountEntity;
}

/**
 * Creates an AccountEntity object from AccountInfo
 * @param accountInfo
 * @param cloudGraphHostName
 * @param msGraphHost
 * @returns
 * @internal
 */
export function createAccountEntityFromAccountInfo(
    accountInfo: AccountInfo,
    cloudGraphHostName?: string,
    msGraphHost?: string
): AccountEntity {
    // Serialize tenant profiles map into an array
    const tenantProfiles = Array.from(
        accountInfo.tenantProfiles?.values() || []
    );
    // Ensure at least the home tenant profile exists
    if (
        tenantProfiles.length === 0 &&
        accountInfo.tenantId &&
        accountInfo.localAccountId
    ) {
        tenantProfiles.push(
            buildTenantProfile(
                accountInfo.homeAccountId,
                accountInfo.localAccountId,
                accountInfo.tenantId,
                accountInfo.nativeAccountId,
                accountInfo.idTokenClaims
            )
        );
    } else if (accountInfo.nativeAccountId) {
        // Ensure nativeAccountId is set on the matching tenant profile
        const matchingProfile = tenantProfiles.find(
            (tp) => tp.tenantId === accountInfo.tenantId
        );
        if (matchingProfile && !matchingProfile.nativeAccountId) {
            matchingProfile.nativeAccountId = accountInfo.nativeAccountId;
        }
    }
    return {
        authorityType:
            accountInfo.authorityType || Constants.CACHE_ACCOUNT_TYPE_GENERIC,
        homeAccountId: accountInfo.homeAccountId,
        localAccountId: accountInfo.localAccountId,
        nativeAccountId: accountInfo.nativeAccountId,
        realm: accountInfo.tenantId,
        environment: accountInfo.environment,
        username: accountInfo.username,
        loginHint: accountInfo.loginHint,
        name: accountInfo.name,
        cloudGraphHostName: cloudGraphHostName,
        msGraphHost: msGraphHost,
        tenantProfiles: tenantProfiles,
        dataBoundary: accountInfo.dataBoundary,
    } as AccountEntity;
}

/**
 * Generate HomeAccountId from server response
 * @param serverClientInfo
 * @param authType
 */
export function generateHomeAccountId(
    serverClientInfo: string,
    authType: AuthorityType,
    logger: Logger,
    cryptoObj: ICrypto,
    correlationId: string,
    idTokenClaims?: TokenClaims
): string {
    // since ADFS/DSTS do not have tid and does not set client_info
    if (!(authType === AuthorityType.Adfs || authType === AuthorityType.Dsts)) {
        // for cases where there is clientInfo
        if (serverClientInfo) {
            try {
                const clientInfo = buildClientInfo(
                    serverClientInfo,
                    cryptoObj.base64Decode
                );
                if (clientInfo.uid && clientInfo.utid) {
                    return `${clientInfo.uid}.${clientInfo.utid}`;
                }
            } catch (e) {}
        }
        logger.warning("No client info in response", correlationId);
    }

    // default to "sub" claim
    return idTokenClaims?.sub || "";
}

/**
 * Validates an entity: checks for all expected params
 * @param entity
 * @internal
 */
export function isAccountEntity(entity: object): entity is AccountEntity {
    if (!entity) {
        return false;
    }

    return (
        entity.hasOwnProperty("homeAccountId") &&
        entity.hasOwnProperty("environment") &&
        entity.hasOwnProperty("realm") &&
        entity.hasOwnProperty("localAccountId") &&
        entity.hasOwnProperty("username") &&
        entity.hasOwnProperty("authorityType")
    );
}
