// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { Injectable } from '@angular/core';
import { Location } from '@angular/common';
import { Params } from '@angular/router';
import { NavController } from '@ionic/angular';
import { Md5 } from 'ts-md5/dist/md5';

import { CoreApp, CoreStoreConfig } from '@services/app';
import { CoreConfig } from '@services/config';
import { CoreEvents, CoreEventSessionExpiredData } from '@singletons/events';
import { CoreSites, CoreLoginSiteInfo } from '@services/sites';
import { CoreWS, CoreWSExternalWarning } from '@services/ws';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUrlParams, CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils';
import { CoreConstants } from '@/core/constants';
import { CoreSite, CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@classes/site';
import { CoreError } from '@classes/errors/error';
import { CoreWSError } from '@classes/errors/wserror';
import { makeSingleton, Translate } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger';
import { CoreUrl } from '@singletons/url';
import { NavigationOptions } from '@ionic/angular/providers/nav-controller';
import { CoreObject } from '@singletons/object';

/**
 * Helper provider that provides some common features regarding authentication.
 */
@Injectable({
    providedIn: 'root',
})
export class CoreLoginHelperProvider {

    static readonly OPEN_COURSE = 'open_course';
    static readonly ONBOARDING_DONE = 'onboarding_done';
    static readonly FAQ_URL_IMAGE_HTML = '<img src="assets/img/login/faq_url.png" role="presentation">';
    static readonly FAQ_QRCODE_IMAGE_HTML = '<img src="assets/img/login/faq_qrcode.png" role="presentation">';

    protected logger: CoreLogger;
    protected isSSOConfirmShown = false;
    protected isOpenEditAlertShown = false;
    protected pageToLoad?: {page: string; params?: Params; time: number}; // Page to load once main menu is opened.
    protected isOpeningReconnect = false;
    waitingForBrowser = false;

    constructor(
        protected location: Location,
        protected navCtrl: NavController,
    ) {
        this.logger = CoreLogger.getInstance('CoreLoginHelper');

        CoreEvents.on(CoreEvents.MAIN_MENU_OPEN, () => {
            /* If there is any page pending to be opened, do it now. Don't open pages stored more than 5 seconds ago, probably
               the function to open the page was called when it shouldn't. */
            if (this.pageToLoad && Date.now() - this.pageToLoad.time < 5000) {
                this.loadPageInMainMenu(this.pageToLoad.page, this.pageToLoad.params);
                delete this.pageToLoad;
            }
        });
    }

    /**
     * Accept site policy.
     *
     * @param siteId Site ID. If not defined, current site.
     * @return Promise resolved if success, rejected if failure.
     */
    async acceptSitePolicy(siteId?: string): Promise<void> {
        const site = await CoreSites.instance.getSite(siteId);

        const result = await site.write<AgreeSitePolicyResult>('core_user_agree_site_policy', {});

        if (!result.status) {
            // Error.
            if (result.warnings && result.warnings.length) {
                // Check if there is a warning 'alreadyagreed'.
                for (const i in result.warnings) {
                    const warning = result.warnings[i];
                    if (warning.warningcode == 'alreadyagreed') {
                        // Policy already agreed, treat it as a success.
                        return;
                    }
                }

                // Another warning, reject.
                throw new CoreWSError(result.warnings[0]);
            } else {
                throw new CoreError('Cannot agree site policy');
            }
        }
    }

    /**
     * Check if a site allows requesting a password reset through the app.
     *
     * @param siteUrl URL of the site.
     * @return Promise resolved with boolean: whether can be done through the app.
     */
    async canRequestPasswordReset(siteUrl: string): Promise<boolean> {
        try {
            await this.requestPasswordReset(siteUrl);

            return true;
        } catch (error) {
            return error.available == 1 || (error.errorcode && error.errorcode != 'invalidrecord');
        }
    }

    /**
     * Function called when an SSO InAppBrowser is closed or the app is resumed. Check if user needs to be logged out.
     */
    checkLogout(): void {
        const currentSite = CoreSites.instance.getCurrentSite();
        const currentPage = CoreApp.instance.getCurrentPage();

        if (!CoreApp.instance.isSSOAuthenticationOngoing() && currentSite?.isLoggedOut() && currentPage == '/login/reconnect') {
            // User must reauthenticate but he closed the InAppBrowser without doing so, logout him.
            CoreSites.instance.logout();
        }
    }

    /**
     * Show a confirm modal if needed and open a browser to perform SSO login.
     *
     * @param siteurl URL of the site where the SSO login will be performed.
     * @param typeOfLogin CoreConstants.LOGIN_SSO_CODE or CoreConstants.LOGIN_SSO_INAPP_CODE.
     * @param service The service to use. If not defined, external service will be used.
     * @param launchUrl The URL to open for SSO. If not defined, local_mobile launch URL will be used.
     * @return Promise resolved when done or if user cancelled.
     */
    async confirmAndOpenBrowserForSSOLogin(
        siteUrl: string,
        typeOfLogin: number,
        service?: string,
        launchUrl?: string,
    ): Promise<void> {
        // Show confirm only if it's needed. Treat "false" (string) as false to prevent typing errors.
        const showConfirmation = this.shouldShowSSOConfirm(typeOfLogin);

        if (showConfirmation) {
            try {
                await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.login.logininsiterequired'));
            } catch (error) {
                // User canceled, stop.
                return;
            }
        }

        this.openBrowserForSSOLogin(siteUrl, typeOfLogin, service, launchUrl);
    }

    /**
     * Helper function to act when the forgotten password is clicked.
     *
     * @param siteUrl Site URL.
     * @param username Username.
     * @param siteConfig Site config.
     */
    async forgottenPasswordClicked(siteUrl: string, username: string, siteConfig?: CoreSitePublicConfigResponse): Promise<void> {
        if (siteConfig && siteConfig.forgottenpasswordurl) {
            // URL set, open it.
            CoreUtils.instance.openInApp(siteConfig.forgottenpasswordurl);

            return;
        }

        // Check if password reset can be done through the app.
        const modal = await CoreDomUtils.instance.showModalLoading();

        try {
            const canReset = await this.canRequestPasswordReset(siteUrl);

            if (canReset) {
                await this.navCtrl.navigateForward(['/login/forgottenpassword'], {
                    queryParams: {
                        siteUrl,
                        username,
                    },
                });
            } else {
                this.openForgottenPassword(siteUrl);
            }
        } finally {
            modal.dismiss();
        }
    }

    /**
     * Format profile fields, filtering the ones that shouldn't be shown on signup and classifying them in categories.
     *
     * @param profileFields Profile fields to format.
     * @return Categories with the fields to show in each one.
     */
    formatProfileFieldsForSignup(profileFields?: AuthEmailSignupProfileField[]): AuthEmailSignupProfileFieldsCategory[] {
        if (!profileFields) {
            return [];
        }

        const categories: Record<number, AuthEmailSignupProfileFieldsCategory> = {};

        profileFields.forEach((field) => {
            if (!field.signup || !field.categoryid) {
                // Not a signup field, ignore it.
                return;
            }

            if (!categories[field.categoryid]) {
                categories[field.categoryid] = {
                    id: field.categoryid,
                    name: field.categoryname || '',
                    fields: [],
                };
            }

            categories[field.categoryid].fields.push(field);
        });

        return Object.keys(categories).map((index) => categories[Number(index)]);
    }

    /**
     * Get disabled features from a site public config.
     *
     * @param config Site public config.
     * @return Disabled features.
     */
    getDisabledFeatures(config?: CoreSitePublicConfigResponse): string {
        const disabledFeatures = config?.tool_mobile_disabledfeatures;
        if (!disabledFeatures) {
            return '';
        }

        return CoreTextUtils.instance.treatDisabledFeatures(disabledFeatures);
    }

    /**
     * Builds an object with error messages for some common errors.
     * Please notice that this function doesn't support all possible error types.
     *
     * @param requiredMsg Code of the string for required error.
     * @param emailMsg Code of the string for invalid email error.
     * @param patternMsg Code of the string for pattern not match error.
     * @param urlMsg Code of the string for invalid url error.
     * @param minlengthMsg Code of the string for "too short" error.
     * @param maxlengthMsg Code of the string for "too long" error.
     * @param minMsg Code of the string for min value error.
     * @param maxMsg Code of the string for max value error.
     * @return Object with the errors.
     */
    getErrorMessages(
        requiredMsg?: string,
        emailMsg?: string,
        patternMsg?: string,
        urlMsg?: string,
        minlengthMsg?: string,
        maxlengthMsg?: string,
        minMsg?: string,
        maxMsg?: string,
    ): Record<string, string> {
        const errors: Record<string, string> = {};

        if (requiredMsg) {
            errors.required = errors.requiredTrue = Translate.instance.instant(requiredMsg);
        }
        if (emailMsg) {
            errors.email = Translate.instance.instant(emailMsg);
        }
        if (patternMsg) {
            errors.pattern = Translate.instance.instant(patternMsg);
        }
        if (urlMsg) {
            errors.url = Translate.instance.instant(urlMsg);
        }
        if (minlengthMsg) {
            errors.minlength = Translate.instance.instant(minlengthMsg);
        }
        if (maxlengthMsg) {
            errors.maxlength = Translate.instance.instant(maxlengthMsg);
        }
        if (minMsg) {
            errors.min = Translate.instance.instant(minMsg);
        }
        if (maxMsg) {
            errors.max = Translate.instance.instant(maxMsg);
        }

        return errors;
    }

    /**
     * Get logo URL from a site public config.
     *
     * @param config Site public config.
     * @return Logo URL.
     */
    getLogoUrl(config: CoreSitePublicConfigResponse): string | undefined {
        return !CoreConstants.CONFIG.forceLoginLogo && config ? (config.logourl || config.compactlogourl) : undefined;
    }

    /**
     * Returns the logout label of a site.
     *
     * @param site Site. If not defined, use current site.
     * @return The string key.
     */
    getLogoutLabel(site?: CoreSite): string {
        site = site || CoreSites.instance.getCurrentSite();
        const config = site?.getStoredConfig();

        return 'core.mainmenu.' + (config && config.tool_mobile_forcelogout == '1' ? 'logout' : 'changesite');
    }

    /**
     * Get the OAuth ID of some URL params (if it has an OAuth ID).
     *
     * @param params Params.
     * @return OAuth ID.
     */
    getOAuthIdFromParams(params: CoreUrlParams): number | undefined {
        return params && typeof params.oauthsso != 'undefined' ? Number(params.oauthsso) : undefined;
    }

    /**
     * Get the site policy.
     *
     * @param siteId Site ID. If not defined, current site.
     * @return Promise resolved with the site policy.
     */
    async getSitePolicy(siteId?: string): Promise<string> {
        const site = await CoreSites.instance.getSite(siteId);

        let sitePolicy: string | undefined;

        try {
            // Try to get the latest config, maybe the site policy was just added or has changed.
            sitePolicy = await site.getConfig('sitepolicy', true);
        } catch (error) {
            // Cannot get config, try to get the site policy using auth_email_get_signup_settings.
            const settings = <AuthEmailSignupSettings> await CoreWS.instance.callAjax(
                'auth_email_get_signup_settings',
                {},
                { siteUrl: site.getURL() },
            );

            sitePolicy = settings.sitepolicy;
        }

        if (!sitePolicy) {
            throw new CoreError('Cannot retrieve site policy');
        }

        return sitePolicy;
    }

    /**
     * Get fixed site or sites.
     *
     * @return Fixed site or list of fixed sites.
     */
    getFixedSites(): string | CoreLoginSiteInfo[] {
        return CoreConstants.CONFIG.siteurl;
    }

    /**
     * Get the valid identity providers from a site config.
     *
     * @param siteConfig Site's public config.
     * @param disabledFeatures List of disabled features already treated. If not provided it will be calculated.
     * @return Valid identity providers.
     */
    getValidIdentityProviders(siteConfig?: CoreSitePublicConfigResponse, disabledFeatures?: string): CoreSiteIdentityProvider[] {
        if (!siteConfig) {
            return [];
        }
        if (this.isFeatureDisabled('NoDelegate_IdentityProviders', siteConfig, disabledFeatures)) {
            // Identity providers are disabled, return an empty list.
            return [];
        }

        const validProviders: CoreSiteIdentityProvider[] = [];
        const httpUrl = CoreTextUtils.instance.concatenatePaths(siteConfig.wwwroot, 'auth/oauth2/');
        const httpsUrl = CoreTextUtils.instance.concatenatePaths(siteConfig.httpswwwroot, 'auth/oauth2/');

        if (siteConfig.identityproviders && siteConfig.identityproviders.length) {
            siteConfig.identityproviders.forEach((provider) => {
                const urlParams = CoreUrlUtils.instance.extractUrlParams(provider.url);

                if (provider.url && (provider.url.indexOf(httpsUrl) != -1 || provider.url.indexOf(httpUrl) != -1) &&
                        !this.isFeatureDisabled('NoDelegate_IdentityProvider_' + urlParams.id, siteConfig, disabledFeatures)) {
                    validProviders.push(provider);
                }
            });
        }

        return validProviders;
    }

    /**
     * Go to the page to add a new site.
     * If a fixed URL is configured, go to credentials instead.
     *
     * @param setRoot True to set the new page as root, false to add it to the stack.
     * @param showKeyboard Whether to show keyboard in the new page. Only if no fixed URL set.
     * @return Promise resolved when done.
     */
    async goToAddSite(setRoot?: boolean, showKeyboard?: boolean): Promise<void> {
        let pageRoute: string;
        let params: Params;

        if (this.isFixedUrlSet()) {
            // Fixed URL is set, go to credentials page.
            const fixedSites = this.getFixedSites();
            const url = typeof fixedSites == 'string' ? fixedSites : fixedSites[0].url;

            pageRoute = '/login/credentials';
            params = { siteUrl: url };
        } else {
            pageRoute = '/login/site';
            params = { showKeyboard: showKeyboard };
        }

        if (setRoot) {
            await this.navCtrl.navigateRoot(pageRoute, {
                queryParams: params,
            });
        } else {
            await this.navCtrl.navigateForward(pageRoute, {
                queryParams: params,
            });
        }
    }

    /**
     * Open a page that doesn't belong to any site.
     *
     * @param page Page to open.
     * @param params Params of the page.
     * @return Promise resolved when done.
     */
    async goToNoSitePage(page: string, params?: Params): Promise<void> {
        const currentPage = CoreApp.instance.getCurrentPage();

        if (currentPage == page) {
            // Already at page, nothing to do.
        } else if (page == '/login/sites') {
            // Just open the page as root.
            await this.navCtrl.navigateRoot(page, { queryParams: params });
        } else if (page == '/login/credentials' && currentPage == '/login/site') {
            // Just open the new page to keep the navigation history.
            await this.navCtrl.navigateForward(page, { queryParams: params });
        } else {
            // Check if there is any site stored.
            const hasSites = await CoreSites.instance.hasSites();

            if (!hasSites) {
                // There are sites stored, open sites page first to be able to go back.
                await this.navCtrl.navigateRoot('/login/sites');

                await this.navCtrl.navigateForward(page, { queryParams: params });
            } else {
                if (page != '/login/site') {
                    // Open the new site page to be able to go back.
                    await this.navCtrl.navigateRoot('/login/site');

                    await this.navCtrl.navigateForward(page, { queryParams: params });
                } else {
                    // Just open the page as root.
                    await this.navCtrl.navigateRoot(page, { queryParams: params });
                }
            }
        }
    }

    /**
     * Go to the initial page of a site depending on 'userhomepage' setting.
     *
     * @param options Options.
     * @return Promise resolved when done.
     */
    goToSiteInitialPage(options?: OpenMainMenuOptions): Promise<void> {
        return this.openMainMenu(options);
    }

    /**
     * Convenient helper to handle authentication in the app using a token received by SSO login. If it's a new account,
     * the site is stored and the user is authenticated. If the account already exists, update its token.
     *
     * @param siteUrl Site's URL.
     * @param token User's token.
     * @param privateToken User's private token.
     * @param oauthId OAuth ID. Only if the authentication was using an OAuth method.
     * @return Promise resolved when the user is authenticated with the token.
     */
    handleSSOLoginAuthentication(siteUrl: string, token: string, privateToken?: string, oauthId?: number): Promise<string> {
        // Always create a new site to prevent overriding data if another user credentials were introduced.
        return CoreSites.instance.newSite(siteUrl, token, privateToken, true, oauthId);
    }

    /**
     * Check if the app is configured to use several fixed URLs.
     *
     * @return Whether there are several fixed URLs.
     */
    hasSeveralFixedSites(): boolean {
        return !!(CoreConstants.CONFIG.siteurl && Array.isArray(CoreConstants.CONFIG.siteurl) &&
            CoreConstants.CONFIG.siteurl.length > 1);
    }

    /**
     * Given a site public config, check if email signup is disabled.
     *
     * @param config Site public config.
     * @param disabledFeatures List of disabled features already treated. If not provided it will be calculated.
     * @return Whether email signup is disabled.
     */
    isEmailSignupDisabled(config?: CoreSitePublicConfigResponse, disabledFeatures?: string): boolean {
        return this.isFeatureDisabled('CoreLoginEmailSignup', config, disabledFeatures);
    }

    /**
     * Given a site public config, check if a certian feature is disabled.
     *
     * @param feature Feature to check.
     * @param config Site public config.
     * @param disabledFeatures List of disabled features already treated. If not provided it will be calculated.
     * @return Whether email signup is disabled.
     */
    isFeatureDisabled(feature: string, config?: CoreSitePublicConfigResponse, disabledFeatures?: string): boolean {
        if (typeof disabledFeatures == 'undefined') {
            disabledFeatures = this.getDisabledFeatures(config);
        }

        const regEx = new RegExp('(,|^)' + feature + '(,|$)', 'g');

        return !!disabledFeatures.match(regEx);
    }

    /**
     * Check if the app is configured to use a fixed URL (only 1).
     *
     * @return Whether there is 1 fixed URL.
     */
    isFixedUrlSet(): boolean {
        if (Array.isArray(CoreConstants.CONFIG.siteurl)) {
            return CoreConstants.CONFIG.siteurl.length == 1;
        }

        return !!CoreConstants.CONFIG.siteurl;
    }

    /**
     * Given a site public config, check if forgotten password is disabled.
     *
     * @param config Site public config.
     * @param disabledFeatures List of disabled features already treated. If not provided it will be calculated.
     * @return Whether it's disabled.
     */
    isForgottenPasswordDisabled(config?: CoreSitePublicConfigResponse, disabledFeatures?: string): boolean {
        return this.isFeatureDisabled('NoDelegate_ForgottenPassword', config, disabledFeatures);
    }

    /**
     * Check if current site is logged out, triggering mmCoreEventSessionExpired if it is.
     *
     * @param pageName Name of the page to go once authenticated if logged out. If not defined, site initial page.
     * @param params Params of the page to go once authenticated if logged out.
     * @return True if user is logged out, false otherwise.
     */
    isSiteLoggedOut(pageName?: string, params?: Params): boolean {
        const site = CoreSites.instance.getCurrentSite();
        if (!site) {
            return false;
        }

        if (site.isLoggedOut()) {
            CoreEvents.trigger(CoreEvents.SESSION_EXPIRED, {
                pageName,
                params,
            }, site.getId());

            return true;
        }

        return false;
    }

    /**
     * Check if a site URL is "allowed". In case the app has fixed sites, only those will be allowed to connect to.
     *
     * @param siteUrl Site URL to check.
     * @return Promise resolved with boolean: whether is one of the fixed sites.
     */
    async isSiteUrlAllowed(siteUrl: string): Promise<boolean> {
        if (this.isFixedUrlSet()) {
            // Only 1 site allowed.
            return CoreUrl.sameDomainAndPath(siteUrl, <string> this.getFixedSites());
        } else if (this.hasSeveralFixedSites()) {
            const sites = <CoreLoginSiteInfo[]> this.getFixedSites();

            return sites.some((site) => CoreUrl.sameDomainAndPath(siteUrl, site.url));
        } else if (CoreConstants.CONFIG.multisitesdisplay == 'sitefinder' && CoreConstants.CONFIG.onlyallowlistedsites) {
            // Call the sites finder to validate the site.
            const result = await CoreSites.instance.findSites(siteUrl.replace(/^https?:\/\/|\.\w{2,3}\/?$/g, ''));

            return result && result.some((site) => CoreUrl.sameDomainAndPath(siteUrl, site.url));
        } else {
            // No fixed sites or it uses a non-restrictive sites finder. Allow connecting.
            return true;
        }
    }

    /**
     * Check if SSO login should use an embedded browser.
     *
     * @param code Code to check.
     * @return True if embedded browser, false othwerise.
     */
    isSSOEmbeddedBrowser(code: number): boolean {
        return code == CoreConstants.LOGIN_SSO_INAPP_CODE;
    }

    /**
     * Check if SSO login is needed based on code returned by the WS.
     *
     * @param code Code to check.
     * @return True if SSO login is needed, false othwerise.
     */
    isSSOLoginNeeded(code: number): boolean {
        return code == CoreConstants.LOGIN_SSO_CODE || code == CoreConstants.LOGIN_SSO_INAPP_CODE;
    }

    /**
     * Load a site and load a certain page in that site.
     *
     * @param siteId Site to load.
     * @param page Name of the page to load.
     * @param params Params to pass to the page.
     * @return Promise resolved when done.
     */
    protected async loadSiteAndPage(siteId: string, page: string, params?: Params): Promise<void> {
        if (siteId == CoreConstants.NO_SITE_ID) {
            // Page doesn't belong to a site, just load the page.
            await this.navCtrl.navigateRoot(page, params);

            return;
        }

        const modal = await CoreDomUtils.instance.showModalLoading();

        try {
            const loggedIn = await CoreSites.instance.loadSite(siteId, page, params);

            if (!loggedIn) {
                return;
            }

            await this.openMainMenu({
                redirectPage: page,
                redirectParams: params,
            });
        } catch (error) {
            // Site doesn't exist.
            await this.navCtrl.navigateRoot('/login/sites');
        } finally {
            modal.dismiss();
        }
    }

    /**
     * Load a certain page in the main menu page.
     *
     * @param page Name of the page to load.
     * @param params Params to pass to the page.
     */
    loadPageInMainMenu(page: string, params?: Params): void {
        if (!CoreApp.instance.isMainMenuOpen()) {
            // Main menu not open. Store the page to be loaded later.
            this.pageToLoad = {
                page: page,
                params: params,
                time: Date.now(),
            };

            return;
        }

        if (page == CoreLoginHelperProvider.OPEN_COURSE) {
            // @todo Use the openCourse function.
        } else {
            CoreEvents.trigger(CoreEvents.LOAD_PAGE_MAIN_MENU, { redirectPage: page, redirectParams: params });
        }
    }

    /**
     * Open the main menu, loading a certain page.
     *
     * @param options Options.
     * @return Promise resolved when done.
     */
    protected async openMainMenu(options?: OpenMainMenuOptions): Promise<void> {

        // Due to DeepLinker, we need to remove the path from the URL before going to main menu.
        // IonTabs checks the URL to determine which path to load for deep linking, so we clear the URL.
        // @todo this.location.replaceState('');

        if (options?.redirectPage == CoreLoginHelperProvider.OPEN_COURSE) {
            // Load the main menu first, and then open the course.
            try {
                await this.navCtrl.navigateRoot('/');
            } finally {
                // @todo: Open course.
            }
        } else {
            // Open the main menu.
            const queryParams: Params = Object.assign({}, options);
            delete queryParams.navigationOptions;

            await this.navCtrl.navigateRoot('/', {
                queryParams,
                ...options?.navigationOptions,
            });
        }
    }

    /**
     * Open a browser to perform OAuth login (Google, Facebook, Microsoft).
     *
     * @param siteUrl URL of the site where the login will be performed.
     * @param provider The identity provider.
     * @param launchUrl The URL to open for SSO. If not defined, tool/mobile launch URL will be used.
     * @param pageName Name of the page to go once authenticated. If not defined, site initial page.
     * @param pageParams Params of the state to go once authenticated.
     * @return True if success, false if error.
     */
    openBrowserForOAuthLogin(
        siteUrl: string,
        provider: CoreSiteIdentityProvider,
        launchUrl?: string,
        pageName?: string,
        pageParams?: Params,
    ): boolean {
        launchUrl = launchUrl || siteUrl + '/admin/tool/mobile/launch.php';
        if (!provider || !provider.url) {
            return false;
        }

        const params = CoreUrlUtils.instance.extractUrlParams(provider.url);

        if (!params.id) {
            return false;
        }

        const service = CoreSites.instance.determineService(siteUrl);
        const loginUrl = this.prepareForSSOLogin(siteUrl, service, launchUrl, pageName, pageParams, {
            oauthsso: params.id,
        });

        // Always open it in browser because the user might have the session stored in there.
        CoreUtils.instance.openInBrowser(loginUrl);

        const nav = <any> window.navigator; // eslint-disable-line @typescript-eslint/no-explicit-any
        nav.app?.exitApp();

        return true;
    }

    /**
     * Open a browser to perform SSO login.
     *
     * @param siteurl URL of the site where the SSO login will be performed.
     * @param typeOfLogin CoreConstants.LOGIN_SSO_CODE or CoreConstants.LOGIN_SSO_INAPP_CODE.
     * @param service The service to use. If not defined, external service will be used.
     * @param launchUrl The URL to open for SSO. If not defined, local_mobile launch URL will be used.
     * @param pageName Name of the page to go once authenticated. If not defined, site initial page.
     * @param pageParams Params of the state to go once authenticated.
     */
    openBrowserForSSOLogin(
        siteUrl: string,
        typeOfLogin: number,
        service?: string,
        launchUrl?: string,
        pageName?: string,
        pageParams?: Params,
    ): void {
        const loginUrl = this.prepareForSSOLogin(siteUrl, service, launchUrl, pageName, pageParams);

        if (this.isSSOEmbeddedBrowser(typeOfLogin)) {
            CoreUtils.instance.openInApp(loginUrl, {
                clearsessioncache: 'yes', // Clear the session cache to allow for multiple logins.
                closebuttoncaption: Translate.instance.instant('core.login.cancel'),
            });
        } else {
            CoreUtils.instance.openInBrowser(loginUrl);

            const nav = <any> window.navigator; // eslint-disable-line @typescript-eslint/no-explicit-any
            nav.app?.exitApp();
        }
    }

    /**
     * Convenient helper to open change password page.
     *
     * @param siteUrl Site URL to construct change password URL.
     * @param error Error message.
     * @return Promise resolved when done.
     */
    async openChangePassword(siteUrl: string, error: string): Promise<void> {
        const alert = await CoreDomUtils.instance.showAlert(Translate.instance.instant('core.notice'), error, undefined, 3000);

        await alert.onDidDismiss();

        CoreUtils.instance.openInApp(siteUrl + '/login/change_password.php');
    }

    /**
     * Open forgotten password in inappbrowser.
     *
     * @param siteUrl URL of the site.
     */
    openForgottenPassword(siteUrl: string): void {
        CoreUtils.instance.openInApp(siteUrl + '/login/forgot_password.php');
    }

    /**
     * Function to open in app browser to change password or complete user profile.
     *
     * @param siteId The site ID.
     * @param path The relative path of the URL to open.
     * @param alertMessage The key of the message to display before opening the in app browser.
     * @param invalidateCache Whether to invalidate site's cache (e.g. when the user is forced to change password).
     * @return Promise resolved when done.
     */
    async openInAppForEdit(siteId: string, path: string, alertMessage?: string, invalidateCache?: boolean): Promise<void> {
        if (!siteId || siteId !== CoreSites.instance.getCurrentSiteId()) {
            // Site that triggered the event is not current site, nothing to do.
            return;
        }

        const currentSite = CoreSites.instance.getCurrentSite();
        const siteUrl = currentSite?.getURL();

        if (!currentSite || !siteUrl) {
            return;
        }

        if (!this.isOpenEditAlertShown && !this.waitingForBrowser) {
            this.isOpenEditAlertShown = true;

            if (invalidateCache) {
                currentSite.invalidateWsCache();
            }

            // Open change password.
            if (alertMessage) {
                alertMessage = Translate.instance.instant(alertMessage) + '<br>' +
                    Translate.instance.instant('core.redirectingtosite');
            }

            try {
                await currentSite.openInAppWithAutoLogin(siteUrl + path, undefined, alertMessage);

                this.waitingForBrowser = true;
            } finally {
                this.isOpenEditAlertShown = false;
            }
        }
    }

    /**
     * Function that should be called when password change is forced. Reserved for core use.
     *
     * @param siteId The site ID.
     */
    async passwordChangeForced(siteId: string): Promise<void> {
        const currentSite = CoreSites.instance.getCurrentSite();
        if (!currentSite || siteId !== currentSite.getId()) {
            return; // Site that triggered the event is not current site.
        }

        const currentPage = CoreApp.instance.getCurrentPage();

        // If current page is already change password, stop.
        if (currentPage == '/login/changepassword') {
            return;
        }

        await this.navCtrl.navigateRoot('/login/changepassword', { queryParams: { siteId } });
    }

    /**
     * Prepare the app to perform SSO login.
     *
     * @param siteUrl URL of the site where the SSO login will be performed.
     * @param service The service to use. If not defined, external service will be used.
     * @param launchUrl The URL to open for SSO. If not defined, local_mobile launch URL will be used.
     * @param pageName Name of the page to go once authenticated. If not defined, site initial page.
     * @param pageParams Params of the state to go once authenticated.
     * @param urlParams Other params to add to the URL.
     * @return Login Url.
     */
    prepareForSSOLogin(
        siteUrl: string,
        service?: string,
        launchUrl?: string,
        pageName?: string,
        pageParams?: Params,
        urlParams?: CoreUrlParams,
    ): string {

        service = service || CoreConstants.CONFIG.wsextservice;
        launchUrl = launchUrl || siteUrl + '/local/mobile/launch.php';

        const passport = Math.random() * 1000;
        let loginUrl = launchUrl + '?service=' + service;

        loginUrl += '&passport=' + passport;
        loginUrl += '&urlscheme=' + CoreConstants.CONFIG.customurlscheme;

        if (urlParams) {
            loginUrl = CoreUrlUtils.instance.addParamsToUrl(loginUrl, urlParams);
        }

        // Store the siteurl and passport in CoreConfigProvider for persistence.
        // We are "configuring" the app to wait for an SSO. CoreConfigProvider shouldn't be used as a temporary storage.
        CoreConfig.instance.set(CoreConstants.LOGIN_LAUNCH_DATA, JSON.stringify(<StoredLoginLaunchData> {
            siteUrl: siteUrl,
            passport: passport,
            pageName: pageName || '',
            pageParams: pageParams || {},
            ssoUrlParams: urlParams || {},
        }));

        return loginUrl;
    }

    /**
     * Redirect to a new page, setting it as the root page and loading the right site if needed.
     *
     * @param page Name of the page to load. Special cases: OPEN_COURSE (to open course page).
     * @param params Params to pass to the page.
     * @param siteId Site to load. If not defined, current site.
     * @return Promise resolved when done.
     */
    async redirect(page: string, params?: Params, siteId?: string): Promise<void> {
        siteId = siteId || CoreSites.instance.getCurrentSiteId();

        if (CoreSites.instance.isLoggedIn()) {
            if (siteId && siteId != CoreSites.instance.getCurrentSiteId()) {
                // Target page belongs to a different site. Change site.
                // @todo: Check site plugins.
                await CoreSites.instance.logout();

                await this.loadSiteAndPage(siteId, page, params);
            } else {
                this.loadPageInMainMenu(page, params);
            }
        } else {
            if (siteId) {
                await this.loadSiteAndPage(siteId, page, params);
            } else {
                await this.navCtrl.navigateRoot('/login/sites');
            }
        }
    }

    /**
     * Request a password reset.
     *
     * @param siteUrl URL of the site.
     * @param username Username to search.
     * @param email Email to search.
     * @return Promise resolved when done.
     */
    requestPasswordReset(siteUrl: string, username?: string, email?: string): Promise<CoreLoginRequestPasswordResetResult> {
        const params: Record<string, string> = {};

        if (username) {
            params.username = username;
        }

        if (email) {
            params.email = email;
        }

        return CoreWS.instance.callAjax('core_auth_request_password_reset', params, { siteUrl });
    }

    /**
     * Function that should be called when the session expires. Reserved for core use.
     *
     * @param data Data received by the SESSION_EXPIRED event.
     * @return Promise resolved when done.
     */
    async sessionExpired(data: CoreEventSessionExpiredData): Promise<void> {
        const siteId = data?.siteId;
        const currentSite = CoreSites.instance.getCurrentSite();
        const siteUrl = currentSite?.getURL();

        if (!currentSite || !siteUrl) {
            return;
        }

        if (siteId && siteId !== currentSite.getId()) {
            return; // Site that triggered the event is not current site.
        }

        try {
            // Check authentication method.
            const result = await CoreSites.instance.checkSite(siteUrl);

            if (result.warning) {
                CoreDomUtils.instance.showErrorModal(result.warning, true, 4000);
            }

            if (this.isSSOLoginNeeded(result.code)) {
                // SSO. User needs to authenticate in a browser. Check if we need to display a message.
                if (!CoreApp.instance.isSSOAuthenticationOngoing() && !this.isSSOConfirmShown && !this.waitingForBrowser) {
                    this.isSSOConfirmShown = true;

                    if (this.shouldShowSSOConfirm(result.code)) {
                        await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.login.' +
                            (currentSite.isLoggedOut() ? 'loggedoutssodescription' : 'reconnectssodescription')));
                    }

                    try {
                        this.waitingForBrowser = true;

                        this.openBrowserForSSOLogin(
                            result.siteUrl,
                            result.code,
                            result.service,
                            result.config?.launchurl,
                            data.pageName,
                            data.params,
                        );
                    } catch (error) {
                        // User cancelled, logout him.
                        CoreSites.instance.logout();
                    } finally {
                        this.isSSOConfirmShown = false;
                    }
                }
            } else {
                if (currentSite.isOAuth()) {
                    // User authenticated using an OAuth method. Check if it's still valid.
                    const identityProviders = this.getValidIdentityProviders(result.config);
                    const providerToUse = identityProviders.find((provider) => {
                        const params = CoreUrlUtils.instance.extractUrlParams(provider.url);

                        return Number(params.id) == currentSite.getOAuthId();
                    });

                    if (providerToUse) {
                        if (!CoreApp.instance.isSSOAuthenticationOngoing() && !this.isSSOConfirmShown && !this.waitingForBrowser) {
                            // Open browser to perform the OAuth.
                            this.isSSOConfirmShown = true;

                            const confirmMessage = Translate.instance.instant('core.login.' +
                                    (currentSite.isLoggedOut() ? 'loggedoutssodescription' : 'reconnectssodescription'));

                            try {
                                await CoreDomUtils.instance.showConfirm(confirmMessage);

                                this.waitingForBrowser = true;
                                CoreSites.instance.unsetCurrentSite(); // Unset current site to make authentication work fine.

                                this.openBrowserForOAuthLogin(
                                    siteUrl,
                                    providerToUse,
                                    result.config?.launchurl,
                                    data.pageName,
                                    data.params,
                                );
                            } catch (error) {
                                // User cancelled, logout him.
                                CoreSites.instance.logout();
                            } finally {
                                this.isSSOConfirmShown = false;
                            }
                        }

                        return;
                    }
                }

                const info = currentSite.getInfo();
                if (typeof info != 'undefined' && typeof info.username != 'undefined' && !this.isOpeningReconnect) {
                    // If current page is already reconnect, stop.
                    if (CoreApp.instance.getCurrentPage() == '/login/reconnect') {
                        return;
                    }

                    this.isOpeningReconnect = true;

                    await CoreUtils.instance.ignoreErrors(this.navCtrl.navigateRoot('/login/reconnect', {
                        queryParams: CoreObject.removeUndefined({
                            siteId,
                            pageName: data.pageName,
                            pageParams: data.params,
                        }),
                    }));

                    this.isOpeningReconnect = false;
                }
            }
        } catch (error) {
            // Error checking site.
            if (currentSite.isLoggedOut()) {
                // Site is logged out, show error and logout the user.
                CoreDomUtils.instance.showErrorModalDefault(error, 'core.networkerrormsg', true);
                CoreSites.instance.logout();
            }
        }
    }

    /**
     * Check if a confirm should be shown to open a SSO authentication.
     *
     * @param typeOfLogin CoreConstants.LOGIN_SSO_CODE or CoreConstants.LOGIN_SSO_INAPP_CODE.
     * @return True if confirm modal should be shown, false otherwise.
     */
    shouldShowSSOConfirm(typeOfLogin: number): boolean {
        return !this.isSSOEmbeddedBrowser(typeOfLogin) &&
            (!CoreConstants.CONFIG.skipssoconfirmation || String(CoreConstants.CONFIG.skipssoconfirmation) === 'false');
    }

    /**
     * Show a modal warning the user that he should use the Workplace app.
     *
     * @param message The warning message.
     */
    protected showWorkplaceNoticeModal(message: string): void {
        const link = CoreApp.instance.getAppStoreUrl({ android: 'com.moodle.workplace', ios: 'id1470929705' });

        CoreDomUtils.instance.showDownloadAppNoticeModal(message, link);
    }

    /**
     * Show a modal warning the user that he should use the current Moodle app.
     *
     * @param message The warning message.
     */
    protected showMoodleAppNoticeModal(message: string): void {
        const storesConfig: CoreStoreConfig = CoreConstants.CONFIG.appstores;
        storesConfig.mobile = 'https://download.moodle.org/mobile/';
        storesConfig.default = 'https://download.moodle.org/mobile/';

        const link = CoreApp.instance.getAppStoreUrl(storesConfig);

        CoreDomUtils.instance.showDownloadAppNoticeModal(message, link);
    }

    /**
     * Show a modal to inform the user that a confirmation email was sent, and a button to resend the email on 3.6+ sites.
     *
     * @param siteUrl Site URL.
     * @param email Email of the user. If set displayed in the message.
     * @param username Username. If not set the button to resend email will not be shown.
     * @param password User password. If not set the button to resend email will not be shown.
     */
    protected async showNotConfirmedModal(siteUrl: string, email?: string, username?: string, password?: string): Promise<void> {
        const title = Translate.instance.instant('core.login.mustconfirm');
        let message: string;
        let canResend = false;
        if (email) {
            message = Translate.instance.instant('core.login.emailconfirmsent', { $a: email });
        } else {
            message = Translate.instance.instant('core.login.emailconfirmsentnoemail');
        }

        // Check whether we need to display the resend button or not.
        if (username && password) {
            canResend = await this.canResendEmail(siteUrl);
        }

        if (!canResend) {
            // Just display an informative alert.
            await CoreDomUtils.instance.showAlert(title, message);

            return;
        }

        const okText = Translate.instance.instant('core.login.resendemail');
        const cancelText = Translate.instance.instant('core.close');

        try {
            // Ask the user if he wants to resend the email.
            await CoreDomUtils.instance.showConfirm(message, title, okText, cancelText);

            // Call the WS to resend the confirmation email.
            const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
            const data = { username, password };
            const preSets = { siteUrl };

            try {
                const result = <ResendConfirmationEmailResult> await CoreWS.instance.callAjax(
                    'core_auth_resend_confirmation_email',
                    data,
                    preSets,
                );

                if (!result.status) {
                    throw new CoreWSError(result.warnings?.[0]);
                }

                const message = Translate.instance.instant('core.login.emailconfirmsentsuccess');
                CoreDomUtils.instance.showAlert(Translate.instance.instant('core.success'), message);
            } finally {
                modal.dismiss();
            }
        } catch (error) {
            CoreDomUtils.instance.showErrorModal(error);
        }
    }

    /**
     * Check if confirmation email an be resent.
     *
     * @param siteUrl Site URL to check.
     * @return Promise.
     */
    protected async canResendEmail(siteUrl: string): Promise<boolean> {
        const modal = await CoreDomUtils.instance.showModalLoading();

        // We don't have site info before login, the only way to check if the WS is available is by calling it.
        try {
            // This call will always fail because we aren't sending parameters.
            await CoreWS.instance.callAjax('core_auth_resend_confirmation_email', {}, { siteUrl });

            return true; // We should never reach here.
        } catch (error) {
            // If the WS responds with an invalid parameter error it means the WS is avaiable.
            return error?.errorcode === 'invalidparameter';
        } finally {
            modal.dismiss();
        }
    }

    /**
     * Function called when site policy is not agreed. Reserved for core use.
     *
     * @param siteId Site ID. If not defined, current site.
     */
    sitePolicyNotAgreed(siteId?: string): void {
        siteId = siteId || CoreSites.instance.getCurrentSiteId();
        if (!siteId || siteId != CoreSites.instance.getCurrentSiteId()) {
            // Only current site allowed.
            return;
        }

        if (!CoreSites.instance.wsAvailableInCurrentSite('core_user_agree_site_policy')) {
            // WS not available, stop.
            return;
        }

        // If current page is already site policy, stop.
        if (CoreApp.instance.getCurrentPage() == '/login/sitepolicy') {
            return;
        }

        this.navCtrl.navigateRoot('/login/sitepolicy', { queryParams: { siteId: siteId } });
    }

    /**
     * Convenient helper to handle get User Token error. It redirects to change password page if forcepassword is set.
     *
     * @param siteUrl Site URL to construct change password URL.
     * @param error Error object containing errorcode and error message.
     * @param username Username.
     * @param password User password.
     */
    treatUserTokenError(siteUrl: string, error: CoreWSError, username?: string, password?: string): void {
        if (error.errorcode == 'forcepasswordchangenotice') {
            this.openChangePassword(siteUrl, CoreTextUtils.instance.getErrorMessageFromError(error)!);
        } else if (error.errorcode == 'usernotconfirmed') {
            this.showNotConfirmedModal(siteUrl, undefined, username, password);
        } else if (error.errorcode == 'connecttomoodleapp') {
            this.showMoodleAppNoticeModal(CoreTextUtils.instance.getErrorMessageFromError(error)!);
        } else if (error.errorcode == 'connecttoworkplaceapp') {
            this.showWorkplaceNoticeModal(CoreTextUtils.instance.getErrorMessageFromError(error)!);
        } else {
            CoreDomUtils.instance.showErrorModal(error);
        }
    }

    /**
     * Convenient helper to validate a browser SSO login.
     *
     * @param url URL received, to be validated.
     * @return Promise resolved on success.
     */
    async validateBrowserSSOLogin(url: string): Promise<CoreLoginSSOData> {
        // Split signature:::token
        const params = url.split(':::');

        const serializedData = await CoreConfig.instance.get<string>(CoreConstants.LOGIN_LAUNCH_DATA);

        const data = <StoredLoginLaunchData | null> CoreTextUtils.instance.parseJSON(serializedData, null);
        if (data === null) {
            throw new CoreError('No launch data stored.');
        }

        const passport = data.passport;
        let launchSiteURL = data.siteUrl;

        // Reset temporary values.
        CoreConfig.instance.delete(CoreConstants.LOGIN_LAUNCH_DATA);

        // Validate the signature.
        // We need to check both http and https.
        let signature = <string> Md5.hashAsciiStr(launchSiteURL + passport);
        if (signature != params[0]) {
            if (launchSiteURL.indexOf('https://') != -1) {
                launchSiteURL = launchSiteURL.replace('https://', 'http://');
            } else {
                launchSiteURL = launchSiteURL.replace('http://', 'https://');
            }
            signature = <string> Md5.hashAsciiStr(launchSiteURL + passport);
        }

        if (signature == params[0]) {
            this.logger.debug('Signature validated');

            return {
                siteUrl: launchSiteURL,
                token: params[1],
                privateToken: params[2],
                pageName: data.pageName,
                pageParams: data.pageParams,
                ssoUrlParams: data.ssoUrlParams,
            };
        } else {
            this.logger.debug('Invalid signature in the URL request yours: ' + params[0] + ' mine: '
                + signature + ' for passport ' + passport);

            throw new CoreError(Translate.instance.instant('core.unexpectederror'));
        }
    }

}

export class CoreLoginHelper extends makeSingleton(CoreLoginHelperProvider) {}

/**
 * Data related to a SSO authentication.
 */
export interface CoreLoginSSOData {
    /**
     * The site's URL.
     */
    siteUrl: string;

    /**
     * User's token.
     */
    token?: string;

    /**
     * User's private token.
     */
    privateToken?: string;

    /**
     * Name of the page to go after authenticated.
     */
    pageName?: string;

    /**
     * Params to page to the page.
     */
    pageParams?: Params;

    /**
     * Other params added to the login url.
     */
    ssoUrlParams?: CoreUrlParams;
};

/**
 * Result of WS core_user_agree_site_policy.
 */
type AgreeSitePolicyResult = {
    status: boolean; // Status: true only if we set the policyagreed to 1 for the user.
    warnings?: CoreWSExternalWarning[];
};

/**
 * Result of WS auth_email_get_signup_settings.
 */
export type AuthEmailSignupSettings = {
    namefields: string[];
    passwordpolicy?: string; // Password policy.
    sitepolicy?: string; // Site policy.
    sitepolicyhandler?: string; // Site policy handler.
    defaultcity?: string; // Default city.
    country?: string; // Default country.
    profilefields?: AuthEmailSignupProfileField[]; // Required profile fields.
    recaptchapublickey?: string; // Recaptcha public key.
    recaptchachallengehash?: string; // Recaptcha challenge hash.
    recaptchachallengeimage?: string; // Recaptcha challenge noscript image.
    recaptchachallengejs?: string; // Recaptcha challenge js url.
    warnings?: CoreWSExternalWarning[];
};

/**
 * Profile field for signup.
 */
export type AuthEmailSignupProfileField = {
    id?: number; // Profile field id.
    shortname?: string; // Profile field shortname.
    name?: string; // Profield field name.
    datatype?: string; // Profield field datatype.
    description?: string; // Profield field description.
    descriptionformat: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
    categoryid?: number; // Profield field category id.
    categoryname?: string; // Profield field category name.
    sortorder?: number; // Profield field sort order.
    required?: number; // Profield field required.
    locked?: number; // Profield field locked.
    visible?: number; // Profield field visible.
    forceunique?: number; // Profield field unique.
    signup?: number; // Profield field in signup form.
    defaultdata?: string; // Profield field default data.
    defaultdataformat: number; // Defaultdata format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
    param1?: string; // Profield field settings.
    param2?: string; // Profield field settings.
    param3?: string; // Profield field settings.
    param4?: string; // Profield field settings.
    param5?: string; // Profield field settings.
};

/**
 * Category of profile fields for signup.
 */
export type AuthEmailSignupProfileFieldsCategory = {
    id: number; // Category ID.
    name: string; // Category name.
    fields: AuthEmailSignupProfileField[]; // Field in the category.
};

/**
 * Result of WS core_auth_request_password_reset.
 */
export type CoreLoginRequestPasswordResetResult = {
    status: string; // The returned status of the process
    notice: string; // Important information for the user about the process.
    warnings?: CoreWSExternalWarning[];
};

/**
 * Result of WS core_auth_resend_confirmation_email.
 */
type ResendConfirmationEmailResult = {
    status: boolean; // True if the confirmation email was sent, false otherwise.
    warnings?: CoreWSExternalWarning[];
};

type StoredLoginLaunchData = {
    siteUrl: string;
    passport: number;
    pageName: string;
    pageParams: Params;
    ssoUrlParams: CoreUrlParams;
};

type OpenMainMenuOptions = {
    redirectPage?: string; // Route of the page to open in main menu. If not defined, default tab will be selected.
    redirectParams?: Params; // Params to pass to the selected tab if any.
    urlToOpen?: string; // URL to open once the main menu is loaded.
    navigationOptions?: NavigationOptions; // Navigation options.
};
