"use strict";

process.env.TZ = "Etc/GMT";

console.log("Loading photos.ketr");

const express = require("express"),
  morgan = require("morgan"),
  bodyParser = require("body-parser"),
  config = require("config"),
  session = require('express-session'),
  hb = require("handlebars"),
  SQLiteStore = require('connect-sqlite3')(session),
  scanner = require("./scanner");

require("./console-line.js"); /* Monkey-patch console.log with line numbers */

const picturesPath = config.get("picturesPath").replace(/\/$/, "") + "/",
  serverConfig = config.get("server");

let basePath = config.get("basePath");
basePath = "/" + basePath.replace(/^\/+/, "").replace(/\/+$/, "") + "/";
if (basePath == "//") {
  basePath = "/";
}

let photoDB = null, userDB = null;
console.log("Loading pictures out of: " + picturesPath);
console.log("Hosting server from: " + basePath);

const app = express();

app.set("basePath", basePath);

/* App is behind an nginx proxy which we trust, so use the remote address
 * set in the headers */
app.set("trust proxy", true);

app.use(basePath, require("./routes/basepath.js"));

/* Handle static files first so excessive logging doesn't occur */
app.use(basePath, express.static("frontend", { index: false }));

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
  extended: false
}));

/* *******************************************************************************
 * Logging - begin
 *
 * This runs before after cookie parsing, but before routes. If we set
 * immediate: true on the morgan options, it happens before cookie parsing
 * */

 morgan.token('remote-user', function (req) {
  return req.user ? req.user.username : "N/A";
});

/* Any path starting with the following won't be logged via morgan */
const logSkipPaths = new RegExp("^" + basePath + "(" + [
  ".*thumbs\\/",
  "bower_components",
].join(")|(") + ")");
app.use(morgan('common', {
  skip: function (req) {
    return logSkipPaths.exec(req.originalUrl);
  }
}));

/* 
 * Logging - end
 * ******************************************************************************* */

/* body-parser does not support text/*, so add support for that here */
app.use(function(req, res, next){
  if (!req.is('text/*')) {
    return next();
  }
  req.setEncoding('utf8');
  let text = '';
  req.on('data', function(chunk) {
    text += chunk;
  });
  req.on('end', function() {
    req.text = text;
    next();
  });
});

app.use(session({
  store: new SQLiteStore({ db: config.get("sessions.db") }),
  secret: config.get("sessions.store-secret"),
  cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 }, // 1 week
  saveUninitialized: false,
  resave: true
}));

const index = require("./routes/index");

if (config.has("admin.mail") &&
  config.has("smtp.host") &&
  config.has("smtp.sender")) {
  app.set("transporter", require("nodemailer").createTransport({
    host: config.get("smtp.host"),
    pool: true,
    port: config.has("smtp.port") ? config.get("smtp.port") : 25
  }));
} else {
  console.log("SMTP disabled. To enable SMTP, configure admin.mail, smtp.host, and smtp.sender");
}

const templates = {
  "html": [
    "<p>The user {{displayName}} has verified their email address ({{mail}}).</p>",
    "",
    "<p>They indicated they know:</p>",
    "<pre>{{notes}}</pre>",
    "",
    "<p>To authenticate:</p>",
    "<p>echo 'UPDATE users SET authenticated=1 WHERE id={{id}};' | sqlite3 users.db</p>",
    "",
    "<p>Sincerely,<br>",
    "James</p>"
  ].join("\n"),
  "text": [
    "The user {{displayName}} has verified their email address ({{mail}}).",
    "",
    "They indicated they know:",
    "{{notes}}",
    "",
    "To authenticate:",
    "echo 'UPDATE users SET authenticated=1 WHERE id={{id}};' | sqlite3 users.db",
    "",
    "Sincerely,",
    "James"
  ].join("\n")
};

/* Look for action-token URLs and process; this does not require a user to be logged
 * in */
app.use(basePath, function(req, res, next) {
  let match = req.url.match(/^\/([0-9a-f]+)$/);
  if (!match) {
    return next();
  }

  let key = match[1];
  return userDB.sequelize.query("SELECT * FROM authentications WHERE key=:key", {
    replacements: {
      key: key
    },
    type: userDB.sequelize.QueryTypes.SELECT
  }).then(function(results) {
    let token;
    if (results.length == 0) {
      console.log("Invalid key. Ignoring.");
      return next();
    }

    token = results[0];

    console.log("Matched token: " + JSON.stringify(token, null, 2));
    switch (token.type) {
    case "account-setup":
      return userDB.sequelize.query("UPDATE users SET mailVerified=1 WHERE id=:userId", {
        replacements: token
      }).then(function() {
        return userDB.sequelize.query("DELETE FROM authentications WHERE key=:key", {
          replacements: token
        });
      }).then(function() {
        return userDB.sequelize.query("SELECT * FROM users WHERE id=:userId", {
          replacements: token,
          type: userDB.sequelize.QueryTypes.SELECT
        }).then(function(results) {
          if (results.length == 0) {
            throw "DB mis-match between authentications and users table";
          }
          const transporter = app.get("transporter");
          if (!transporter) {
            console.log("Not sending VERIFIED email; SMTP not configured.");
            return;
          }

          let user = results[0],
            envelope = {
              to: config.get("admin.mail"),
              from: config.get("smtp.sender"),
              subject: "VERIFIED: Account'" + user.displayName + "'",
              cc: "",
              bcc: "",
              text: hb.compile(templates.text)(user),
              html: hb.compile(templates.html)(user)
            };

          req.session.userId = user.id;

          return new Promise(function (resolve, reject) {
            let attempts = 10;

            function send(envelope) {
              /* Rate limit to ten per second */
              transporter.sendMail(envelope, function (error, info) {
                if (!error) {
                  console.log('Message sent: ' + info.response);
                  return resolve();
                }

                if (attempts == 0) {
                  console.log("Error sending email: ", error)
                  return reject(error);
                }
                
                attempts--;
                console.log("Unable to send mail. Trying again in 100ms (" + attempts + " attempts remain): ", error);
                setTimeout(send.bind(undefined, envelope), 100);
              });
            }

            send(envelope);
          });
        }).then(function() {
          return res.redirect(308, basePath);
        });
      });
    }
    
    return next();
  });
});

/* Allow loading of the app w/out being logged in */
app.use(basePath, index);

/* Allow access to the 'users' API w/out being logged in */
const users = require("./routes/users");
app.use(basePath + "api/v1/users", users.router);

app.use(function(err, req, res, next) {
  res.status(err.status || 500).json({
    message: err.message,
    error: {}
  });
});

/* Check authentication */
app.use(basePath, function(req, res, next) {
  return users.getSessionUser(req).then(function(user) {
    if (user.restriction) {
      return res.status(401).send(user.restriction);
    }
    req.user = user;
    return next();
  }).catch(function(error) {
    return res.status(403).send(error);
  });
});

/* Everything below here requires a successful authentication */
app.use(basePath, express.static(picturesPath, { index: false }));

app.use(basePath + "api/v1/photos", require("./routes/photos"));
app.use(basePath + "api/v1/days", require("./routes/days"));
app.use(basePath + "api/v1/albums", require("./routes/albums"));
app.use(basePath + "api/v1/holidays", require("./routes/holidays"));
app.use(basePath + "api/v1/scan", require("./routes/scan")(scanner));

/* Declare the "catch all" index route last; the final route is a 404 dynamic router */
app.use(basePath, index);

/**
 * Create HTTP server and listen for new connections
 */
app.set("port", serverConfig.port);

const server = require("http").createServer(app);

require("./db/photos").then(function(db) {
  photoDB = db;
}).then(function() {
  return require("./db/users").then(function(db) {
    userDB = db;
  });
}).then(function() {
  console.log("DB connected. Opening server.");
  server.listen(serverConfig.port);
}).then(function() {
  console.log("Scanning.");
  scanner.init(photoDB);
  return scanner.scan();
}).then(function() {
  console.log("Scanning completed.");
}).catch(function(error) {
  console.error(error);
  process.exit(-1);
});

server.on("error", function(error) {
  if (error.syscall !== "listen") {
    throw error;
  }

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case "EACCES":
      console.error(serverConfig.port + " requires elevated privileges");
      process.exit(1);
      break;
    case "EADDRINUSE":
      console.error(serverConfig.port + " is already in use");
      process.exit(1);
      break;
    default:
      throw error;
  }
});

server.on("listening", function() {
  console.log("Listening on " + serverConfig.port);
});
