'use strict';
const config = require('../config');
const tusboy = require('tusboy').default;
const express = require('express');
const morgan = require('morgan');
const compression = require('compression');
const Store = require('./store');
const uuid = require('uuid/v4');
const path = require('path');
const fs = require("fs");
const tusMeta = require('tus-metadata');
const assert = require('assert');
const AES = require("crypto-js/aes");
const MD5 = require("crypto-js/md5");
const debug = require('debug')('psitransfer:main');
const archiver = require('archiver');
const zlib = require('zlib');
const _ = require('lodash');
const axios = require('axios');

const errorPage = fs.readFileSync(path.join(__dirname, '../public/html/error.html')).toString();
const store = new Store(config.uploadDir);
const Db = require('./db');
const db = new Db(config.uploadDir, store);
db.init();
const app = express();

app.disable('x-powered-by');
app.use(compression());

if (config.accessLog) {
  app.use(morgan(config.accessLog));
}

if(config.forceHttps) {
  app.enable('trust proxy');
  app.use(function(req, res, next) {
    if (req.secure) return next();
    const target = config.forceHttps === 'true' ? 'https://' + req.headers.host : config.forceHttps;
    res.redirect(target + req.url);
  });
}

// Static files
app.use('/app', express.static(path.join(__dirname, '../public/app')));
app.use('/assets', express.static(path.join(__dirname, '../public/assets')));

// robots.txt
app.get('/robots.txt', (req, res) => {
  res.sendFile(path.join(__dirname, '../public/robots.txt'));
});

// Upload App
app.get('/', (req, res) => {
  if (config.uploadAppPath !== '/') {
    res.status(304).redirect(config.uploadAppPath);
  } else {
    res.sendFile(path.join(__dirname, '../public/html/upload.html'));
  }
});

app.get(config.uploadAppPath, (req, res) => {
  res.sendFile(path.join(__dirname, '../public/html/upload.html'));
});

// Config
app.get('/config.json', (req, res) => {
  res.json({
    retentions: config.retentions,
    defaultRetention: config.defaultRetention,
    mailTemplate: config.mailTemplate
  });
});


app.get('/admin', (req, res, next) => {
  if (!config.adminPass) return next();
  res.sendFile(path.join(__dirname, '../public/html/admin.html'));
});
app.get('/admin/data.json', (req, res, next) => {
  if (!config.adminPass) return next();

  const bfTimeout = 500;

  if (!req.get('x-passwd')) {
    // delay answer to make brute force attacks more difficult
    setTimeout(() => res.status(401).send('Unauthorized'), bfTimeout);
    return;
  }
  if (req.get('x-passwd') !== config.adminPass) {
    setTimeout(() => res.status(403).send('Forbidden'), bfTimeout);
    return;
  }

  const result = _.chain(db.db)
    .cloneDeep()
    .forEach(bucket => {
      bucket.forEach(file => {
        if (file.metadata.password) {
          file.metadata._password = true;
          delete file.metadata.password;
          delete file.metadata.key;
          delete file.key;
          delete file.url;
        }
      })
    })
    .value();

  setTimeout(() => res.json(result), bfTimeout);
});


// List files / Download App
app.get('/:sid', (req, res, next) => {
  if (req.url.endsWith('.json')) {
    const sid = req.params.sid.substr(0, req.params.sid.length - 5);
    if (!db.get(sid)) return res.status(404).end();

    res.header('Cache-control', 'private, max-age=0, no-cache, no-store, must-revalidate');
    res.json({
      items: db.get(sid).map(data => {
        const item = Object.assign(data, { url: `/files/${ sid }++${ data.key }` });
        if (item.metadata.password) {
          return AES.encrypt(JSON.stringify(data), item.metadata.password).toString();
        } else {
          return item;
        }
      }),
      config: {
        maxPreviewSize: config.maxPreviewSize
      }
    });
  } else {
    if (!db.get(req.params.sid)) return next();
    res.sendFile(path.join(__dirname, '../public/html/download.html'));
  }
});


// Download files
app.get('/files/:fid', async (req, res, next) => {
  // let tusboy handle HEAD requests with Tus Header
  if (req.method === 'HEAD' && req.get('Tus-Resumable')) return next();

  const sid = req.params.fid.split('++')[0];

  // Download all files
  if (req.params.fid.match(/^[a-z0-9+]+\.(tar\.gz|zip)$/)) {
    const format = req.params.fid.endsWith('.zip') ? 'zip' : 'tar.gz';
    const bucket = db.get(sid);

    if (!bucket) return res.status(404).send(errorPage.replace('%%ERROR%%', 'Download bucket not found.'));

    if (req.params.fid !== sid + '++' + MD5(bucket.map(f => f.key).join()).toString() + '.' + format) {
      res.status(404).send(errorPage.replace('%%ERROR%%', 'Invalid link'));
      return;
    }
    debug(`Download Bucket ${ sid }`);

    if (format === 'zip') res.header('ContentType', 'application/zip');
    if (format === 'tar.gz') res.header('ContentType', 'application/x-gtar');
    res.header('Content-Disposition', `attachment; filename="${ sid }.${ format }"`);

    const archive = archiver(format === 'zip' ? 'zip' : 'tar');
    archive.on('error', function(err) {
      console.error(err);
    });

    bucket.forEach(info => {
      archive.append(
        fs.createReadStream(store.getFilename(info.metadata.sid + '++' + info.key)),
        { name: info.metadata.name }
      );
    });

    if (format === 'tar.gz') {
      archive.pipe(zlib.createGzip()).pipe(res);
    } else {
      archive.pipe(res);
    }
    archive.finalize();

    try {
      res.on('finish', async () => {
        bucket.forEach(async info => {
          if (info.metadata.retention === 'one-time') {
            await db.remove(info.metadata.sid, info.metadata.key);
          } else {
            await db.updateLastDownload(info.metadata.sid, info.metadata.key);
          }
        });

        // Trigger fileDownloadedWebhook
        if(config.fileDownloadedWebhook) {
          axios.post(config.fileDownloadedWebhook, {
            fid: sid,
            file: '_archive_',
            date: Date.now()
          }).catch(err => console.error(err));
        }

      });
    }
    catch (e) {
      console.error(e);
    }

    return;
  }

  // Download single file
  debug(`Download ${ req.params.fid }`);
  try {
    const info = await store.info(req.params.fid); // throws on 404
    res.download(store.getFilename(req.params.fid), info.metadata.name);

    // remove one-time files after download
    res.on('finish', async () => {
      if (info.metadata.retention === 'one-time') {
        await db.remove(info.metadata.sid, info.metadata.key);
      } else {
        await db.updateLastDownload(info.metadata.sid, info.metadata.key);
      }

      // Trigger fileDownloadedWebhook
      if (config.fileDownloadedWebhook) {
        axios.post(config.fileDownloadedWebhook, {
          bucket: sid,
          file: info.metadata.name,
          date: Date.now()
        }).catch(err => console.error(err));
      }
    });
  }
  catch (e) {
    res.status(404).send(errorPage.replace('%%ERROR%%', e.message));
  }
});


// Upload file
app.use('/files',
  function(req, res, next) {
    if (req.method === 'GET') return res.status(405).end();

    if (req.method === 'POST') {
      // validate meta-data
      // !! tusMeta.encode supports only strings !!
      const meta = tusMeta.decode(req.get('Upload-Metadata'));

      try {
        assert(meta.name, 'tus meta prop missing: name');
        assert(meta.sid, 'tus meta prop missing: sid');
        assert(meta.retention, 'tus meta prop missing: retention');
        assert(Object.keys(config.retentions).indexOf(meta.retention) >= 0,
          `invalid tus meta prop retention. Value ${ meta.retention } not in [${ Object.keys(config.retentions).join(',') }]`);

        meta.key = uuid();
        meta.createdAt = Date.now().toString();

        // store changed metadata for tusboy
        req.headers['upload-metadata'] = tusMeta.encode(meta);
        // for tusboy getKey()
        req.FID = meta.sid + '++' + meta.key;

        db.add(meta.sid, meta.key, {
          "isPartial": true,
          metadata: meta
        });
      }
      catch (e) {
        return res.status(400).end(e.message);
      }
    }

    next();
  },

  // let tusboy handle the upload
  tusboy(store, {
    getKey: req => req.FID,
    afterComplete: (req, upload, fid) => {
      db.add(upload.metadata.sid, upload.metadata.key, upload);
      debug(`Completed upload ${ fid }, size=${ upload.size } name=${ upload.metadata.name }`);

      // Trigger fileUploadedWebhook
      if (config.fileUploadedWebhook) {
        axios.post(config.fileUploadedWebhook, {
          metadata: upload.metadata,
          date: Date.now()
        }).catch(err => console.error(err));
      };
    },
  })
);

app.use((req, res, next) => {
  res.status(404).send(errorPage.replace('%%ERROR%%', 'Download bucket not found.'));
});

module.exports = app;
