/**
 * Copyright JS Foundation and other contributors, http://js.foundation
 *
 * 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.
 **/


/**
 * @mixin @node-red/editor-api_auth
 */

var passport = require("passport");
var oauth2orize = require("oauth2orize");

var strategies = require("./strategies");
var Tokens = require("./tokens");
var Users = require("./users");
var permissions = require("./permissions");

var theme = require("../editor/theme");

var settings = null;
var log = require("@node-red/util").log; // TODO: separate module


passport.use(strategies.bearerStrategy.BearerStrategy);
passport.use(strategies.clientPasswordStrategy.ClientPasswordStrategy);
passport.use(strategies.anonymousStrategy);
passport.use(strategies.tokensStrategy);

var server = oauth2orize.createServer();

server.exchange(oauth2orize.exchange.password(strategies.passwordTokenExchange));

function init(_settings,storage) {
    settings = _settings;
    if (settings.adminAuth) {
        var mergedAdminAuth = Object.assign({}, settings.adminAuth, settings.adminAuth.module);
        Users.init(mergedAdminAuth);
        Tokens.init(mergedAdminAuth,storage);
    }
}
/**
 * Returns an Express middleware function that ensures the user making a request
 * has the necessary permission.
 *
 * @param {String} permission - the permission required for the request, such as `flows.write`
 * @return {Function} - an Express middleware
 * @memberof @node-red/editor-api_auth
 */
function needsPermission(permission) {
    return function(req,res,next) {
        if (settings && settings.adminAuth) {
            return passport.authenticate(['bearer','tokens','anon'],{ session: false })(req,res,function() {
                if (!req.user) {
                    return next();
                }
                if (permissions.hasPermission(req.authInfo.scope,permission)) {
                    return next();
                }
                log.audit({event: "permission.fail", permissions: permission},req);
                return res.status(401).end();
            });
        } else {
            next();
        }
    }
}

function ensureClientSecret(req,res,next) {
    if (!req.body.client_secret) {
        req.body.client_secret = 'not_available';
    }
    next();
}
function authenticateClient(req,res,next) {
    return passport.authenticate(['oauth2-client-password'], {session: false})(req,res,next);
}
function getToken(req,res,next) {
    return server.token()(req,res,next);
}

async function login(req,res) {
    var response = {};
    if (settings.adminAuth) {
        var mergedAdminAuth = Object.assign({}, settings.adminAuth, settings.adminAuth.module);
        if (mergedAdminAuth.type === "credentials") {
            response = {
                "type":"credentials",
                "prompts":[{id:"username",type:"text",label:"user.username"},{id:"password",type:"password",label:"user.password"}]
            }
        } else if (mergedAdminAuth.type === "strategy") {

            var urlPrefix = (settings.httpAdminRoot||"").replace(/\/$/,"");
            if (urlPrefix.length > 0) {
                urlPrefix += "/";
            }
            response = {
                "type":"strategy"
            }
            if (mergedAdminAuth.strategy.autoLogin) {
                response.autoLogin = true
                response.loginRedirect = urlPrefix + "auth/strategy"
            }
            response.prompts = [
                {type:"button",label:mergedAdminAuth.strategy.label, url: urlPrefix + "auth/strategy"}
            ]
            if (mergedAdminAuth.strategy.icon) {
                response.prompts[0].icon = mergedAdminAuth.strategy.icon;
            }
            if (mergedAdminAuth.strategy.image) {
                response.prompts[0].image = theme.serveFile('/login/',mergedAdminAuth.strategy.image);
            }
        }
        let themeContext = await theme.context();
        if (themeContext.login && themeContext.login.image) {
            response.image = themeContext.login.image;
        }
        if (themeContext.login?.message) {
            response.loginMessage = themeContext.login?.message
        }
        if (themeContext.login?.button) {
            response.prompts = [
                { type: "button", ...themeContext.login.button }
            ]
        }
    }
    res.json(response);
}

function revoke(req,res) {
    var token = req.body.token;
    // TODO: audit log
    Tokens.revoke(token).then(function() {
        log.audit({event: "auth.login.revoke"},req);
        if (settings.editorTheme && settings.editorTheme.logout && settings.editorTheme.logout.redirect) {
            res.json({redirect:settings.editorTheme.logout.redirect});
        } else {
            res.status(200).end();
        }
    });
}

function completeVerify(profile,done) {
    Users.authenticate(profile).then(function(user) {
        if (user) {
            Tokens.create(user.username,"node-red-editor",user.permissions).then(function(tokens) {
                log.audit({event: "auth.login",user,username:user.username,scope:user.permissions});
                user.tokens = tokens;
                done(null,user);
            });
        } else {
            log.audit({event: "auth.login.fail.oauth",username:typeof profile === "string"?profile:profile.username});
            done(null,false);
        }
    });
}


function genericStrategy(adminApp,strategy) {
    const crypto = require("crypto")
    const session = require('express-session')
    const MemoryStore = require('memorystore')(session)

    const sessionOptions = {
        // As the session is only used across the life-span of an auth
        // hand-shake, we can use a instance specific random string
        secret: crypto.randomBytes(20).toString('hex'),
        resave: false,
        saveUninitialized: false,
        store: new MemoryStore({
          checkPeriod: 86400000 // prune expired entries every 24h
        })
    }
    if (settings.httpAdminCookieOptions) {
        sessionOptions.cookie = {
            path: '/',
            httpOnly: true,
            secure: false,
            maxAge: null,
            ...settings.httpAdminCookieOptions
        }
        if (sessionOptions.cookie.name){
            sessionOptions.name = sessionOptions.cookie.name
            delete sessionOptions.cookie.name
        }
    }
    adminApp.use(session(sessionOptions));
    //TODO: all passport references ought to be in ./auth
    adminApp.use(passport.initialize());
    adminApp.use(passport.session());

    var options = strategy.options;
    var verify = function() {
        var originalDone = arguments[arguments.length-1];
        if (options.verify) {
            var args = Array.from(arguments);
            args[args.length-1] = function(err,profile) {
                if (err) {
                    return originalDone(err);
                } else {
                    return completeVerify(profile,originalDone);
                }
            };

            options.verify.apply(this,args);
        } else {
            var profile = arguments[arguments.length - 2];
            return completeVerify(profile,originalDone);
        }
    };
    // Give our callback the same arity as the original one from options
    if (options.verify) {
        Object.defineProperty(verify, "length", { value: options.verify.length })
    }

    passport.use(new strategy.strategy(options, verify));

    adminApp.get('/auth/strategy',
        passport.authenticate(strategy.name, {
            session:false,
            failWithError: true,
            failureMessage: true
        }),
        completeGenericStrategyAuth,
        handleStrategyError
    );

    var callbackMethodFunc = adminApp.get;
    if (/^post$/i.test(options.callbackMethod)) {
        callbackMethodFunc = adminApp.post;
    }
    callbackMethodFunc.call(adminApp,'/auth/strategy/callback',
        passport.authenticate(strategy.name, {
            session:false,
            failureMessage: true,
            failWithError: true
        }),
        completeGenericStrategyAuth,
        handleStrategyError
    );

}
function completeGenericStrategyAuth(req,res) {
    var tokens = req.user.tokens;
    delete req.user.tokens;
    // Successful authentication, redirect home.
    res.redirect(settings.httpAdminRoot + '?access_token='+tokens.accessToken);
}
function handleStrategyError(err, req, res, next) {
    if (res.headersSent) {
        return next(err)
    }
    // Remove the header that passport auto-adds as we don't need it
    res.removeHeader('WWW-Authenticate')
    log.audit({event: "auth.login.fail.oauth",error:err.toString()});
    res.redirect(settings.httpAdminRoot + '?session_message='+err.toString());
}

module.exports = {
    init: init,
    needsPermission: needsPermission,
    ensureClientSecret: ensureClientSecret,
    authenticateClient: authenticateClient,
    getToken: getToken,
    errorHandler: function(err,req,res,next) {
        //TODO: audit log statment
        //console.log(err.stack);
        //log.log({level:"audit",type:"auth",msg:err.toString()});
        return server.errorHandler()(err,req,res,next);
    },
    login: login,
    revoke: revoke,
    genericStrategy: genericStrategy
}
