const express = require('express')
const Path = require('path')
const fs = require('fs-extra')

const Logger = require('./Logger')
const { isObject } = require('./utils/index')
const audioFileScanner = require('./utils/audioFileScanner')

const BookFinder = require('./BookFinder')

const Library = require('./objects/Library')
const User = require('./objects/User')

class ApiController {
  constructor(MetadataPath, db, scanner, auth, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, emitter, clientEmitter) {
    this.db = db
    this.scanner = scanner
    this.auth = auth
    this.streamManager = streamManager
    this.rssFeeds = rssFeeds
    this.downloadManager = downloadManager
    this.coverController = coverController
    this.backupManager = backupManager
    this.watcher = watcher
    this.emitter = emitter
    this.clientEmitter = clientEmitter
    this.MetadataPath = MetadataPath

    this.bookFinder = new BookFinder()

    this.router = express()
    this.init()
  }

  init() {
    this.router.get('/find/covers', this.findCovers.bind(this))
    this.router.get('/find/:method', this.find.bind(this))

    this.router.get('/libraries', this.getLibraries.bind(this))
    this.router.patch('/libraries/order', this.reorderLibraries.bind(this))
    this.router.get('/library/:id/search', this.searchLibrary.bind(this))
    this.router.get('/library/:id', this.getLibrary.bind(this))
    this.router.delete('/library/:id', this.deleteLibrary.bind(this))
    this.router.patch('/library/:id', this.updateLibrary.bind(this))
    this.router.get('/library/:id/audiobooks', this.getLibraryAudiobooks.bind(this))
    this.router.post('/library', this.createNewLibrary.bind(this))

    this.router.get('/audiobooks', this.getAudiobooks.bind(this)) // Old route should pass library id
    this.router.delete('/audiobooks', this.deleteAllAudiobooks.bind(this))
    this.router.post('/audiobooks/delete', this.batchDeleteAudiobooks.bind(this))
    this.router.post('/audiobooks/update', this.batchUpdateAudiobooks.bind(this))

    this.router.get('/audiobook/:id', this.getAudiobook.bind(this))
    this.router.delete('/audiobook/:id', this.deleteAudiobook.bind(this))
    this.router.patch('/audiobook/:id/tracks', this.updateAudiobookTracks.bind(this))
    this.router.post('/audiobook/:id/cover', this.uploadAudiobookCover.bind(this))
    this.router.patch('/audiobook/:id/coverfile', this.updateAudiobookCoverFromFile.bind(this))
    this.router.get('/audiobook/:id/match', this.matchAudiobookBook.bind(this))
    this.router.patch('/audiobook/:id', this.updateAudiobook.bind(this))

    this.router.patch('/match/:id', this.match.bind(this))

    // Old Route : Wait until refactor of mobile app to replace with path /reset-progress
    this.router.delete('/user/audiobook/:id', this.resetUserAudiobookProgress.bind(this))

    this.router.patch('/user/audiobook/:id/reset-progress', this.resetUserAudiobookProgress.bind(this))
    this.router.patch('/user/audiobook/:id', this.updateUserAudiobookData.bind(this))
    this.router.patch('/user/audiobooks', this.batchUpdateUserAudiobookData.bind(this))

    this.router.patch('/user/password', this.userChangePassword.bind(this))
    this.router.patch('/user/settings', this.userUpdateSettings.bind(this))
    this.router.get('/users', this.getUsers.bind(this))
    this.router.post('/user', this.createUser.bind(this))
    this.router.get('/user/:id', this.getUser.bind(this))
    this.router.patch('/user/:id', this.updateUser.bind(this))
    this.router.delete('/user/:id', this.deleteUser.bind(this))

    this.router.patch('/serverSettings', this.updateServerSettings.bind(this))

    this.router.delete('/backup/:id', this.deleteBackup.bind(this))
    this.router.post('/backup/upload', this.uploadBackup.bind(this))

    this.router.post('/authorize', this.authorize.bind(this))

    this.router.get('/genres', this.getGenres.bind(this))

    this.router.post('/feed', this.openRssFeed.bind(this))

    this.router.get('/download/:id', this.download.bind(this))

    this.router.get('/filesystem', this.getFileSystemPaths.bind(this))

    this.router.get('/scantracks/:id', this.scanAudioTrackNums.bind(this))
  }

  async find(req, res) {
    var provider = req.query.provider || 'google'
    var title = req.query.title || ''
    var author = req.query.author || ''
    var results = await this.bookFinder.search(provider, title, author)
    res.json(results)
  }

  findCovers(req, res) {
    this.scanner.findCovers(req, res)
  }

  authorize(req, res) {
    if (!req.user) {
      Logger.error('Invalid user in authorize')
      return res.sendStatus(401)
    }
    res.json({ user: req.user })
  }

  getLibraries(req, res) {
    var libraries = this.db.libraries.map(lib => lib.toJSON())
    res.json(libraries)
  }

  async reorderLibraries(req, res) {
    if (!req.user || !req.user.isRoot) {
      Logger.error('[ApiController] ReorderLibraries invalid user', req.user)
      return res.sendStatus(401)
    }

    var orderdata = req.body
    var hasUpdates = false
    for (let i = 0; i < orderdata.length; i++) {
      var library = this.db.libraries.find(lib => lib.id === orderdata[i].id)
      if (!library) {
        Logger.error(`[ApiController] Invalid library not found in reorder ${orderdata[i].id}`)
        return res.sendStatus(500)
      }
      if (library.update({ displayOrder: orderdata[i].newOrder })) {
        hasUpdates = true
        await this.db.updateEntity('library', library)
      }
    }

    if (hasUpdates) {
      Logger.info(`[ApiController] Updated library display orders`)
    } else {
      Logger.info(`[ApiController] Library orders were up to date`)
    }

    var libraries = this.db.libraries.map(lib => lib.toJSON())
    res.json(libraries)
  }

  searchLibrary(req, res) {
    var library = this.db.libraries.find(lib => lib.id === req.params.id)
    if (!library) {
      return res.status(404).send('Library not found')
    }
    if (!req.query.q) {
      return res.status(400).send('No query string')
    }
    var maxResults = req.query.max || 3

    var bookMatches = []
    var authorMatches = {}
    var seriesMatches = {}
    var tagMatches = {}

    var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
    audiobooksInLibrary.forEach((ab) => {
      var queryResult = ab.searchQuery(req.query.q)
      if (queryResult.book) {
        var bookMatchObj = {
          audiobook: ab,
          matchKey: queryResult.book,
          matchText: queryResult.bookMatchText
        }
        bookMatches.push(bookMatchObj)
      }
      if (queryResult.authors) {
        queryResult.authors.forEach((author) => {
          if (!authorMatches[author]) {
            authorMatches[author] = {
              author: author
            }
          }
        })
      }
      if (queryResult.series) {
        if (!seriesMatches[queryResult.series]) {
          seriesMatches[queryResult.series] = {
            series: queryResult.series,
            audiobooks: [ab]
          }
        } else {
          seriesMatches[queryResult.series].audiobooks.push(ab)
        }
      }
      if (queryResult.tags && queryResult.tags.length) {
        queryResult.tags.forEach((tag) => {
          if (!tagMatches[tag]) {
            tagMatches[tag] = {
              tag,
              audiobooks: [ab]
            }
          } else {
            tagMatches[tag].audiobooks.push(ab)
          }
        })
      }
    })

    res.json({
      audiobooks: bookMatches.slice(0, maxResults),
      tags: Object.values(tagMatches).slice(0, maxResults),
      authors: Object.values(authorMatches).slice(0, maxResults),
      series: Object.values(seriesMatches).slice(0, maxResults)
    })
  }

  getLibrary(req, res) {
    var library = this.db.libraries.find(lib => lib.id === req.params.id)
    if (!library) {
      return res.status(404).send('Library not found')
    }
    return res.json(library.toJSON())
  }

  async deleteLibrary(req, res) {
    var library = this.db.libraries.find(lib => lib.id === req.params.id)
    if (!library) {
      return res.status(404).send('Library not found')
    }

    // Remove library watcher
    this.watcher.removeLibrary(library)

    // Remove audiobooks in this library
    var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
    Logger.info(`[Server] deleting library "${library.name}" with ${audiobooks.length} audiobooks"`)
    for (let i = 0; i < audiobooks.length; i++) {
      await this.handleDeleteAudiobook(audiobooks[i])
    }

    var libraryJson = library.toJSON()
    await this.db.removeEntity('library', library.id)
    this.emitter('library_removed', libraryJson)
    return res.json(libraryJson)
  }

  async updateLibrary(req, res) {
    var library = this.db.libraries.find(lib => lib.id === req.params.id)
    if (!library) {
      return res.status(404).send('Library not found')
    }
    var hasUpdates = library.update(req.body)
    if (hasUpdates) {
      // Update watcher
      this.watcher.updateLibrary(library)

      // Remove audiobooks no longer in library
      var audiobooksToRemove = this.db.audiobooks.filter(ab => !library.checkFullPathInLibrary(ab.fullPath))
      if (audiobooksToRemove.length) {
        Logger.info(`[Scanner] Updating library, removing ${audiobooksToRemove.length} audiobooks`)
        for (let i = 0; i < audiobooksToRemove.length; i++) {
          await this.handleDeleteAudiobook(audiobooksToRemove[i])
        }
      }
      await this.db.updateEntity('library', library)
      this.emitter('library_updated', library.toJSON())
    }
    return res.json(library.toJSON())
  }

  getLibraryAudiobooks(req, res) {
    var libraryId = req.params.id
    var library = this.db.libraries.find(lib => lib.id === libraryId)
    if (!library) {
      return res.status(400).send('Library does not exist')
    }

    var audiobooks = []
    if (req.query.q) {
      audiobooks = this.db.audiobooks.filter(ab => {
        return ab.libraryId === libraryId && ab.isSearchMatch(req.query.q)
      }).map(ab => ab.toJSONMinified())
    } else {
      audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId).map(ab => ab.toJSONMinified())
    }
    res.json(audiobooks)
  }

  async createNewLibrary(req, res) {
    var newLibraryPayload = {
      ...req.body
    }
    if (!newLibraryPayload.name || !newLibraryPayload.folders || !newLibraryPayload.folders.length) {
      return res.status(500).send('Invalid request')
    }

    var library = new Library()
    newLibraryPayload.displayOrder = this.db.libraries.length + 1
    library.setData(newLibraryPayload)
    await this.db.insertEntity('library', library)
    this.emitter('library_added', library.toJSON())

    // Add library watcher
    this.watcher.addLibrary(library)

    res.json(library)
  }

  getAudiobooks(req, res) {
    var audiobooks = []
    if (req.query.q) {
      audiobooks = this.db.audiobooks.filter(ab => {
        return ab.isSearchMatch(req.query.q)
      }).map(ab => ab.toJSONMinified())
    } else {
      audiobooks = this.db.audiobooks.map(ab => ab.toJSONMinified())
    }
    res.json(audiobooks)
  }

  async deleteAllAudiobooks(req, res) {
    if (!req.user.isRoot) {
      Logger.warn('User other than root attempted to delete all audiobooks', req.user)
      return res.sendStatus(403)
    }
    Logger.info('Removing all Audiobooks')
    var success = await this.db.recreateAudiobookDb()
    if (success) res.sendStatus(200)
    else res.sendStatus(500)
  }

  getAudiobook(req, res) {
    if (!req.user) {
      return res.sendStatus(403)
    }
    var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
    if (!audiobook) return res.sendStatus(404)

    // Check user can access this audiobooks library
    if (!req.user.checkCanAccessLibrary(audiobook.libraryId)) {
      return res.sendStatus(403)
    }

    res.json(audiobook.toJSONExpanded())
  }

  async handleDeleteAudiobook(audiobook) {
    // Remove audiobook from users
    for (let i = 0; i < this.db.users.length; i++) {
      var user = this.db.users[i]
      var madeUpdates = user.deleteAudiobookData(audiobook.id)
      if (madeUpdates) {
        await this.db.updateEntity('user', user)
      }
    }

    // remove any streams open for this audiobook
    var streams = this.streamManager.streams.filter(stream => stream.audiobookId === audiobook.id)
    for (let i = 0; i < streams.length; i++) {
      var stream = streams[i]
      var client = stream.client
      await stream.close()
      if (client && client.user) {
        client.user.stream = null
        client.stream = null
        this.db.updateUserStream(client.user.id, null)
      }
    }

    var audiobookJSON = audiobook.toJSONMinified()
    await this.db.removeEntity('audiobook', audiobook.id)
    this.emitter('audiobook_removed', audiobookJSON)
  }

  async deleteAudiobook(req, res) {
    if (!req.user.canDelete) {
      Logger.warn('User attempted to delete without permission', req.user)
      return res.sendStatus(403)
    }
    var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
    if (!audiobook) return res.sendStatus(404)

    await this.handleDeleteAudiobook(audiobook)
    res.sendStatus(200)
  }

  async batchDeleteAudiobooks(req, res) {
    if (!req.user.canDelete) {
      Logger.warn('User attempted to delete without permission', req.user)
      return res.sendStatus(403)
    }
    var { audiobookIds } = req.body
    if (!audiobookIds || !audiobookIds.length) {
      return res.sendStatus(500)
    }

    var audiobooksToDelete = this.db.audiobooks.filter(ab => audiobookIds.includes(ab.id))
    if (!audiobooksToDelete.length) {
      return res.sendStatus(404)
    }
    for (let i = 0; i < audiobooksToDelete.length; i++) {
      Logger.info(`[ApiController] Deleting Audiobook "${audiobooksToDelete[i].title}"`)
      await this.handleDeleteAudiobook(audiobooksToDelete[i])
    }
    res.sendStatus(200)
  }

  async batchUpdateAudiobooks(req, res) {
    if (!req.user.canUpdate) {
      Logger.warn('User attempted to batch update without permission', req.user)
      return res.sendStatus(403)
    }
    var audiobooks = req.body
    if (!audiobooks || !audiobooks.length) {
      return res.sendStatus(500)
    }

    var audiobooksUpdated = 0
    audiobooks = audiobooks.map((ab) => {
      var _ab = this.db.audiobooks.find(__ab => __ab.id === ab.id)
      if (!_ab) return null
      var hasUpdated = _ab.update(ab)
      if (!hasUpdated) return null
      audiobooksUpdated++
      return _ab
    }).filter(ab => ab)

    if (audiobooksUpdated) {
      Logger.info(`[ApiController] ${audiobooksUpdated} Audiobooks have updates`)
      for (let i = 0; i < audiobooks.length; i++) {
        await this.db.updateAudiobook(audiobooks[i])
        this.emitter('audiobook_updated', audiobooks[i].toJSONMinified())
      }
    }

    res.json({
      success: true,
      updates: audiobooksUpdated
    })
  }

  async updateAudiobookTracks(req, res) {
    if (!req.user.canUpdate) {
      Logger.warn('User attempted to update audiotracks without permission', req.user)
      return res.sendStatus(403)
    }
    var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
    if (!audiobook) return res.sendStatus(404)
    var orderedFileData = req.body.orderedFileData
    Logger.info(`Updating audiobook tracks called ${audiobook.id}`)
    audiobook.updateAudioTracks(orderedFileData)
    await this.db.updateAudiobook(audiobook)
    this.emitter('audiobook_updated', audiobook.toJSONMinified())
    res.json(audiobook.toJSON())
  }

  async uploadAudiobookCover(req, res) {
    if (!req.user.canUpload || !req.user.canUpdate) {
      Logger.warn('User attempted to upload a cover without permission', req.user)
      return res.sendStatus(403)
    }

    var audiobookId = req.params.id
    var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId)
    if (!audiobook) {
      return res.status(404).send('Audiobook not found')
    }

    var result = null
    if (req.body && req.body.url) {
      Logger.debug(`[ApiController] Requesting download cover from url "${req.body.url}"`)
      result = await this.coverController.downloadCoverFromUrl(audiobook, req.body.url)
    } else if (req.files && req.files.cover) {
      Logger.debug(`[ApiController] Handling uploaded cover`)
      var coverFile = req.files.cover
      result = await this.coverController.uploadCover(audiobook, coverFile)
    } else {
      return res.status(400).send('Invalid request no file or url')
    }

    if (result && result.error) {
      return res.status(400).send(result.error)
    } else if (!result || !result.cover) {
      return res.status(500).send('Unknown error occurred')
    }

    await this.db.updateAudiobook(audiobook)
    this.emitter('audiobook_updated', audiobook.toJSONMinified())
    res.json({
      success: true,
      cover: result.cover
    })
  }

  async updateAudiobookCoverFromFile(req, res) {
    if (!req.user.canUpdate) {
      Logger.warn('User attempted to update without permission', req.user)
      return res.sendStatus(403)
    }
    var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
    if (!audiobook) return res.sendStatus(404)

    var coverFile = req.body
    var updated = await audiobook.setCoverFromFile(coverFile)

    if (updated) {
      await this.db.updateAudiobook(audiobook)
      this.emitter('audiobook_updated', audiobook.toJSONMinified())
    }

    if (updated) res.status(200).send('Cover updated successfully')
    else res.status(200).send('No update was made to cover')
  }

  async matchAudiobookBook(req, res) {
    var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
    if (!audiobook) return res.sendStatus(404)

    var provider = req.query.provider || 'google'
    var excludeAuthor = req.query.excludeAuthor === '1'
    var authorSearch = excludeAuthor ? null : audiobook.authorFL

    var results = await this.bookFinder.search(provider, audiobook.title, authorSearch)
    res.json(results)
  }

  async updateAudiobook(req, res) {
    if (!req.user.canUpdate) {
      Logger.warn('User attempted to update without permission', req.user)
      return res.sendStatus(403)
    }
    var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
    if (!audiobook) return res.sendStatus(404)
    var hasUpdates = audiobook.update(req.body)
    if (hasUpdates) {
      await this.db.updateAudiobook(audiobook)
    }
    this.emitter('audiobook_updated', audiobook.toJSONMinified())
    res.json(audiobook.toJSON())
  }

  async match(req, res) {
    var body = req.body
    var audiobookId = req.params.id
    var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId)
    var bookData = {
      olid: body.id,
      publish_year: body.first_publish_year,
      description: body.description,
      title: body.title,
      author: body.author,
      cover: body.cover
    }
    audiobook.setBook(bookData)
    await this.db.updateAudiobook(audiobook)

    this.emitter('audiobook_updated', audiobook.toJSONMinified())

    res.sendStatus(200)
  }

  async resetUserAudiobookProgress(req, res) {
    var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
    if (!audiobook) {
      return res.status(404).send('Audiobook not found')
    }
    req.user.resetAudiobookProgress(audiobook)
    await this.db.updateEntity('user', req.user)
    this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
    res.sendStatus(200)
  }

  async updateUserAudiobookData(req, res) {
    var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
    if (!audiobook) {
      return res.status(404).send('Audiobook not found')
    }
    var wasUpdated = req.user.updateAudiobookData(audiobook, req.body)
    if (wasUpdated) {
      await this.db.updateEntity('user', req.user)
      this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
    }
    res.sendStatus(200)
  }

  async batchUpdateUserAudiobookData(req, res) {
    var userAbDataPayloads = req.body
    if (!userAbDataPayloads || !userAbDataPayloads.length) {
      return res.sendStatus(500)
    }

    var shouldUpdate = false
    userAbDataPayloads.forEach((userAbData) => {
      var audiobook = this.db.audiobooks.find(ab => ab.id === userAbData.audiobookId)
      if (audiobook) {
        var wasUpdated = req.user.updateAudiobookData(audiobook, userAbData)
        if (wasUpdated) shouldUpdate = true
      }
    })

    if (shouldUpdate) {
      await this.db.updateEntity('user', req.user)
      this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
    }

    res.sendStatus(200)
  }

  userChangePassword(req, res) {
    this.auth.userChangePassword(req, res)
  }

  async openRssFeed(req, res) {
    var audiobookId = req.body.audiobookId
    var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId)
    if (!audiobook) return res.sendStatus(404)
    var feed = await this.rssFeeds.openFeed(audiobook)
    console.log('Feed open', feed)
    res.json(feed)
  }

  async userUpdateSettings(req, res) {
    var settingsUpdate = req.body
    if (!settingsUpdate || !isObject(settingsUpdate)) {
      return res.sendStatus(500)
    }
    var madeUpdates = req.user.updateSettings(settingsUpdate)
    if (madeUpdates) {
      await this.db.updateEntity('user', req.user)
    }
    return res.json({
      success: true,
      settings: req.user.settings
    })
  }

  userJsonWithBookProgressDetails(user) {
    var json = user.toJSONForBrowser()

    // User audiobook progress attach book details
    if (json.audiobooks && Object.keys(json.audiobooks).length) {
      for (const audiobookId in json.audiobooks) {
        var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId)
        if (!audiobook) {
          Logger.error('[ApiController] Audiobook not found for users progress ' + audiobookId)
        } else {
          json.audiobooks[audiobookId].book = audiobook.book.toJSON()
        }
      }
    }

    return json
  }

  getUsers(req, res) {
    if (req.user.type !== 'root') return res.sendStatus(403)
    var users = this.db.users.map(u => this.userJsonWithBookProgressDetails(u))
    res.json(users)
  }

  async createUser(req, res) {
    if (!req.user.isRoot) {
      Logger.warn('Non-root user attempted to create user', req.user)
      return res.sendStatus(403)
    }
    var account = req.body

    var username = account.username
    var usernameExists = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase())
    if (usernameExists) {
      return res.status(500).send('Username already taken')
    }

    account.id = (Math.trunc(Math.random() * 1000) + Date.now()).toString(36)
    account.pash = await this.auth.hashPass(account.password)
    delete account.password
    account.token = await this.auth.generateAccessToken({ userId: account.id })
    account.createdAt = Date.now()
    var newUser = new User(account)
    var success = await this.db.insertEntity('user', newUser)
    if (success) {
      this.clientEmitter(req.user.id, 'user_added', newUser)
      res.json({
        user: newUser.toJSONForBrowser()
      })
    } else {
      return res.status(500).send('Failed to save new user')
    }
  }

  async getUser(req, res) {
    if (!req.user.isRoot) {
      Logger.error('User other than root attempting to get user', req.user)
      return res.sendStatus(403)
    }

    var user = this.db.users.find(u => u.id === req.params.id)
    if (!user) {
      return res.sendStatus(404)
    }

    res.json(this.userJsonWithBookProgressDetails(user))
  }

  async updateUser(req, res) {
    if (!req.user.isRoot) {
      Logger.error('User other than root attempting to update user', req.user)
      return res.sendStatus(403)
    }

    var user = this.db.users.find(u => u.id === req.params.id)
    if (!user) {
      return res.sendStatus(404)
    }

    var account = req.body

    if (account.username !== undefined && account.username !== user.username) {
      var usernameExists = this.db.users.find(u => u.username.toLowerCase() === account.username.toLowerCase())
      if (usernameExists) {
        return res.status(500).send('Username already taken')
      }
    }

    // Updating password
    if (account.password) {
      account.pash = await this.auth.hashPass(account.password)
      delete account.password
    }

    var hasUpdated = user.update(account)
    if (hasUpdated) {
      await this.db.updateEntity('user', user)
    }

    this.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser())
    res.json({
      success: true,
      user: user.toJSONForBrowser()
    })
  }

  async deleteUser(req, res) {
    if (!req.user.isRoot) {
      Logger.error('User other than root attempting to delete user', req.user)
      return res.sendStatus(403)
    }
    if (req.params.id === 'root') {
      return res.sendStatus(500)
    }
    if (req.user.id === req.params.id) {
      Logger.error('Attempting to delete themselves...')
      return res.sendStatus(500)
    }
    var user = this.db.users.find(u => u.id === req.params.id)
    if (!user) {
      Logger.error('User not found')
      return res.json({
        error: 'User not found'
      })
    }

    // Todo: check if user is logged in and cancel streams

    var userJson = user.toJSONForBrowser()
    await this.db.removeEntity('user', user.id)
    this.clientEmitter(req.user.id, 'user_removed', userJson)
    res.json({
      success: true
    })
  }

  async updateServerSettings(req, res) {
    if (!req.user.isRoot) {
      Logger.error('User other than root attempting to update server settings', req.user)
      return res.sendStatus(403)
    }
    var settingsUpdate = req.body
    if (!settingsUpdate || !isObject(settingsUpdate)) {
      return res.status(500).send('Invalid settings update object')
    }

    var madeUpdates = this.db.serverSettings.update(settingsUpdate)
    if (madeUpdates) {
      // If backup schedule is updated - update backup manager
      if (settingsUpdate.backupSchedule !== undefined) {
        this.backupManager.updateCronSchedule()
      }

      await this.db.updateEntity('settings', this.db.serverSettings)
    }
    return res.json({
      success: true,
      serverSettings: this.db.serverSettings
    })
  }

  async deleteBackup(req, res) {
    if (!req.user.isRoot) {
      Logger.error(`[ApiController] Non-Root user attempting to delete backup`, req.user)
      return res.sendStatus(403)
    }
    var backup = this.backupManager.backups.find(b => b.id === req.params.id)
    if (!backup) {
      return res.sendStatus(404)
    }
    await this.backupManager.removeBackup(backup)
    res.json(this.backupManager.backups.map(b => b.toJSON()))
  }

  async uploadBackup(req, res) {
    if (!req.user.isRoot) {
      Logger.error(`[ApiController] Non-Root user attempting to upload backup`, req.user)
      return res.sendStatus(403)
    }
    if (!req.files.file) {
      Logger.error('[ApiController] Upload backup invalid')
      return res.sendStatus(500)
    }
    this.backupManager.uploadBackup(req, res)
  }

  async download(req, res) {
    if (!req.user.canDownload) {
      Logger.error('User attempting to download without permission', req.user)
      return res.sendStatus(403)
    }
    var downloadId = req.params.id
    Logger.info('Download Request', downloadId)
    var download = this.downloadManager.getDownload(downloadId)
    if (!download) {
      Logger.error('Download request not found', downloadId)
      return res.sendStatus(404)
    }

    var options = {
      headers: {
        'Content-Type': download.mimeType
      }
    }
    res.download(download.fullPath, download.filename, options, (err) => {
      if (err) {
        Logger.error('Download Error', err)
      }
    })
  }

  getGenres(req, res) {
    res.json({
      genres: this.db.getGenres()
    })
  }

  async getDirectories(dir, relpath, excludedDirs, level = 0) {
    try {
      var paths = await fs.readdir(dir)

      var dirs = await Promise.all(paths.map(async dirname => {
        var fullPath = Path.join(dir, dirname)
        var path = Path.join(relpath, dirname)

        var isDir = (await fs.lstat(fullPath)).isDirectory()
        if (isDir && !excludedDirs.includes(path) && dirname !== 'node_modules') {
          return {
            path,
            dirname,
            fullPath,
            level,
            dirs: level < 4 ? (await this.getDirectories(fullPath, path, excludedDirs, level + 1)) : []
          }
        } else {
          return false
        }
      }))
      dirs = dirs.filter(d => d)
      return dirs
    } catch (error) {
      Logger.error('Failed to readdir', dir, error)
      return []
    }
  }

  async getFileSystemPaths(req, res) {
    var excludedDirs = ['node_modules', 'client', 'server', '.git', 'static', 'build', 'dist', 'metadata', 'config', 'sys', 'proc'].map(dirname => {
      return Path.sep + dirname
    })

    // Do not include existing mapped library paths in response
    this.db.libraries.forEach(lib => {
      lib.folders.forEach((folder) => {
        var dir = folder.fullPath
        if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
        excludedDirs.push(dir)
      })
    })

    Logger.debug(`[Server] get file system paths, excluded: ${excludedDirs.join(', ')}`)
    var dirs = await this.getDirectories(global.appRoot, '/', excludedDirs)
    res.json(dirs)
  }

  async scanAudioTrackNums(req, res) {
    if (!req.user || !req.user.isRoot) {
      return res.sendStatus(403)
    }
    var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
    if (!audiobook) {
      return res.status(404).send('Audiobook not found')
    }

    var scandata = await audioFileScanner.scanTrackNumbers(audiobook)
    res.json(scandata)
  }
}
module.exports = ApiController