import express from 'express';
import _ from 'lodash';

import MeasurementModel from '../models/measurement';
import MeasurementTemplateModel from '../models/measurement_template';
import SampleModel from '../models/sample';
import MeasurementValidate from './validate/measurement';
import IdValidate from './validate/id';
import res400 from './validate/res400';
import ParametersValidate from './validate/parameters';
import db from '../db';
import globals from '../globals';
import mongoose from "mongoose";


const router = express.Router();

router.get('/measurement/' + IdValidate.parameter(), (req, res, next) => {
  if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return;

  MeasurementModel.findById(req.params.id).lean().exec((err, data: any) => {
    if (err) return next(err);
    if (!data) {
      return res.status(404).json({status: 'Not found'});
    }
    // deleted measurements only available for dev/admin
    if (data.status === globals.status.del && !req.auth(res, ['dev', 'admin'], 'all')) return;

    res.json(MeasurementValidate.output(data, req));
  });
});

router.put('/measurement/' + IdValidate.parameter(), async (req, res, next) => {
  if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;

  const {error, value: measurement} = MeasurementValidate.input(req.body, 'change');
  if (error) return res400(error, res);

  const data = await MeasurementModel.findById(req.params.id).lean().exec().catch(err => {next(err);}) as any;
  if (data instanceof Error) return;
  if (!data) {
    return res.status(404).json({status: 'Not found'});
  }
  if (data.status === 'deleted') {
    return res.status(403).json({status: 'Forbidden'});
  }

  // add properties needed for sampleIdCheck
  measurement.measurement_template = data.measurement_template;
  measurement.sample_id = data.sample_id;
  if (!await sampleIdCheck(measurement, req, res, next)) return;

  // check for changes
  if (measurement.values) {  // fill not changed values from database
    measurement.values = _.assign({}, data.values, measurement.values);
    if (!_.isEqual(measurement.values, data.values)) {
      measurement.status = globals.status.new;  // set status to new
    }
  }

  if (!await templateCheck(measurement, 'change', res, next)) return;
  await MeasurementModel.findByIdAndUpdate(req.params.id, measurement, {new: true})
    .log(req).lean().exec((err, data) => {
    if (err) return next(err);
    res.json(MeasurementValidate.output(data, req));
  });
});

router.delete('/measurement/' + IdValidate.parameter(), (req, res, next) => {
  if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;

  MeasurementModel.findById(req.params.id).lean().exec(async (err, data) => {
    if (err) return next(err);
    if (!data) {
      return res.status(404).json({status: 'Not found'});
    }
    if (!await sampleIdCheck(data, req, res, next)) return;
    await MeasurementModel.findByIdAndUpdate(req.params.id, {status: globals.status.del})
      .log(req).lean().exec(err => {
      if (err) return next(err);
      return res.json({status: 'OK'});
    });
  });
});

router.get('/measurement/sample/' + IdValidate.parameter(), (req, res, next) => {
  if (!req.auth(res, ['dev', 'admin'], 'basic')) return;

  MeasurementModel.find({sample_id: mongoose.Types.ObjectId(req.params.id)}).lean().exec((err, data: any) => {
    if (err) return next(err);
    if (!data.length) {
      return res.status(404).json({status: 'Not found'});
    }

    res.json(_.compact(data.map(e => MeasurementValidate.output(e, req, true))));
  });
});

router.put('/measurement/restore/' + IdValidate.parameter(), (req, res, next) => {
  if (!req.auth(res, ['dev', 'admin'], 'basic')) return;

  setStatus(globals.status.new, req, res, next);
});

router.put('/measurement/validate/' + IdValidate.parameter(), (req, res, next) => {
  if (!req.auth(res, ['dev', 'admin'], 'basic')) return;

  setStatus(globals.status.val, req, res, next);
});

router.post('/measurement/new', async (req, res, next) => {
  if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;

  const {error, value: measurement} = MeasurementValidate.input(req.body, 'new');
  if (error) return res400(error, res);

  if (!await sampleIdCheck(measurement, req, res, next)) return;
  measurement.values = await templateCheck(measurement, 'new', res, next);
  if (!measurement.values) return;

  measurement.status = globals.status.new;
  await new MeasurementModel(measurement).save((err, data) => {
    if (err) return next(err);
    db.log(req, 'measurements', {_id: data._id}, data.toObject());
    res.json(MeasurementValidate.output(data.toObject(), req));
  });
});


module.exports = router;


// validate sample_id, returns false if invalid or user has no access for this sample
async function sampleIdCheck (measurement, req, res, next) {
  const sampleData = await SampleModel.findById(measurement.sample_id)
    .lean().exec().catch(err => {next(err); return false;}) as any;
  if (!sampleData) {  // sample_id not found
    res.status(400).json({status: 'Sample id not available'});
    return false
  }
  // sample does not belong to user
  return !(sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic'));
}

// validate measurement_template and values, returns values, true if values are {} or false if invalid,
// param for 'new'/'change'
async function templateCheck (measurement, param, res, next) {
  const templateData = await MeasurementTemplateModel.findById(measurement.measurement_template)
    .lean().exec().catch(err => {next(err); return false;}) as any;
  if (!templateData) {  // template not found
    res.status(400).json({status: 'Measurement template not available'});
    return false
  }

  // fill not given values for new measurements
  if (param === 'new') {
    // get all template versions and check if given is latest
    const templateVersions = await MeasurementTemplateModel.find({first_id: templateData.first_id}).sort({version: -1})
      .lean().exec().catch(err => next(err)) as any;
    if (templateVersions instanceof Error) return false;
    if (measurement.measurement_template !== templateVersions[0]._id.toString()) {  // template not latest
      res.status(400).json({status: 'Old template version not allowed'});
      return false;
    }

    if (Object.keys(measurement.values).length === 0) {
      res.status(400).json({status: 'At least one value is required'});
      return false
    }
    const fillValues = {};  // initialize not given values with null
    templateData.parameters.forEach(parameter => {
      fillValues[parameter.name] = null;
    });
    measurement.values = _.assign({}, fillValues, measurement.values);
  }

  // validate values
  const {error, value} = ParametersValidate.input(measurement.values, templateData.parameters, 'null');
  if (error) {res400(error, res); return false;}
  return value || true;
}

function setStatus (status, req, res, next) {  // set measurement status
  MeasurementModel.findByIdAndUpdate(req.params.id, {status: status}).log(req).lean().exec((err, data) => {
    if (err) return next(err);

    if (!data) {
      return res.status(404).json({status: 'Not found'});
    }
    res.json({status: 'OK'});
  });
}