text: const path = require('path');
  const config_api = require('../config');
  const consts = require('../consts');
  var subscriptions_api = require('../subscriptions')
  const fs = require('fs-extra');
  var jwt = require('jsonwebtoken');
  const { uuid } = require('uuidv4');
  var bcrypt = require('bcrypt');
  
  
  var LocalStrategy = require('passport-local').Strategy;
  var JwtStrategy = require('passport-jwt').Strategy,
      ExtractJwt = require('passport-jwt').ExtractJwt;
  
  // other required vars
  let logger = null;
  var users_db = null;
  let SERVER_SECRET = null;
  let JWT_EXPIRATION = null;
  let opts = null;
  let saltRounds = null;
  
  exports.initialize = function(input_users_db, input_logger) {
    setLogger(input_logger)
    setDB(input_users_db);
  
    /*************************
     * Authentication module
     ************************/
    saltRounds = 10;
  
    JWT_EXPIRATION = (60 * 60); // one hour
  
    SERVER_SECRET = null;
    if (users_db.get('jwt_secret').value()) {
      SERVER_SECRET = users_db.get('jwt_secret').value();
    } else {
      SERVER_SECRET = uuid();
      users_db.set('jwt_secret', SERVER_SECRET).write();
    }
  
    opts = {}
    opts.jwtFromRequest = ExtractJwt.fromUrlQueryParameter('jwt');
    opts.secretOrKey = SERVER_SECRET;
    /*opts.issuer = 'example.com';
    opts.audience = 'example.com';*/
  
    exports.passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
      const user = users_db.get('users').find({uid: jwt_payload.user}).value();
      if (user) {
          return done(null, user);
      } else {
          return done(null, false);
          // or you could create a new account
      }
    }));
  }
  
  function setLogger(input_logger) {
    logger = input_logger;
  }
  
  function setDB(input_users_db) {
    users_db = input_users_db;
  }
  
  exports.passport = require('passport');
  
  exports.passport.serializeUser(function(user, done) {
      done(null, user);
  });
    
  exports.passport.deserializeUser(function(user, done) {
      done(null, user);
  });
  
  /***************************************
   * Register user with hashed password
   **************************************/
  exports.registerUser = function(req, res) {
    var userid = req.body.userid;
    var username = req.body.username;
    var plaintextPassword = req.body.password;
  
    if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) {
      res.sendStatus(409);
      logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
      return;
    }
    
    bcrypt.hash(plaintextPassword, saltRounds)
      .then(function(hash) {
        let new_user = {
          name: username,
          uid: userid,
          passhash: hash,
          files: {
            audio: [],
            video: []
          },
          playlists: {
            audio: [],
            video: []
          },
          subscriptions: [],
          created: Date.now(),
          role: userid === 'admin' ? 'admin' : 'user',
          permissions: [],
          permission_overrides: []
        };
        // check if user exists
        if (users_db.get('users').find({uid: userid}).value()) {
          // user id is taken!
          logger.error('Registration failed: UID is already taken!');
          res.status(409).send('UID is already taken!');
        } else if (users_db.get('users').find({name: username}).value()) {
            // user name is taken!
            logger.error('Registration failed: User name is already taken!');
            res.status(409).send('User name is already taken!');
        } else {
          // add to db
          users_db.get('users').push(new_user).write();
          logger.verbose(`New user created: ${new_user.name}`);
          res.send({
            user: new_user
          });
        }
      })
      .then(function(result) {
        
      })
      .catch(function(err) {
        logger.error(err);
        if( err.code == 'ER_DUP_ENTRY' ) {
          res.status(409).send('UserId already taken');
        } else {
          res.sendStatus(409);
        }
      });
  }
  
  /***************************************
   * Login methods
   **************************************/
  
  /*************************************************
   * This gets called when passport.authenticate()
   * gets called.
   * 
   * This checks that the credentials are valid.
   * If so, passes the user info to the next middleware.
   ************************************************/
  
  
  exports.passport.use(new LocalStrategy({
      usernameField: 'userid',
      passwordField: 'password'},
      function(username, password, done) {
          const user = users_db.get('users').find({name: username}).value();
          if (!user) { logger.error(`User ${username} not found`); return done(null, false); }
          if (user) {
              return done(null, bcrypt.compareSync(password, user.passhash) ? user : false);
          }
      }
  ));
  
  /*passport.use(new BasicStrategy(
    function(userid, plainTextPassword, done) {
      const user = users_db.get('users').find({name: userid}).value();
      if (user) {
            var hashedPwd = user.passhash;
            return bcrypt.compare(plainTextPassword, hashedPwd);
      } else {
            return false;
      }
    }
  ));
  */
  
  /*************************************************************
   * This is a wrapper for auth.passport.authenticate().
   * We use this to change WWW-Authenticate header so
   * the browser doesn't pop-up challenge dialog box by default.
   * Browser's will pop-up up dialog when status is 401 and 
   * "WWW-Authenticate:Basic..."
   *************************************************************/
  /*
  exports.authenticateViaPassport = function(req, res, next) {
    exports.passport.authenticate('basic',{session:false}, 
      function(err, user, info) {
        if(!user){
          res.set('WWW-Authenticate', 'x'+info); // change to xBasic
          res.status(401).send('Invalid Authentication');
        } else {
          req.user = user;
          next();
        }
      }
    )(req, res, next);
  };
  */
  
  /**********************************
   * Generating/Signing a JWT token
   * And attaches the user info into
   * the payload to be sent on every
   * request.
   *********************************/
  exports.generateJWT = function(req, res, next) {
    var payload = {
        exp: Math.floor(Date.now() / 1000) + JWT_EXPIRATION
      , user: req.user.uid
    };
    req.token = jwt.sign(payload, SERVER_SECRET);
    next();  
  }
  
  exports.returnAuthResponse = function(req, res) {
    res.status(200).json({
      user: req.user,
      token: req.token,
      permissions: exports.userPermissions(req.user.uid),
      available_permissions: consts['AVAILABLE_PERMISSIONS']
    });  
  }
  
  /***************************************
   * Authorization: middleware that checks the 
   * JWT token for validity before allowing
   * the user to access anything.
   *
   * It also passes the user object to the next
   * middleware through res.locals
   **************************************/
  exports.ensureAuthenticatedElseError = function(req, res, next) {
    var token = getToken(req.query);
    if( token ) {
      try {
        var payload = jwt.verify(token, SERVER_SECRET);
        // console.log('payload: ' + JSON.stringify(payload));
        // check if user still exists in database if you'd like
        res.locals.user = payload.user;
        next();
      } catch(err) {
        res.status(401).send('Invalid Authentication');
      }
    } else {
      res.status(401).send('Missing Authorization header');
    }
  }
  
  // change password
  exports.changeUserPassword = async function(user_uid, new_pass) {
    return new Promise(resolve => {
      bcrypt.hash(new_pass, saltRounds)
      .then(function(hash) {
        users_db.get('users').find({uid: user_uid}).assign({passhash: hash}).write();
        resolve(true);
      }).catch(err => {
        resolve(false);
      });
    });
  }
  
  // change user permissions
  exports.changeUserPermissions = function(user_uid, permission, new_value) {
    try {
      const user_db_obj = users_db.get('users').find({uid: user_uid});
      user_db_obj.get('permissions').pull(permission).write();
      user_db_obj.get('permission_overrides').pull(permission).write();
      if (new_value === 'yes') {
        user_db_obj.get('permissions').push(permission).write();
        user_db_obj.get('permission_overrides').push(permission).write();
      } else if (new_value === 'no') {
        user_db_obj.get('permission_overrides').push(permission).write();
      }
      return true;
    } catch (err) {
      logger.error(err);
      return false;
    }
  }
  
  // change role permissions
  exports.changeRolePermissions = function(role, permission, new_value) {
    try {
      const role_db_obj = users_db.get('roles').get(role);
      role_db_obj.get('permissions').pull(permission).write();
      if (new_value === 'yes') {
        role_db_obj.get('permissions').push(permission).write();
      }
      return true;
    } catch (err) {
      logger.error(err);
      return false;
    }
  }
  
  exports.adminExists = function() {
    return !!users_db.get('users').find({uid: 'admin'}).value();
  }
  
  // video stuff
  
  exports.getUserVideos = function(user_uid, type) {
      const user = users_db.get('users').find({uid: user_uid}).value();
      return user['files'][type];
  }
  
  exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false) {
    if (!type) {
      file = users_db.get('users').find({uid: user_uid}).get(`files.audio`).find({uid: file_uid}).value();
      if (!file) {
          file = users_db.get('users').find({uid: user_uid}).get(`files.video`).find({uid: file_uid}).value();
          if (file) type = 'video';
      } else {
          type = 'audio';
      }
    }
  
    if (!file && type) file = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
  
    // prevent unauthorized users from accessing the file info
    if (requireSharing && !file['sharingEnabled']) file = null;
  
    return file;
  }
  
  exports.addPlaylist = function(user_uid, new_playlist, type) {
    users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).push(new_playlist).write();
    return true;
  }
  
  exports.updatePlaylist = function(user_uid, playlistID, new_filenames, type) {
    users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID}).assign({fileNames: new_filenames});
    return true;
  }
  
  exports.removePlaylist = function(user_uid, playlistID, type) {
    users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).remove({id: playlistID}).write();
    return true;
  }
  
  exports.getUserPlaylists = function(user_uid, type) {
    const user = users_db.get('users').find({uid: user_uid}).value();
    return user['playlists'][type];
  }
  
  exports.getUserPlaylist = function(user_uid, playlistID, type, requireSharing = false) {
    let playlist = null;
    if (!type) {
      playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.audio`).find({id: playlistID}).value();
      if (!playlist) {
        playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.video`).find({id: playlistID}).value();
        if (playlist) type = 'video';
      } else {
          type = 'audio';
      }
    }
    if (!playlist) playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID}).value();
  
    // prevent unauthorized users from accessing the file info
    if (requireSharing && !playlist['sharingEnabled']) playlist = null;
  
    return playlist;
  }
  
  exports.registerUserFile = function(user_uid, file_object, type) {
    users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
      .remove({
          path: file_object['path']
      }).write();
  
    users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
        .push(file_object)
        .write();
  }
  
  exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = false) {
    let success = false;
    const file_obj = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
    if (file_obj) {
      const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
      const ext = type === 'audio' ? '.mp3' : '.mp4';
  
      // close descriptors
      if (config_api.descriptors[file_obj.id]) {
        try {
            for (let i = 0; i < config_api.descriptors[file_obj.id].length; i++) {
              config_api.descriptors[file_obj.id][i].destroy();
            }
        } catch(e) {
  
        }
      } 
  
      const full_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext);
      users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
        .remove({
            uid: file_uid
        }).write();
      if (fs.existsSync(full_path)) {
        // remove json and file
        const json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + '.info.json');
        const alternate_json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext + '.info.json');
        let youtube_id = null;
        if (fs.existsSync(json_path)) {
          youtube_id = fs.readJSONSync(json_path).id;
          fs.unlinkSync(json_path);
        } else if (fs.existsSync(alternate_json_path)) {
          youtube_id = fs.readJSONSync(alternate_json_path).id;
          fs.unlinkSync(alternate_json_path);
        }
  
        fs.unlinkSync(full_path);
  
        // do archive stuff
  
        let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
        if (useYoutubeDLArchive) {
            const archive_path = path.join(usersFileFolder, user_uid, 'archives', `archive_${type}.txt`);
  
            // use subscriptions API to remove video from the archive file, and write it to the blacklist
            if (fs.existsSync(archive_path)) {
                const line = youtube_id ? subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null;
                if (blacklistMode && line) {
                  let blacklistPath = path.join(usersFileFolder, user_uid, 'archives', `blacklist_${type}.txt`);
                  // adds newline to the beginning of the line
                  line = '\n' + line;
                  fs.appendFileSync(blacklistPath, line);
                }
            } else {
                logger.info(`Could not find archive file for ${type} files. Creating...`);
                fs.ensureFileSync(archive_path);
            }
        }
      }
      success = true;
    } else {
      success = false;
      logger.warn(`User file ${file_uid} does not exist!`);
    }
  
    return success;
  }
  
  exports.changeSharingMode = function(user_uid, file_uid, type, is_playlist, enabled) {
    let success = false;
    const user_db_obj = users_db.get('users').find({uid: user_uid});
    if (user_db_obj.value()) {
      const file_db_obj = is_playlist ? user_db_obj.get(`playlists.${type}`).find({id: file_uid}) : user_db_obj.get(`files.${type}`).find({uid: file_uid});
      if (file_db_obj.value()) {
        success = true;
        file_db_obj.assign({sharingEnabled: enabled}).write();
      }
    }
    
    return success;
  }
  
  exports.userHasPermission = function(user_uid, permission) {
    const user_obj = users_db.get('users').find({uid: user_uid}).value();
    const role = user_obj['role'];
    if (!role) {
      // role doesn't exist
      logger.error('Invalid role ' + role);
      return false;
    }
    const role_permissions = (users_db.get('roles').value())['permissions'];
  
    const user_has_explicit_permission = user_obj['permissions'].includes(permission);
    const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
    
    // check if user has a negative/positive override
    if (user_has_explicit_permission && permission_in_overrides) {
      // positive override
      return true;
    } else if (!user_has_explicit_permission && permission_in_overrides) {
      // negative override
      return false;
    }
  
    // no overrides, let's check if the role has the permission
    if (role_permissions.includes(permission)) {
      return true;
    } else {
      logger.verbose(`User ${user_uid} failed to get permission ${permission}`);
      return false;
    }
  }
  
  exports.userPermissions = function(user_uid) {
    let user_permissions = [];
    const user_obj = users_db.get('users').find({uid: user_uid}).value();
    const role = user_obj['role'];
    if (!role) {
      // role doesn't exist
      logger.error('Invalid role ' + role);
      return null;
    }
    const role_permissions = users_db.get('roles').get(role).get('permissions').value()
  
    for (let i = 0; i < consts['AVAILABLE_PERMISSIONS'].length; i++) {
      let permission = consts['AVAILABLE_PERMISSIONS'][i];
  
      const user_has_explicit_permission = user_obj['permissions'].includes(permission);
      const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
      
      // check if user has a negative/positive override
      if (user_has_explicit_permission && permission_in_overrides) {
        // positive override
        user_permissions.push(permission);
      } else if (!user_has_explicit_permission && permission_in_overrides) {
        // negative override
        continue;
      }
  
      // no overrides, let's check if the role has the permission
      if (role_permissions.includes(permission)) {
        user_permissions.push(permission);
      } else {
        continue;
      }
    }
  
    return user_permissions;
  }
  
  function getToken(queryParams) {
    if (queryParams && queryParams.jwt) {
      var parted = queryParams.jwt.split(' ');
      if (parted.length === 2) {
        return parted[1];
      } else {
        return null;
      }
    } else {
      return null;
    }
  };