/* ----------------------------------------------------------------------------

 Question Server
 GitLab: <https://gitlab.com/MrFry/mrfrys-node-server>

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program. If not, see <https://www.gnu.org/licenses/>.

 ------------------------------------------------------------------------- */

// package requires
const express = require('express')
const bodyParser = require('body-parser')
const busboy = require('connect-busboy')
const uuidv4 = require('uuid/v4') // TODO: deprecated, but imports are not supported
const fs = require('fs')
const app = express()

// other requires
const logger = require('../../utils/logger.js')
const utils = require('../../utils/utils.js')
const actions = require('../../utils/actions.js')
const dbtools = require('../../utils/dbtools.js')
const auth = require('../../middlewares/auth.middleware.js')

// files
const msgFile = 'stats/msgs'
const passwordFile = 'data/dataEditorPasswords.json'
const dataEditsLog = 'stats/dataEdits'
const dailyDataCountFile = 'stats/dailyDataCount'
const usersDbBackupPath = 'data/dbs/backup'

// other constants
const maxVeteranPwGetCount = 10
const addPWPerDay = 1 // every x day a user can give a pw
const maxPWCount = 6 // maximum pw give opportunities a user can have at once
// const daysAfterUserGetsPWs = 2 // days after user gets pw-s

// stuff gotten from server.js
let userDB
let url // eslint-disable-line
let publicdirs = []

function GetApp () {
  const p = publicdirs[0]
  if (!p) {
    throw new Error(`No public dir! ( API )`)
  }

  // files in public dirs
  const recivedFiles = p + 'recivedfiles'
  const uloadFiles = p + 'f'
  const dataFile = p + 'data.json'
  const motdFile = p + 'motd'
  const versionFile = p + 'version'

  app.use(bodyParser.urlencoded({
    limit: '10mb',
    extended: true
  }))
  app.use(bodyParser.json({
    limit: '10mb'
  }))
  app.set('view engine', 'ejs')
  app.set('views', [
    './modules/api/views',
    './sharedViews'
  ])
  app.use(auth({
    userDB: userDB,
    jsonResponse: true,
    exceptions: [
      '/favicon.ico',
      '/login',
      '/getveteranpw',
      '/postfeedbackfile',
      '/postfeedback',
      '/fosuploader',
      '/badtestsender'
    ]
  }))
  publicdirs.forEach((pdir) => {
    logger.Log(`Using public dir: ${pdir}`)
    app.use(express.static(pdir))
  })
  app.use(busboy({
    limits: {
      fileSize: 50000 * 1024 * 1024
    }
  }))

  var data = actions.LoadJSON(dataFile)
  var version = ''
  var motd = ''

  function LoadVersion () {
    version = utils.ReadFile(versionFile)
  }

  function LoadMOTD () {
    motd = utils.ReadFile(motdFile)
  }

  function Load () {
    utils.WatchFile(motdFile, (newData) => {
      logger.Log(`Motd changed: ${newData.replace(/\/n/g, '')}`)
      LoadMOTD()
    })
    utils.WatchFile(versionFile, (newData) => {
      logger.Log(`Version changed: ${newData.replace(/\/n/g, '')}`)
      LoadVersion()
    })

    LoadVersion()
    LoadMOTD()
  }

  Load()

  // -------------------------------------------------------------

  app.get('/avaiblePWS', (req, res) => {
    logger.LogReq(req)

    const user = req.session.user

    res.json({
      result: 'success',
      userCreated: user.created,
      avaiblePWS: user.avaiblePWRequests,
      requestedPWS: user.pwRequestCount,
      maxPWCount: maxPWCount,
      // daysAfterUserGetsPWs: daysAfterUserGetsPWs,
      addPWPerDay: addPWPerDay
    })
  })

  app.post('/getpw', function (req, res) {
    logger.LogReq(req)

    const requestingUser = req.session.user

    if (requestingUser.avaiblePWRequests <= 0) {
      res.json({
        result: 'error',
        msg: 'Too many passwords requested or cant request password yet, try later'
      })
      logger.Log(`User #${requestingUser.id} requested too much passwords`, logger.GetColor('cyan'))
      return
    }

    dbtools.Update(userDB, 'users', {
      avaiblePWRequests: requestingUser.avaiblePWRequests - 1,
      pwRequestCount: requestingUser.pwRequestCount + 1
    }, {
      id: requestingUser.id
    })

    const pw = uuidv4()
    const insertRes = dbtools.Insert(userDB, 'users', {
      pw: pw,
      avaiblePWRequests: 0,
      created: utils.GetDateString()
    })

    logger.Log(`User #${requestingUser.id} created new user #${insertRes.lastInsertRowid}`, logger.GetColor('cyan'))

    res.json({
      result: 'success',
      pw: pw,
      requestedPWS: requestingUser.pwRequestCount + 1,
      remaining: requestingUser.avaiblePWRequests - 1
    })
  })

  app.post('/getveteranpw', function (req, res) {
    logger.LogReq(req)
    const ip = req.headers['cf-connecting-ip'] || req.connection.remoteAddress
    const tries = dbtools.Select(userDB, 'veteranPWRequests', {
      ip: ip
    })[0]

    if (tries) {
      if (tries.count > maxVeteranPwGetCount) {
        res.json({
          result: 'error',
          msg: 'Too many tries from this IP'
        })
        logger.Log(`Too many veteran PW requests from ${ip}!`, logger.GetColor('cyan'))
        return
      } else {
        dbtools.Update(userDB, 'veteranPWRequests', {
          count: tries.count + 1,
          lastDate: utils.GetDateString()
        }, {
          id: tries.id
        })
      }
    } else {
      dbtools.Insert(userDB, 'veteranPWRequests', {
        ip: ip,
        lastDate: utils.GetDateString()
      })
    }

    const oldUserID = req.body.cid

    if (!oldUserID) {
      res.json({
        result: 'error',
        msg: 'No Client ID recieved'
      })
      logger.Log(`No client ID recieved`, logger.GetColor('cyan'))
      return
    }

    const user = dbtools.Select(userDB, 'users', {
      oldCID: oldUserID
    })[0]

    if (user) {
      if (user.pwGotFromCID === 0) {
        logger.Log(`Sent password to veteran user #${user.id}`, logger.GetColor('cyan'))
        dbtools.Update(userDB, 'users', {
          pwGotFromCID: 1
        }, {
          id: user.id
        })

        res.json({
          result: 'success',
          pw: user.pw
        })
      } else {
        logger.Log(`Veteran user #${user.id} already requested password`, logger.GetColor('cyan'))
        res.json({
          result: 'error',
          msg: 'Password already requested'
        })
      }
    } else {
      logger.Log(`Invalid password request with CID: ${oldUserID}`, logger.GetColor('cyan'))
      res.json({
        result: 'error',
        msg: 'No such Client ID'
      })
    }
  })

  app.post('/login', (req, res) => {
    logger.LogReq(req)
    const pw = req.body.pw || false
    const cid = req.body.cid
    const isScript = req.body.script
    const ip = req.headers['cf-connecting-ip'] || req.connection.remoteAddress
    const user = dbtools.Select(userDB, 'users', {
      pw: pw
    })[0]

    if (user) {
      const sessionID = uuidv4()

      // FIXME: Users now can only log in in one session, this might be too strict.
      const existingSessions = dbtools.Select(userDB, 'sessions', {
        userID: user.id,
        isScript: isScript ? 1 : 0
      })

      if (existingSessions.length > 0) {
        logger.Log(`Multiple ${isScript ? 'script' : 'website'} sessions ( ${existingSessions.length} ) for #${user.id}, deleting olds`, logger.GetColor('cyan'))
        existingSessions.forEach((sess) => {
          dbtools.Delete(userDB, 'sessions', {
            id: sess.id,
            isScript: isScript ? 1 : 0
          })
        })
      }

      dbtools.Update(userDB, 'users', {
        loginCount: user.loginCount + 1,
        lastIP: ip,
        lastLogin: utils.GetDateString()
      }, {
        id: user.id
      })

      dbtools.Insert(userDB, 'sessions', {
        id: sessionID,
        ip: ip,
        userID: user.id,
        isScript: isScript ? 1 : 0,
        createDate: utils.GetDateString()
      })

      // https://www.npmjs.com/package/cookie
      // TODO: cookie age
      res.cookie('sessionID', sessionID, {
        domain: '.frylabs.net', // TODO: use url. url: "https://api.frylabs.net"
        sameSite: 'none'
      })
      res.cookie('sessionID', sessionID, {
        sameSite: 'none'
      })

      res.json({
        result: 'success',
        msg: 'you are now logged in'
      })
      logger.Log(`Successfull login to ${isScript ? 'script' : 'website'} with user ID: #${user.id}`, logger.GetColor('cyan'))
    } else {
      logger.Log(`Login attempt with invalid pw: ${pw} to ${isScript ? 'script' : 'website'}${cid ? ', CID:' + cid : ''}`, logger.GetColor('cyan'))
      res.json({
        result: 'error',
        msg: 'Invalid password'
      })
    }
  })

  app.post('/logout', (req, res) => {
    logger.LogReq(req)
    const sessionID = req.cookies.sessionID

    // removing session from db
    dbtools.Delete(userDB, 'sessions', {
      id: sessionID
    })
    // TODO: remove old sessions every once in a while
    res.clearCookie('sessionID').json({
      result: 'success'
    })
  })

  // --------------------------------------------------------------

  app.get('/', function (req, res) {
    logger.LogReq(req)
    res.redirect('https://www.youtube.com/watch?v=ieqGJgqiXFk')
  })

  app.post('/postfeedbackfile', function (req, res) {
    UploadFile(req, res, uloadFiles, (fn) => {
      res.json({ success: true })
    })

    logger.LogReq(req)
    logger.Log('New feedback file', logger.GetColor('bluebg'), true)
  })

  app.post('/postfeedback', function (req, res) {
    logger.LogReq(req)
    if (res.body.fromLogin) {
      logger.Log('New feedback message from Login page', logger.GetColor('bluebg'), true)
    } else {
      logger.Log('New feedback message from feedback page', logger.GetColor('bluebg'), true)
    }
    utils.AppendToFile(utils.GetDateString() + ':\n' + JSON.stringify(req.body), msgFile)
    res.json({ success: true })
  })

  function UploadFile (req, res, path, next) {
    try {
      var fstream
      req.pipe(req.busboy)
      req.busboy.on('file', function (fieldname, file, filename) {
        logger.Log('Uploading: ' + filename, logger.GetColor('blue'))

        utils.CreatePath(path, true)
        let d = new Date()
        let fn = d.getHours() + '' + d.getMinutes() + '' + d.getSeconds() + '_' + filename

        fstream = fs.createWriteStream(path + '/' + fn)
        file.pipe(fstream)
        fstream.on('close', function () {
          logger.Log('Upload Finished of ' + path + '/' + fn, logger.GetColor('blue'))
          next(fn)
        })
        fstream.on('error', function (err) {
          console.log(err)
          res.end('something bad happened :s')
        })
      })
    } catch (e) {
      logger.Log(`Unable to upload file!`, logger.GetColor('redbg'))
      console.log(e)
    }
  }

  app.route('/fosuploader').post(function (req, res, next) {
    UploadFile(req, res, uloadFiles, (fn) => {
      res.redirect('/f/' + fn)
    })
  })

  app.route('/badtestsender').post(function (req, res, next) {
    UploadFile(req, res, recivedFiles, (fn) => {
      res.redirect('back')
    })
    logger.LogReq(req)
  })

  app.get('/allqr.txt', function (req, res) {
    res.set('Content-Type', 'text/plain')
    res.send(data.toString())
    res.end()
    logger.LogReq(req)
  })

  // -------------------------------------------------------------------------------------------
  // API

  app.post('/uploaddata', (req, res) => {
  // body: JSON.stringify({
  //   newData: data,
  //   count: getCount(data),
  //   initialCount: initialCount,
  //   password: password,
  //   editedQuestions: editedQuestions
  // })

    const { count, initialCount, editedQuestions, password, newData } = req.body
    const respStatuses = {
      invalidPass: 'invalidPass',
      ok: 'ok',
      error: 'error'
    }

    logger.LogReq(req)

    try {
    // finding user
      const pwds = JSON.parse(utils.ReadFile(passwordFile))
      let user = Object.keys(pwds).find((key) => {
        const u = pwds[key]
        return u.password === password
      })
      user = pwds[user]

      // logging and stuff
      logger.Log(`Data upload`, logger.GetColor('bluebg'))
      logger.Log(`PWD: ${password}`, logger.GetColor('bluebg'))
      // returning if user password is not ok
      if (!user) {
        logger.Log(`Data upload: invalid password ${password}`, logger.GetColor('red'))
        utils.AppendToFile(utils.GetDateString() + '\n' + password + '(FAILED PASSWORD)\n' + JSON.stringify(editedQuestions) + '\n\n', dataEditsLog)
        res.json({ status: respStatuses.invalidPass })
        return
      }

      logger.Log(`Password accepted for ${user.name}`, logger.GetColor('bluebg'))
      logger.Log(`Old Subjects/Questions: ${initialCount.subjectCount} / ${initialCount.questionCount} | New: ${count.subjectCount} / ${count.questionCount} | Edited question count: ${Object.keys(editedQuestions).length}`, logger.GetColor('bluebg'))
      // saving detailed editedCount
      utils.AppendToFile(utils.GetDateString() + '\n' + JSON.stringify(user) + '\n' + JSON.stringify(editedQuestions) + '\n\n', dataEditsLog)

      // making backup
      utils.CopyFile('./' + dataFile, `./public/backs/data_before_${user.name}_${utils.GetDateString().replace(/ /g, '_')}`)
      logger.Log('Backup made')
      // writing data
      utils.WriteFile(JSON.stringify(newData), dataFile)
      logger.Log('New data file written')
      // reloading data file
      data = actions.LoadJSON(dataFile)
      // data = newData
      logger.Log('Data set to newData')

      res.json({
        status: respStatuses.ok,
        user: user.name
      })
      logger.Log('Data updating done!', logger.GetColor('bluebg'))
    } catch (e) {
      logger.Log(`Data upload error! `, logger.GetColor('redbg'))
      console.error(e)
      res.json({ status: respStatuses.error, msg: e.message })
    }
  })

  app.post('/isAdding', function (req, res) {
    logger.LogReq(req)

    // automatically saves to dataFile every n write
    // FIXME: req.body.datatoadd is for backwards compatibility, remove this sometime in the future
    let result = actions.ProcessIncomingRequest(
      req.body.datatoadd || req.body,
      data,
      { motd, version }
    )

    res.json({
      success: result !== -1,
      newQuestions: result
    })
  })

  app.get('/ask', function (req, res) {
    if (Object.keys(req.query).length === 0) {
      logger.DebugLog(`No query params`, 'ask', 1)
      res.json({
        message: `ask something! ?q=[question]&subj=[subject]&data=[question data]. 'subj' is optimal for faster result`,
        result: [],
        recievedData: JSON.stringify(req.query),
        success: false
      })
    } else {
      if (req.query.q && req.query.data) {
        let subj = req.query.subj || ''
        let question = req.query.q
        let recData = {}
        try {
          recData = JSON.parse(req.query.data)
        } catch (e) {
          logger.Log(`Unable to parse recieved question data! '${req.query.data}'`, logger.GetColor('redbg'))
        }
        let r = data.Search(question, subj, recData)

        res.json({
          result: r,
          success: true
        })
        logger.DebugLog(`Question result length: ${r.length}`, 'ask', 1)
        logger.DebugLog(r, 'ask', 2)
      } else {
        logger.DebugLog(`Invalid question`, 'ask', 1)
        res.json({
          message: `Invalid question :(`,
          result: [],
          recievedData: JSON.stringify(req.query),
          success: false
        })
      }
    }
  })

  function getSimplreRes () {
    return {
      subjects: data.length,
      questions: data.Subjects.reduce((acc, subj) => {
        return acc + subj.length
      }, 0)
    }
  }
  function getDetailedRes () {
    return data.Subjects.map((subj) => {
      return {
        name: subj.Name,
        count: subj.length
      }
    })
  }

  app.get('/datacount', function (req, res) {
    logger.LogReq(req)
    if (req.query.detailed === 'all') {
      res.json({
        detailed: getDetailedRes(),
        simple: getSimplreRes()
      })
    } else if (req.query.detailed) {
      res.json(getDetailedRes())
    } else {
      res.json(getSimplreRes())
    }
  })

  app.get('/infos', function (req, res) {
    const user = req.session.user

    let result = {
      result: 'success',
      uid: user.id
    }
    if (req.query.subjinfo) {
      result.subjinfo = getSimplreRes()
    }
    if (req.query.version) {
      result.version = version
    }
    if (req.query.motd) {
      result.motd = motd
    }
    res.json(result)
  })

  // -------------------------------------------------------------------------------------------

  app.get('*', function (req, res) {
    res.status(404).render('404')
  })

  app.post('*', function (req, res) {
    res.status(404).render('404')
  })

  function ExportDailyDataCount () {
    logger.Log('Saving daily data count ...')
    utils.AppendToFile(JSON.stringify({
      date: utils.GetDateString(),
      subjectCount: data.Subjects.length,
      questionCOunt: data.Subjects.reduce((acc, subj) => {
        return acc + subj.Questions.length
      }, 0),
      userCount: dbtools.TableInfo(userDB, 'users').dataCount
    }), dailyDataCountFile)
  }

  function BackupDB () {
    logger.Log('Backing up auth DB ...')
    utils.CreatePath(usersDbBackupPath, true)
    userDB.backup(`${usersDbBackupPath}/users.${utils.GetDateString().replace(/ /g, '_')}.db`)
      .then(() => {
        logger.Log('Auth DB backup complete!')
      })
      .catch((err) => {
        logger.Log('Auth DB backup failed!', logger.GetColor('redbg'))
        console.error(err)
      })
  }

  function IncrementAvaiblePWs () {
    // FIXME: check this if this is legit and works
    logger.Log('Incrementing avaible PW-s ...')
    const users = dbtools.SelectAll(userDB, 'users')
    const today = new Date()
    const getDayDiff = (dateString) => {
      let msdiff = today - new Date(dateString)
      return Math.floor(msdiff / (1000 * 3600 * 24))
    }

    users.forEach((u) => {
      if (u.avaiblePWRequests >= maxPWCount) {
        return
      }

      const dayDiff = getDayDiff(u.created)
      // if (dayDiff < daysAfterUserGetsPWs) {
      //   logger.Log(`User #${u.id} is not registered long enough to get password ( ${dayDiff} days, ${daysAfterUserGetsPWs} needed)`, logger.GetColor('cyan'))
      //   return
      // }

      if (dayDiff % addPWPerDay === 0) {
        logger.Log(`Incrementing avaible PW-s for user #${u.id}: ${u.avaiblePWRequests} -> ${u.avaiblePWRequests + 1}`, logger.GetColor('cyan'))
        dbtools.Update(userDB, 'users', {
          avaiblePWRequests: u.avaiblePWRequests + 1
        }, {
          id: u.id
        })
      }
    })
  }

  function DailyAction () {
    ExportDailyDataCount()
    BackupDB()
    IncrementAvaiblePWs()
  }

  return {
    dailyAction: DailyAction,
    app: app
  }
}

exports.name = 'API'
exports.getApp = GetApp
exports.setup = (data) => {
  userDB = data.userDB
  url = data.url
  publicdirs = data.publicdirs
}