import EventEmitter from 'eventemitter3';
import { getAuthenticationToken, storeAuthTokenInCache, getCacheAuthToken } from './authToken';
import { getEmbedConfig } from './embed/embedConfig';
import { initMixpanel } from './mixpanel-service';
import {
    AuthType, DOMSelector, EmbedConfig, EmbedEvent,
} from './types';
import { getDOMNode, getRedirectUrl, getSSOMarker } from './utils';
import {
    EndPoints,
    fetchAuthPostService,
    fetchAuthService,
    fetchBasicAuthService,
    fetchLogoutService,
} from './utils/authService';
import { isActiveService } from './utils/authService/tokenizedAuthService';
import { logger } from './utils/logger';
import { getSessionInfo, getPreauthInfo } from './utils/sessionInfoService';
import { ERROR_MESSAGE } from './errors';
import { resetAllCachedServices } from './utils/resetServices';

 
export let loggedInStatus = false;
 
export let samlAuthWindow: Window = null;
 
export let samlCompletionPromise: Promise<void> = null;

let releaseVersion = '';

export const SSO_REDIRECTION_MARKER_GUID = '5e16222e-ef02-43e9-9fbd-24226bf3ce5b';

/**
 * Enum for auth failure types.
 * This value is passed to the listener for {@link AuthStatus.FAILURE}.
 * @group Authentication / Init
 */
export enum AuthFailureType {
    /**
     * Authentication failed in the SDK authentication flow.
     *
     * Emitted when `init()` or auto-authentication cannot establish a logged-in session.
     * For example, this can happen because of an invalid token, an auth request failure,
     * or an auth promise rejection.
     */
    SDK = 'SDK',
    /**
     * Browser cookie access is blocked for the embedded app.
     *
     * Emitted when the iframe reports that required cookies
     * cannot be read or sent, commonly due to third-party cookie restrictions.
     */
    NO_COOKIE_ACCESS = 'NO_COOKIE_ACCESS',
    /**
     * The current authentication token or session has expired.
     *
     * Emitted when the embed receives an auth-expiry signal and starts auth refresh
     * handling.
     */
    EXPIRY = 'EXPIRY',
    /**
     * A generic authentication failure that does not match a more specific type.
     *
     * Emitted as a fallback for app-reported auth failures in standard auth flows.
     */
    OTHER = 'OTHER',
    /**
     * The user session timed out due to inactivity.
     *
     * Emitted when the app reports an idle-session timeout.
     */
    IDLE_SESSION_TIMEOUT = 'IDLE_SESSION_TIMEOUT',
    /**
     * The app reports that the user is unauthenticated.
     *
     * Used primarily to classify unauthenticated failures in Embedded SSO flows.
     */
    UNAUTHENTICATED_FAILURE = 'UNAUTHENTICATED_FAILURE',
}

/**
 * Enum for auth status emitted by the emitter returned from {@link init}.
 * @group Authentication / Init
 */
export enum AuthStatus {
    /**
     * Emits when the SDK fails to authenticate.
     */
    FAILURE = 'FAILURE',
    /**
     * Emits when the SDK authentication step completes
     * successfully (e.g., token exchange, cookie set).
     * This fires before any iframe is rendered. Use
     * this to know that auth passed and it is safe to
     * proceed with rendering. The callback receives no
     * arguments.
     * @example
     * ```js
     * const authEE = init({ ... });
     * authEE.on(AuthStatus.SDK_SUCCESS, () => {
     *     // Auth done, iframe not loaded yet
     * });
     * ```
     */
    SDK_SUCCESS = 'SDK_SUCCESS',
    /**
     * @hidden
     * Emits when iframe is loaded and session
     * information is available.
     */
    SESSION_INFO_SUCCESS = 'SESSION_INFO_SUCCESS',
    /**
     * Emits when the ThoughtSpot app inside the
     * embedded iframe confirms its session is active.
     * This fires after the iframe loads and sends back an `AuthInit` event.
     * @param sessionInfo Information about the user session, with details like `userGUID`.
     * @see EmbedEvent.AuthInit
     * @example
     * ```js
     * const authEE = init({ ... });
     * authEE.on(AuthStatus.SUCCESS, (sessionInfo) => {
     *     // App is loaded and authenticated
     *     console.log(sessionInfo.userGUID);
     * });
     * ```
     */
    SUCCESS = 'SUCCESS',
    /**
     * Emits when a user logs out
     */
    LOGOUT = 'LOGOUT',
    /**
     * Emitted when inPopup is true in the SAMLRedirect flow and the
     * popup is waiting to be triggered either programmatically
     * or by the trigger button.
     * @version SDK: 1.19.0
     */
    WAITING_FOR_POPUP = 'WAITING_FOR_POPUP',

    /**
     * Emitted when the SAML popup is closed without authentication
     */
    SAML_POPUP_CLOSED_NO_AUTH = 'SAML_POPUP_CLOSED_NO_AUTH',
}

/**
 * Event emitter returned from {@link init}.
 * @group Authentication / Init
 */
export interface AuthEventEmitter {
    /**
     * Register a listener on Auth failure.
     * @param event
     * @param listener
     */
    on(event: AuthStatus.FAILURE, listener: (failureType: AuthFailureType) => void): this;
    /**
     * Register a listener on Auth SDK success.
     * @param event
     * @param listener
     */
    on(
        event: AuthStatus.SDK_SUCCESS | AuthStatus.LOGOUT | AuthStatus.WAITING_FOR_POPUP | AuthStatus.SAML_POPUP_CLOSED_NO_AUTH,
        listener: () => void,
    ): this;
    on(event: AuthStatus.SUCCESS, listener: (sessionInfo: any) => void): this;
    once(event: AuthStatus.FAILURE, listener: (failureType: AuthFailureType) => void): this;
    once(
        event: AuthStatus.SDK_SUCCESS | AuthStatus.LOGOUT | AuthStatus.WAITING_FOR_POPUP | AuthStatus.SAML_POPUP_CLOSED_NO_AUTH,
        listener: () => void,
    ): this;
    once(event: AuthStatus.SUCCESS, listener: (sessionInfo: any) => void): this;
    /**
     * Trigger an event on the emitter returned from init.
     * @param {@link AuthEvent}
     */
    emit(event: AuthEvent, ...args: any[]): boolean;
    /**
     * Remove listener from the emitter returned from init.
     * @param event
     * @param listener
     * @param context
     * @param once
     */
    off(event: AuthStatus, listener: (...args: any[]) => void, context: any, once: boolean): this;
    /**
     * Remove all the event listeners
     * @param event
     */
    removeAllListeners(event: AuthStatus): this;
}

/**
 * Events which can be triggered on the emitter returned from {@link init}.
 * @group Authentication / Init
 */
export enum AuthEvent {
    /**
     * Manually trigger the SSO popup. This is useful when
     * authStatus is SAMLRedirect/OIDCRedirect and inPopup is set to true
     */
    TRIGGER_SSO_POPUP = 'TRIGGER_SSO_POPUP',
}

let authEE: EventEmitter<AuthStatus | AuthEvent>;

/**
 *
 */
export function getAuthEE(): EventEmitter<AuthStatus | AuthEvent> {
    return authEE;
}

/**
 *
 * @param eventEmitter
 */
export function setAuthEE(eventEmitter: EventEmitter<AuthStatus | AuthEvent>): void {
    authEE = eventEmitter;
}

/**
 *
 */
export function notifyAuthSDKSuccess(): void {
    if (!authEE) {
        logger.error(ERROR_MESSAGE.SDK_NOT_INITIALIZED);
        return;
    }
    authEE.emit(AuthStatus.SDK_SUCCESS);
}

/**
 *
 */
export async function notifyAuthSuccess(): Promise<void> {
    if (!authEE) {
        logger.error(ERROR_MESSAGE.SDK_NOT_INITIALIZED);
        return;
    }
    try {
        getPreauthInfo();
        const sessionInfo = await getSessionInfo();
        authEE.emit(AuthStatus.SUCCESS, sessionInfo);
    } catch (e) {
        logger.error(ERROR_MESSAGE.SESSION_INFO_FAILED);
    }
}

/**
 *
 * @param failureType
 */
export function notifyAuthFailure(failureType: AuthFailureType): void {
    if (!authEE) {
        logger.error(ERROR_MESSAGE.SDK_NOT_INITIALIZED);
        return;
    }
    authEE.emit(AuthStatus.FAILURE, failureType);
}

/**
 *
 */
export function notifyLogout(): void {
    if (!authEE) {
        logger.error(ERROR_MESSAGE.SDK_NOT_INITIALIZED);
        return;
    }
    authEE.emit(AuthStatus.LOGOUT);
}

/**
 * Check if we are logged into the ThoughtSpot cluster
 * @param thoughtSpotHost The ThoughtSpot cluster hostname or IP
 */
async function isLoggedIn(thoughtSpotHost: string): Promise<boolean> {
    try {
        const response = await isActiveService(thoughtSpotHost);
        return response;
    } catch (e) {
        return false;
    }
}

/**
 * Services to be called after the login is successful,
 * This should be called after the cookie is set for cookie auth or
 * after the token is set for cookieless.
 * @return {Promise<void>}
 * @example
 * ```js
 * await postLoginService();
 * ```
 * @version SDK: 1.28.3 | ThoughtSpot: *
 */
export async function postLoginService(): Promise<void> {
    try {
        getPreauthInfo();
        const sessionInfo = await getSessionInfo();
        releaseVersion = sessionInfo.releaseVersion;
        const embedConfig = getEmbedConfig();
        if (!embedConfig.disableSDKTracking) {
            initMixpanel(sessionInfo);
        }
    } catch (e) {
        logger.error('Post login services failed.', e.message, e);
    }
}

/**
 * Return releaseVersion if available
 */
export function getReleaseVersion() {
    return releaseVersion;
}

/**
 * Check if we are stuck at the SSO redirect URL
 */
function isAtSSORedirectUrl(): boolean {
    return window.location.href.indexOf(getSSOMarker(SSO_REDIRECTION_MARKER_GUID)) >= 0;
}

/**
 * Remove the SSO redirect URL marker
 */
function removeSSORedirectUrlMarker(): void {
    // Note (sunny): This will leave a # around even if it was not in the URL
    // to begin with. Trying to remove the hash by changing window.location will
    // reload the page which we don't want. We'll live with adding an
    // unnecessary hash to the parent page URL until we find any use case where
    // that creates an issue.

    // Replace any occurrences of ?ssoMarker=guid or &ssoMarker=guid.
    let updatedHash = window.location.hash.replace(`?${getSSOMarker(SSO_REDIRECTION_MARKER_GUID)}`, '');
    updatedHash = updatedHash.replace(`&${getSSOMarker(SSO_REDIRECTION_MARKER_GUID)}`, '');
    window.location.hash = updatedHash;
}

/**
 * Perform token based authentication
 * @param embedConfig The embed configuration
 */
export const doTokenAuth = async (embedConfig: EmbedConfig): Promise<boolean> => {
    const {
        thoughtSpotHost, username, authEndpoint, getAuthToken,
    } = embedConfig;
    if (!authEndpoint && !getAuthToken) {
        throw new Error('Either auth endpoint or getAuthToken function must be provided');
    }
    loggedInStatus = await isLoggedIn(thoughtSpotHost);

    if (!loggedInStatus) {
        let authToken: string;
        try {
            authToken = await getAuthenticationToken(embedConfig);
        } catch (e) {
            loggedInStatus = false;
            throw e;
        }
        let resp;
        try {
            resp = await fetchAuthPostService(thoughtSpotHost, username, authToken);
        } catch (e) {
            resp = await fetchAuthService(thoughtSpotHost, username, authToken);
        }
        // token login issues a 302 when successful
        loggedInStatus = resp.ok || resp.type === 'opaqueredirect';
        if (loggedInStatus && embedConfig.detectCookieAccessSlow) {
            // When 3rd party cookie access is blocked, this will fail because
            // cookies will not be sent with the call.
            loggedInStatus = await isLoggedIn(thoughtSpotHost);
        }
    }
    return loggedInStatus;
};

/**
 * Validate embedConfig parameters required for cookielessTokenAuth
 * @param embedConfig The embed configuration
 */
export const doCookielessTokenAuth = async (embedConfig: EmbedConfig): Promise<boolean> => {
    const { authEndpoint, getAuthToken } = embedConfig;
    if (!authEndpoint && !getAuthToken) {
        throw new Error('Either auth endpoint or getAuthToken function must be provided');
    }
    let authSuccess = false;
    try {
        const authToken = await getAuthenticationToken(embedConfig);
        if (authToken) {
            authSuccess = true;
        }
    } catch {
        authSuccess = false;
    }

    return authSuccess;
};

/**
 * Perform basic authentication to the ThoughtSpot cluster using the cluster
 * credentials.
 *
 * Warning: This feature is primarily intended for developer testing. It is
 * strongly advised not to use this authentication method in production.
 * @param embedConfig The embed configuration
 */
export const doBasicAuth = async (embedConfig: EmbedConfig): Promise<boolean> => {
    const { thoughtSpotHost, username, password } = embedConfig;
    const loggedIn = await isLoggedIn(thoughtSpotHost);
    if (!loggedIn) {
        const response = await fetchBasicAuthService(thoughtSpotHost, username, password);
        loggedInStatus = response.ok;
        if (embedConfig.detectCookieAccessSlow) {
            loggedInStatus = await isLoggedIn(thoughtSpotHost);
        }
    } else {
        loggedInStatus = true;
    }
    return loggedInStatus;
};

/**
 *
 * @param ssoURL
 * @param triggerContainer
 * @param triggerText
 */
async function samlPopupFlow(ssoURL: string, triggerContainer: DOMSelector, triggerText: string) {
    let popupClosedCheck: NodeJS.Timeout;
    const openPopup = () => {
        if (samlAuthWindow === null || samlAuthWindow.closed) {
            samlAuthWindow = window.open(
                ssoURL,
                '_blank',
                'location=no,height=570,width=520,scrollbars=yes,status=yes',
            );
            if (samlAuthWindow) {
                popupClosedCheck = setInterval(() => {
                    if (samlAuthWindow.closed) {
                        clearInterval(popupClosedCheck);
                        if (samlCompletionPromise && !samlCompletionResolved) {
                            authEE?.emit(AuthStatus.SAML_POPUP_CLOSED_NO_AUTH);
                        }
                    }
                }, 500);
            }
        } else {
            samlAuthWindow.focus();
        }
    };
    let samlCompletionResolved = false;
    authEE?.emit(AuthStatus.WAITING_FOR_POPUP);
    const containerEl = getDOMNode(triggerContainer);
    if (containerEl) {
        containerEl.innerHTML = '<button id="ts-auth-btn" class="ts-auth-btn" style="margin: auto;"></button>';
        const authElem = document.getElementById('ts-auth-btn');
        authElem.textContent = triggerText;
        authElem.addEventListener('click', openPopup, { once: true });
    }
    samlCompletionPromise = samlCompletionPromise || new Promise<void>((resolve, reject) => {
        window.addEventListener('message', (e) => {
            if (e.data.type === EmbedEvent.SAMLComplete) {
                if (e.data.accessToken) {
                    const decodedToken = decodeURIComponent(e.data.accessToken);
                    storeAuthTokenInCache(decodedToken);
                }
                samlCompletionResolved = true;
                if (popupClosedCheck) {
                    clearInterval(popupClosedCheck);
                }
                (e.source as Window).close();
                resolve();
            }
        });
    });

    authEE?.once(AuthEvent.TRIGGER_SSO_POPUP, openPopup);
    return samlCompletionPromise;
}

/**
 * Perform SAML authentication
 * @param embedConfig The embed configuration
 * @param ssoEndPoint
 */
const doSSOAuth = async (embedConfig: EmbedConfig, ssoEndPoint: string): Promise<void> => {
    const { thoughtSpotHost } = embedConfig;
    const loggedIn = await isLoggedIn(thoughtSpotHost);
    if (loggedIn) {
        if (isAtSSORedirectUrl()) {
            removeSSORedirectUrlMarker();
        }
        loggedInStatus = true;
        return;
    }

    // we have already tried authentication and it did not succeed, restore
    // the current URL to the original one and invoke the callback.
    if (isAtSSORedirectUrl()) {
        removeSSORedirectUrlMarker();
        loggedInStatus = false;
        return;
    }

    const ssoURL = `${thoughtSpotHost}${ssoEndPoint}`;
    if (embedConfig.inPopup) {
        await samlPopupFlow(ssoURL, embedConfig.authTriggerContainer, embedConfig.authTriggerText);
        const cachedToken = getCacheAuthToken();
        if (cachedToken) {
            loggedInStatus = true;
        } else {
            loggedInStatus = await isLoggedIn(thoughtSpotHost);
        }
        return;
    }

    window.location.href = ssoURL;
};

export const doSamlAuth = async (embedConfig: EmbedConfig) => {
    const { thoughtSpotHost } = embedConfig;
    // redirect for SSO, when the SSO authentication is done, this page will be
    // loaded again and the same JS will execute again.
    const ssoRedirectUrl = embedConfig.inPopup
        ? `${thoughtSpotHost}/v2/#/embed/saml-complete`
        : getRedirectUrl(
            window.location.href,
            SSO_REDIRECTION_MARKER_GUID,
            embedConfig.redirectPath,
        );

    // bring back the page to the same URL
    const ssoEndPoint = `${EndPoints.SAML_LOGIN_TEMPLATE(encodeURIComponent(ssoRedirectUrl))}`;

    await doSSOAuth(embedConfig, ssoEndPoint);
    return loggedInStatus;
};

export const doOIDCAuth = async (embedConfig: EmbedConfig) => {
    const { thoughtSpotHost } = embedConfig;
    // redirect for SSO, when the SSO authentication is done, this page will be
    // loaded again and the same JS will execute again.
    const ssoRedirectUrl = embedConfig.noRedirect || embedConfig.inPopup
        ? `${thoughtSpotHost}/v2/#/embed/saml-complete`
        : getRedirectUrl(
            window.location.href,
            SSO_REDIRECTION_MARKER_GUID,
            embedConfig.redirectPath,
        );

    // bring back the page to the same URL
    const baseEndpoint = `${EndPoints.OIDC_LOGIN_TEMPLATE(encodeURIComponent(ssoRedirectUrl))}`;
    const ssoEndPoint = `${baseEndpoint}${baseEndpoint.includes('?') ? '&' : '?'}forceSAMLAutoRedirect=true`;

    await doSSOAuth(embedConfig, ssoEndPoint);
    return loggedInStatus;
};

export const logout = async (embedConfig: EmbedConfig): Promise<boolean> => {
    const { thoughtSpotHost } = embedConfig;
    await fetchLogoutService(thoughtSpotHost);
    resetAllCachedServices();
    const thoughtspotIframes = document.querySelectorAll("[data-ts-iframe='true']");
    if (thoughtspotIframes?.length) {
        thoughtspotIframes.forEach((el) => {
            el.parentElement.innerHTML = embedConfig.loginFailedMessage;
        });
    }
    loggedInStatus = false;
    return loggedInStatus;
};

/**
 * Perform authentication on the ThoughtSpot cluster
 * @param embedConfig The embed configuration
 */
export const authenticate = async (embedConfig: EmbedConfig): Promise<boolean> => {
    const { authType } = embedConfig;
    switch (authType) {
        case AuthType.SSO:
        case AuthType.SAMLRedirect:
        case AuthType.SAML:
            return doSamlAuth(embedConfig);
        case AuthType.OIDC:
        case AuthType.OIDCRedirect:
            return doOIDCAuth(embedConfig);
        case AuthType.AuthServer:
        case AuthType.TrustedAuthToken:
            return doTokenAuth(embedConfig);
        case AuthType.TrustedAuthTokenCookieless:
            return doCookielessTokenAuth(embedConfig);
        case AuthType.Basic:
            return doBasicAuth(embedConfig);
        default:
            return Promise.resolve(true);
    }
};

/**
 * Check if we are authenticated to the ThoughtSpot cluster
 */
export const isAuthenticated = (): boolean => loggedInStatus;
