/**
 * 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.
 **/

var BearerStrategy = require('passport-http-bearer').Strategy;
var ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy;

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

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

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

var bearerStrategy = function (accessToken, done) {
    // is this a valid token?
    Tokens.get(accessToken).then(function(token) {
        if (token) {
            Users.get(token.user).then(function(user) {
                if (user) {
                    done(null,user,{scope:token.scope});
                } else {
                    log.audit({event: "auth.invalid-token"});
                    done(null,false);
                }
            });
        } else {
            log.audit({event: "auth.invalid-token"});
            done(null,false);
        }
    });
}
bearerStrategy.BearerStrategy = new BearerStrategy(bearerStrategy);

var clientPasswordStrategy = function(clientId, clientSecret, done) {
    Clients.get(clientId).then(function(client) {
        if (client && client.secret == clientSecret) {
            done(null,client);
        } else {
            log.audit({event: "auth.invalid-client",client:clientId});
            done(null,false);
        }
    });
}
clientPasswordStrategy.ClientPasswordStrategy = new ClientPasswordStrategy(clientPasswordStrategy);

var loginAttempts = [];
var loginSignInWindow = 600000; // 10 minutes


var passwordTokenExchange = function(client, username, password, scope, done) {
    var now = Date.now();
    loginAttempts = loginAttempts.filter(function(logEntry) {
        return logEntry.time + loginSignInWindow > now;
    });
    loginAttempts.push({time:now, user:username});
    var attemptCount = 0;
    loginAttempts.forEach(function(logEntry) {
        /* istanbul ignore else */
        if (logEntry.user == username) {
            attemptCount++;
        }
    });
    if (attemptCount > 5) {
        log.audit({event: "auth.login.fail.too-many-attempts",username:username,client:client.id});
        done(new Error("Too many login attempts. Wait 10 minutes and try again"),false);
        return;
    }

    Users.authenticate(username,password).then(function(user) {
        if (user) {
            if (scope === "") {
                scope = user.permissions;
            }
            if (permissions.hasPermission(user.permissions,scope)) {
                loginAttempts = loginAttempts.filter(function(logEntry) {
                    return logEntry.user !== username;
                });
                // Check if the user contains a user defined token and use it
                // instead of generating a new token
                if(user.token){
                    done(null,user.token,null,null);
                } else {
                    Tokens.create(username,client.id,scope).then(function(tokens) {
                        log.audit({event: "auth.login",user,username:username,client:client.id,scope:scope});
                        done(null,tokens.accessToken,null,{expires_in:tokens.expires_in});
                    });
                }
            } else {
                log.audit({event: "auth.login.fail.permissions",username:username,client:client.id,scope:scope});
                done(null,false);
            }
        } else {
            log.audit({event: "auth.login.fail.credentials",username:username,client:client.id,scope:scope});
            done(null,false);
        }
    });
}

function AnonymousStrategy() {
  passport.Strategy.call(this);
  this.name = 'anon';
}
util.inherits(AnonymousStrategy, passport.Strategy);
AnonymousStrategy.prototype.authenticate = function(req) {
    var self = this;
    Users.default().then(function(anon) {
        if (anon) {
            self.success(anon,{scope:anon.permissions});
        } else {
            self.fail(401);
        }
    });
}


function authenticateUserToken(req) {
    return new Promise( (resolve,reject) => {
        var token = null;
        var tokenHeader = Users.tokenHeader();
        if (Users.tokenHeader() === null) {
            // No custom user token provided. Fail the request
            reject();
            return;
        } else if (Users.tokenHeader() === 'authorization') {
            if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
                token = req.headers.authorization.split(' ')[1];
            }
        } else {
            token = req.headers[Users.tokenHeader()];
        }
        if (token) {
            Users.tokens(token).then(function(user) {
                if (user) {
                    resolve(user);
                } else {
                    reject();
                }
            }).catch(reject);
        } else {
            reject();
        }
    });
}


function TokensStrategy() {
  passport.Strategy.call(this);
  this.name = 'tokens';
}
util.inherits(TokensStrategy, passport.Strategy);
TokensStrategy.prototype.authenticate = function(req) {
    authenticateUserToken(req).then(user => {
        this.success(user,{scope:user.permissions});
    }).catch(err => {
        if (err) {
            log.trace("token authentication failure: "+err.stack?err.stack:err)
        }
        this.fail(401);
    });
}



module.exports = {
    bearerStrategy: bearerStrategy,
    clientPasswordStrategy: clientPasswordStrategy,
    passwordTokenExchange: passwordTokenExchange,
    anonymousStrategy: new AnonymousStrategy(),
    tokensStrategy: new TokensStrategy(),
    authenticateUserToken: authenticateUserToken
}
