// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

import Log from './Log';
import OidcClient from './OidcClient';
import UserManagerSettings from './UserManagerSettings';
import User from './User';
import UserManagerEvents from './UserManagerEvents';
import SilentRenewService from './SilentRenewService';
import SessionMonitor from './SessionMonitor';
import TokenRevocationClient from './TokenRevocationClient';

export default class UserManager extends OidcClient {
    constructor(settings = {},
        SilentRenewServiceCtor = SilentRenewService,
        SessionMonitorCtor = SessionMonitor,
        TokenRevocationClientCtor = TokenRevocationClient
    ) {

        if (!(settings instanceof UserManagerSettings)) {
            settings = new UserManagerSettings(settings);
        }
        super(settings);

        this._events = new UserManagerEvents(settings);
        this._silentRenewService = new SilentRenewServiceCtor(this);
        
        // order is important for the following properties; these services depend upon the events.
        if (this.settings.automaticSilentRenew) {
            Log.debug("automaticSilentRenew is configured, setting up silent renew");
            this.startSilentRenew();
        }

        if (this.settings.monitorSession) {
            Log.debug("monitorSession is configured, setting up session monitor");
            this._sessionMonitor = new SessionMonitorCtor(this);
        }

        this._tokenRevocationClient = new TokenRevocationClientCtor(this._settings);
    }

    get _redirectNavigator() {
        return this.settings.redirectNavigator;
    }
    get _popupNavigator() {
        return this.settings.popupNavigator;
    }
    get _iframeNavigator() {
        return this.settings.iframeNavigator;
    }
    get _userStore() {
        return this.settings.userStore;
    }

    get events() {
        return this._events;
    }

    getUser() {
        Log.debug("UserManager.getUser");

        return this._loadUser().then(user => {
            if (user) {
                Log.info("user loaded");

                this._events.load(user, false);

                return user;
            }
            else {
                Log.info("user not found in storage");
                return null;
            }
        });
    }

    removeUser() {
        Log.debug("UserManager.removeUser");

        return this.storeUser(null).then(() => {
            Log.info("user removed from storage");
            this._events.unload();
        });
    }

    signinRedirect(args) {
        Log.debug("UserManager.signinRedirect");
        return this._signinStart(args, this._redirectNavigator).then(()=>{
            Log.info("signinRedirect successful");
        });
    }
    signinRedirectCallback(url) {
        Log.debug("UserManager.signinRedirectCallback");
        return this._signinEnd(url || this._redirectNavigator.url).then(user => {
            if (user) {
                if (user.profile && user.profile.sub) {
                    Log.info("signinRedirectCallback successful, signed in sub: ", user.profile.sub);
                }
                else {
                    Log.info("signinRedirectCallback successful");
                }
            }

            return user;
        });
    }
    
    signinPopup(args = {}) {
        Log.debug("UserManager.signinPopup");

        let url = args.redirect_uri || this.settings.popup_redirect_uri || this.settings.redirect_uri;
        if (!url) {
            Log.error("No popup_redirect_uri or redirect_uri configured");
            return Promise.reject(new Error("No popup_redirect_uri or redirect_uri configured"));
        }

        args.redirect_uri = url;
        args.display = "popup";

        return this._signin(args, this._popupNavigator, {
            startUrl: url,
            popupWindowFeatures: args.popupWindowFeatures || this.settings.popupWindowFeatures,
            popupWindowTarget: args.popupWindowTarget || this.settings.popupWindowTarget
        }).then(user => {
            if (user) {
                if (user.profile && user.profile.sub) {
                    Log.info("signinPopup successful, signed in sub: ", user.profile.sub);
                }
                else {
                    Log.info("signinPopup successful");
                }
            }

            return user;
        });
    }
    signinPopupCallback(url) {
        Log.debug("UserManager.signinPopupCallback");
        return this._signinCallback(url, this._popupNavigator).then(user => {
            if (user) {
                if (user.profile && user.profile.sub) {
                    Log.info("signinPopupCallback successful, signed in sub: ", user.profile.sub);
                }
                else {
                    Log.info("signinPopupCallback successful");
                }
            }

            return user;
        });
    }

    signinSilent(args = {}) {
        Log.debug("UserManager.signinSilent");

        let url = args.redirect_uri || this.settings.silent_redirect_uri;
        if (!url) {
            Log.error("No silent_redirect_uri configured");
            return Promise.reject(new Error("No silent_redirect_uri configured"));
        }

        args.redirect_uri = url;
        args.prompt = "none";

        let setIdToken;
        if (args.id_token_hint || !this.settings.includeIdTokenInSilentRenew) {
            setIdToken = Promise.resolve();
        }
        else {
            setIdToken = this._loadUser().then(user => {
                args.id_token_hint = user && user.id_token;
            });
        }

        return setIdToken.then(() => {
            return this._signin(args, this._iframeNavigator, {
                startUrl: url,
                silentRequestTimeout: args.silentRequestTimeout || this.settings.silentRequestTimeout
            });
        }).then(user => {
            if (user) {
                if (user.profile && user.profile.sub) {
                    Log.info("signinSilent successful, signed in sub: ", user.profile.sub);
                }
                else {
                    Log.info("signinSilent successful");
                }
            }

            return user;
        });
    }
    signinSilentCallback(url) {
        Log.debug("UserManager.signinSilentCallback");
        return this._signinCallback(url, this._iframeNavigator).then(user => {
            if (user) {
                if (user.profile && user.profile.sub) {
                    Log.info("signinSilentCallback successful, signed in sub: ", user.profile.sub);
                }
                else {
                    Log.info("signinSilentCallback successful");
                }
            }

            return user;
        });
    }

    querySessionStatus(args = {}) {
        Log.debug("UserManager.querySessionStatus");

        let url = args.redirect_uri || this.settings.silent_redirect_uri;
        if (!url) {
            Log.error("No silent_redirect_uri configured");
            return Promise.reject(new Error("No silent_redirect_uri configured"));
        }

        args.redirect_uri = url;
        args.prompt = "none";
        args.response_type = "id_token";
        args.scope = "openid";

        return this._signinStart(args, this._iframeNavigator, {
            startUrl: url,
            silentRequestTimeout: args.silentRequestTimeout || this.settings.silentRequestTimeout
        }).then(navResponse => {
            return this.processSigninResponse(navResponse.url).then(signinResponse => {
                Log.debug("got signin response");

                if (signinResponse.session_state && signinResponse.profile.sub && signinResponse.profile.sid) {
                    Log.info("querySessionStatus success for sub: ",  signinResponse.profile.sub);
                    return {
                        session_state: signinResponse.session_state,
                        sub: signinResponse.profile.sub,
                        sid: signinResponse.profile.sid
                    };
                }
                else {
                    Log.info("querySessionStatus successful, user not authenticated");
                }
            });
        });
    }

    _signin(args, navigator, navigatorParams = {}) {
        Log.debug("_signin");
        return this._signinStart(args, navigator, navigatorParams).then(navResponse => {
            return this._signinEnd(navResponse.url);
        });
    }
    _signinStart(args, navigator, navigatorParams = {}) {
        Log.debug("_signinStart");

        return navigator.prepare(navigatorParams).then(handle => {
            Log.debug("got navigator window handle");

            return this.createSigninRequest(args).then(signinRequest => {
                Log.debug("got signin request");

                navigatorParams.url = signinRequest.url;
                navigatorParams.id = signinRequest.state.id;
                
                return handle.navigate(navigatorParams);
            }).catch(err => {
                if (handle.close) {
                    Log.debug("Error after preparing navigator, closing navigator window");
                    handle.close();
                }
                throw err;
            });
        });
    }
    _signinEnd(url) {
        Log.debug("_signinEnd");

        return this.processSigninResponse(url).then(signinResponse => {
            Log.debug("got signin response");

            let user = new User(signinResponse);

            return this.storeUser(user).then(() => {
                Log.debug("user stored");

                this._events.load(user);

                return user;
            });
        });
    }
    _signinCallback(url, navigator) {
        Log.debug("_signinCallback");
        return navigator.callback(url);
    }

    signoutRedirect(args = {}) {
        Log.debug("UserManager.signoutRedirect");
        let postLogoutRedirectUri = args.post_logout_redirect_uri || this.settings.post_logout_redirect_uri;
        if (postLogoutRedirectUri){
            args.post_logout_redirect_uri = postLogoutRedirectUri;
        }
        return this._signoutStart(args, this._redirectNavigator).then(()=>{
            Log.info("signoutRedirect successful");
        });
    }
    signoutRedirectCallback(url) {
        Log.debug("UserManager.signoutRedirectCallback");
        return this._signoutEnd(url || this._redirectNavigator.url).then(response=>{
            Log.info("signoutRedirectCallback successful");
            return response;
        });
    }

    signoutPopup(args = {}) {
        Log.debug("UserManager.signinPopup");

        let url = args.post_logout_redirect_uri || this.settings.popup_post_logout_redirect_uri || this.settings.post_logout_redirect_uri;
        args.post_logout_redirect_uri = url;
        args.display = "popup";
        if (args.post_logout_redirect_uri){
            // we're putting a dummy entry in here because we 
            // need a unique id from the state for notification
            // to the parent window, which is necessary if we
            // plan to return back to the client after signout
            // and so we can close the popup after signout
            args.state = args.state || {};
        }

        return this._signout(args, this._popupNavigator, {
            startUrl: url,
            popupWindowFeatures: args.popupWindowFeatures || this.settings.popupWindowFeatures,
            popupWindowTarget: args.popupWindowTarget || this.settings.popupWindowTarget
        }).then(() => {
            Log.info("signoutPopup successful");
        });
    }
    signoutPopupCallback(url, keepOpen) {
        if (typeof(keepOpen) === 'undefined' && typeof(url) === 'boolean') {
            url = null;
            keepOpen = true;
        }
        Log.debug("UserManager.signoutPopupCallback");
        let delimiter = '?';
        return this._popupNavigator.callback(url, keepOpen, delimiter).then(() => {
            Log.info("signoutPopupCallback successful");
        });
    }

    _signout(args, navigator, navigatorParams = {}) {
        Log.debug("_signout");
        return this._signoutStart(args, navigator, navigatorParams).then(navResponse => {
            return this._signoutEnd(navResponse.url);
        });
    }
    _signoutStart(args = {}, navigator, navigatorParams = {}) {
        Log.debug("_signoutStart");

        return navigator.prepare(navigatorParams).then(handle => {
            Log.debug("got navigator window handle");

            return this._loadUser().then(user => {
                Log.debug("loaded current user from storage");

                var revokePromise = this._settings.revokeAccessTokenOnSignout ? this._revokeInternal(user) : Promise.resolve();
                return revokePromise.then(() => {

                    var id_token = args.id_token_hint || user && user.id_token;
                    if (id_token) {
                        Log.debug("Setting id_token into signout request");
                        args.id_token_hint = id_token;
                    }

                    return this.removeUser().then(() => {
                        Log.debug("user removed, creating signout request");

                        return this.createSignoutRequest(args).then(signoutRequest => {
                            Log.debug("got signout request");

                            navigatorParams.url = signoutRequest.url;
                            if (signoutRequest.state) {
                                navigatorParams.id = signoutRequest.state.id;
                            }
                            return handle.navigate(navigatorParams);
                        });
                    });
                });
            }).catch(err => {
                if (handle.close) {
                    Log.debug("Error after preparing navigator, closing navigator window");
                    handle.close();
                }
                throw err;
            });
        });
    }
    _signoutEnd(url) {
        Log.debug("_signoutEnd");

        return this.processSignoutResponse(url).then(signoutResponse => {
            Log.debug("got signout response");

            return signoutResponse;
        });
    }

    revokeAccessToken() {
        Log.debug("UserManager.revokeAccessToken");

        return this._loadUser().then(user => {
            return this._revokeInternal(user, true).then(success => {
                if (success) {
                    Log.debug("removing token properties from user and re-storing");

                    user.access_token = null;
                    user.expires_at = null;
                    user.token_type = null;

                    return this.storeUser(user).then(() => {
                        Log.debug("user stored");
                        this._events.load(user);
                    });
                }
            });
        }).then(()=>{
            Log.info("access token revoked successfully");
        });
    }

    _revokeInternal(user, required) {
        Log.debug("checking if token revocation is necessary");

        var access_token = user && user.access_token;

        // check for JWT vs. reference token
        if (!access_token || access_token.indexOf('.') >= 0) {
            Log.debug("no need to revoke due to no user, token, or JWT format");
            return Promise.resolve(false);
        }

        return this._tokenRevocationClient.revoke(access_token, required).then(() => true);
    }

    startSilentRenew() {
        this._silentRenewService.start();
    }

    stopSilentRenew() {
        this._silentRenewService.stop();
    }

    get _userStoreKey() {
        return `user:${this.settings.authority}:${this.settings.client_id}`;
    }

    _loadUser() {
        Log.debug("_loadUser");

        return this._userStore.get(this._userStoreKey).then(storageString => {
            if (storageString) {
                Log.debug("user storageString loaded");
                return User.fromStorageString(storageString);
            }

            Log.debug("no user storageString");
            return null;
        });
    }

    storeUser(user) {
        if (user) {
            Log.debug("storeUser storing user");

            var storageString = user.toStorageString();
            return this._userStore.set(this._userStoreKey, storageString);
        }
        else {
            Log.debug("storeUser removing user storage");
            return this._userStore.remove(this._userStoreKey);
        }
    }
}
