const express = require('express');
const exphbs = require('express-handlebars');
const busboy = require('connect-busboy');
const path = require('path');
const bodyParser = require('body-parser');
const helmet = require('helmet');
const bytes = require('bytes');
const conf = require('./config.js');
const storage = require('./storage.js');
const Raven = require('raven');
const crypto = require('crypto');
const fs = require('fs');
const version = require('../public/version.json');

if (conf.sentry_dsn) {
  Raven.config(conf.sentry_dsn).install();
}

const mozlog = require('./log.js');

const log = mozlog('send.server');

const STATIC_PATH = path.join(__dirname, '../public');

const app = express();

function allLangs() {
  return fs
    .readdirSync(path.join(STATIC_PATH, 'locales'))
    .map(function(f) {
      return f.split('.')[0];
    })
    .join(',');
}

function prodLangs() {
  return require('../package.json').availableLanguages.join(',');
}

const availableLanguages = conf.l10n_dev ? allLangs() : prodLangs();

app.engine(
  'handlebars',
  exphbs({
    defaultLayout: 'main',
    partialsDir: 'views/partials/',
    helpers: {
      availableLanguages,
      l10nDev: conf.l10n_dev
    }
  })
);
app.set('view engine', 'handlebars');

app.use(helmet());
app.use(
  helmet.hsts({
    maxAge: 31536000,
    force: conf.env === 'production'
  })
);
app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      connectSrc: [
        "'self'",
        'https://sentry.prod.mozaws.net',
        'https://www.google-analytics.com'
      ],
      imgSrc: ["'self'", 'https://www.google-analytics.com'],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", 'https://code.cdn.mozilla.net'],
      fontSrc: ["'self'", 'https://code.cdn.mozilla.net'],
      formAction: ["'none'"],
      frameAncestors: ["'none'"],
      objectSrc: ["'none'"],
      reportUri: '/__cspreport__'
    }
  })
);
app.use(
  busboy({
    limits: {
      fileSize: conf.max_file_size
    }
  })
);
app.use(bodyParser.json());
app.use(express.static(STATIC_PATH));

app.get('/', (req, res) => {
  res.render('index');
});

app.get('/unsupported/:reason', (req, res) => {
  const outdated = req.params.reason === 'outdated';
  res.render('unsupported', {
    outdated,
    fira: true
  });
});

app.get('/legal', (req, res) => {
  res.render('legal');
});

app.get('/jsconfig.js', (req, res) => {
  res.set('Content-Type', 'application/javascript');
  res.render('jsconfig', {
    googleAnalyticsId: conf.analytics_id,
    sentryId: conf.sentry_id,
    version: version.version,
    commit: version.commit,
    maxFileSize: conf.max_file_size,
    expireSeconds: conf.expire_seconds,
    layout: false
  });
});

app.get('/exists/:id', async (req, res) => {
  const id = req.params.id;
  if (!validateID(id)) {
    res.sendStatus(404);
    return;
  }

  try {
    await storage.exists(id);
    res.sendStatus(200);
  } catch (e) {
    res.sendStatus(404);
  }
});

app.get('/download/:id', async (req, res) => {
  const id = req.params.id;
  if (!validateID(id)) {
    res.sendStatus(404);
    return;
  }

  try {
    const filename = await storage.filename(id);
    const contentLength = await storage.length(id);
    const timeToExpiry = await storage.ttl(id);
    res.render('download', {
      filename: decodeURIComponent(filename),
      filesize: bytes(contentLength),
      sizeInBytes: contentLength,
      timeToExpiry: timeToExpiry
    });
  } catch (e) {
    res.status(404).render('notfound');
  }
});

app.get('/assets/download/:id', async (req, res) => {
  const id = req.params.id;
  if (!validateID(id)) {
    res.sendStatus(404);
    return;
  }

  try {
    const meta = await storage.metadata(id);
    const contentLength = await storage.length(id);
    res.writeHead(200, {
      'Content-Disposition': `attachment; filename=${meta.filename}`,
      'Content-Type': 'application/octet-stream',
      'Content-Length': contentLength,
      'X-File-Metadata': JSON.stringify(meta)
    });
    const file_stream = storage.get(id);

    file_stream.on('end', async () => {
      try {
        const err = await storage.forceDelete(id);
        if (!err) {
          log.info('Deleted:', id);
        }
      } catch (e) {
        log.info('DeleteError:', id);
      }
    });

    file_stream.pipe(res);
  } catch (e) {
    res.sendStatus(404);
  }
});

app.post('/delete/:id', async (req, res) => {
  const id = req.params.id;

  if (!validateID(id)) {
    res.sendStatus(404);
    return;
  }

  const delete_token = req.body.delete_token;

  if (!delete_token) {
    res.sendStatus(404);
    return;
  }

  try {
    const err = await storage.delete(id, delete_token);
    if (!err) {
      log.info('Deleted:', id);
      res.sendStatus(200);
    }
  } catch (e) {
    res.sendStatus(404);
  }
});

app.post('/upload', (req, res, next) => {
  const newId = crypto.randomBytes(5).toString('hex');
  let meta;

  try {
    meta = JSON.parse(req.header('X-File-Metadata'));
  } catch (e) {
    res.sendStatus(400);
    return;
  }

  if (
    !meta.hasOwnProperty('aad') ||
    !meta.hasOwnProperty('id') ||
    !meta.hasOwnProperty('filename') ||
    !validateIV(meta.id)
  ) {
    res.sendStatus(404);
    return;
  }

  meta.delete = crypto.randomBytes(10).toString('hex');
  log.info('meta', meta);
  req.pipe(req.busboy);

  req.busboy.on('file', async (fieldname, file, filename) => {
    log.info('Uploading:', newId);

    try {
      await storage.set(newId, file, filename, meta);

      const protocol = conf.env === 'production' ? 'https' : req.protocol;
      const url = `${protocol}://${req.get('host')}/download/${newId}/`;
      res.json({
        url,
        delete: meta.delete,
        id: newId
      });
    } catch (e) {
      if (e.message === 'limit') {
        return res.sendStatus(413);
      }
      res.sendStatus(500);
    }
  });

  req.on('close', async err => {
    try {
      const err = await storage.forceDelete(newId);
      if (!err) {
        log.info('Deleted:', newId);
      }
    } catch (e) {
      log.info('DeleteError:', newId);
    }
  });
});

app.get('/__lbheartbeat__', (req, res) => {
  res.sendStatus(200);
});

app.get('/__heartbeat__', async (req, res) => {
  try {
    await storage.ping();
    res.sendStatus(200);
  } catch (e) {
    res.sendStatus(500);
  }
});

app.get('/__version__', (req, res) => {
  res.sendFile(path.join(STATIC_PATH, 'version.json'));
});

const server = app.listen(conf.listen_port, () => {
  log.info('startServer:', `Send app listening on port ${conf.listen_port}!`);
});

const validateID = route_id => {
  return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
};

const validateIV = route_id => {
  return route_id.match(/^[0-9a-fA-F]{24}$/) !== null;
};

module.exports = {
  server: server,
  storage: storage
};