const Path = require('path')
const express = require('express')
const http = require('http')
const fs = require('./libs/fsExtra')
const fileUpload = require('./libs/expressFileupload')
const rateLimit = require('./libs/expressRateLimit')

const { version } = require('../package.json')

// Utils
const filePerms = require('./utils/filePerms')
const fileUtils = require('./utils/fileUtils')
const Logger = require('./Logger')

const Auth = require('./Auth')
const Watcher = require('./Watcher')
const Scanner = require('./scanner/Scanner')
const Database = require('./Database')
const SocketAuthority = require('./SocketAuthority')

const routes = require('./routes/index')

const ApiRouter = require('./routers/ApiRouter')
const HlsRouter = require('./routers/HlsRouter')

const NotificationManager = require('./managers/NotificationManager')
const EmailManager = require('./managers/EmailManager')
const CoverManager = require('./managers/CoverManager')
const AbMergeManager = require('./managers/AbMergeManager')
const CacheManager = require('./managers/CacheManager')
const LogManager = require('./managers/LogManager')
const BackupManager = require('./managers/BackupManager')
const PlaybackSessionManager = require('./managers/PlaybackSessionManager')
const PodcastManager = require('./managers/PodcastManager')
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
const RssFeedManager = require('./managers/RssFeedManager')
const CronManager = require('./managers/CronManager')
const TaskManager = require('./managers/TaskManager')

class Server {
  constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
    this.Port = PORT
    this.Host = HOST
    global.Source = SOURCE
    global.isWin = process.platform === 'win32'
    global.Uid = isNaN(UID) ? undefined : Number(UID)
    global.Gid = isNaN(GID) ? undefined : Number(GID)
    global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH))
    global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
    global.RouterBasePath = ROUTER_BASE_PATH
    global.XAccel = process.env.USE_X_ACCEL

    if (!fs.pathExistsSync(global.ConfigPath)) {
      fs.mkdirSync(global.ConfigPath)
      filePerms.setDefaultDirSync(global.ConfigPath, false)
    }
    if (!fs.pathExistsSync(global.MetadataPath)) {
      fs.mkdirSync(global.MetadataPath)
      filePerms.setDefaultDirSync(global.MetadataPath, false)
    }

    this.watcher = new Watcher()
    this.auth = new Auth()

    // Managers
    this.taskManager = new TaskManager()
    this.notificationManager = new NotificationManager()
    this.emailManager = new EmailManager()
    this.backupManager = new BackupManager()
    this.logManager = new LogManager()
    this.cacheManager = new CacheManager()
    this.abMergeManager = new AbMergeManager(this.taskManager)
    this.playbackSessionManager = new PlaybackSessionManager()
    this.coverManager = new CoverManager(this.cacheManager)
    this.podcastManager = new PodcastManager(this.watcher, this.notificationManager, this.taskManager)
    this.audioMetadataManager = new AudioMetadataMangaer(this.taskManager)
    this.rssFeedManager = new RssFeedManager()

    this.scanner = new Scanner(this.coverManager, this.taskManager)
    this.cronManager = new CronManager(this.scanner, this.podcastManager)

    // Routers
    this.apiRouter = new ApiRouter(this)
    this.hlsRouter = new HlsRouter(this.auth, this.playbackSessionManager)

    Logger.logManager = this.logManager

    this.server = null
    this.io = null
  }

  authMiddleware(req, res, next) {
    this.auth.authMiddleware(req, res, next)
  }

  async init() {
    Logger.info('[Server] Init v' + version)
    await this.playbackSessionManager.removeOrphanStreams()

    await Database.init(false)

    // Create token secret if does not exist (Added v2.1.0)
    if (!Database.serverSettings.tokenSecret) {
      await this.auth.initTokenSecret()
    }

    await this.cleanUserData() // Remove invalid user item progress
    await this.purgeMetadata() // Remove metadata folders without library item
    await this.cacheManager.ensureCachePaths()

    await this.backupManager.init()
    await this.logManager.init()
    await this.apiRouter.checkRemoveEmptySeries(Database.series) // Remove empty series
    await this.rssFeedManager.init()
    this.cronManager.init()

    if (Database.serverSettings.scannerDisableWatcher) {
      Logger.info(`[Server] Watcher is disabled`)
      this.watcher.disabled = true
    } else {
      this.watcher.initWatcher(Database.libraries)
      this.watcher.on('files', this.filesChanged.bind(this))
    }
  }

  async start() {
    Logger.info('=== Starting Server ===')
    await this.init()

    const app = express()
    const router = express.Router()
    app.use(global.RouterBasePath, router)
    app.disable('x-powered-by')

    this.server = http.createServer(app)

    router.use(this.auth.cors)
    router.use(fileUpload({
      defCharset: 'utf8',
      defParamCharset: 'utf8',
      useTempFiles: true,
      tempFileDir: Path.join(global.MetadataPath, 'tmp')
    }))
    router.use(express.urlencoded({ extended: true, limit: "5mb" }));
    router.use(express.json({ limit: "5mb" }))

    // Static path to generated nuxt
    const distPath = Path.join(global.appRoot, '/client/dist')
    router.use(express.static(distPath))

    // Static folder
    router.use(express.static(Path.join(global.appRoot, 'static')))

    // router.use('/api/v1', routes) // TODO: New routes
    router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router)
    router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)

    // RSS Feed temp route
    router.get('/feed/:slug', (req, res) => {
      Logger.info(`[Server] Requesting rss feed ${req.params.slug}`)
      this.rssFeedManager.getFeed(req, res)
    })
    router.get('/feed/:slug/cover', (req, res) => {
      this.rssFeedManager.getFeedCover(req, res)
    })
    router.get('/feed/:slug/item/:episodeId/*', (req, res) => {
      Logger.debug(`[Server] Requesting rss feed episode ${req.params.slug}/${req.params.episodeId}`)
      this.rssFeedManager.getFeedItem(req, res)
    })

    // Client dynamic routes
    const dyanimicRoutes = [
      '/item/:id',
      '/author/:id',
      '/audiobook/:id/chapters',
      '/audiobook/:id/edit',
      '/audiobook/:id/manage',
      '/library/:library',
      '/library/:library/search',
      '/library/:library/bookshelf/:id?',
      '/library/:library/authors',
      '/library/:library/series/:id?',
      '/library/:library/podcast/search',
      '/library/:library/podcast/latest',
      '/config/users/:id',
      '/config/users/:id/sessions',
      '/config/item-metadata-utils/:id',
      '/collection/:id',
      '/playlist/:id'
    ]
    dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))

    router.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res))
    router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
    router.post('/init', (req, res) => {
      if (Database.hasRootUser) {
        Logger.error(`[Server] attempt to init server when server already has a root user`)
        return res.sendStatus(500)
      }
      this.initializeServer(req, res)
    })
    router.get('/status', (req, res) => {
      // status check for client to see if server has been initialized
      // server has been initialized if a root user exists
      const payload = {
        isInit: Database.hasRootUser,
        language: Database.serverSettings.language
      }
      if (!payload.isInit) {
        payload.ConfigPath = global.ConfigPath
        payload.MetadataPath = global.MetadataPath
      }
      res.json(payload)
    })
    router.get('/ping', (req, res) => {
      Logger.info('Received ping')
      res.json({ success: true })
    })
    app.get('/healthcheck', (req, res) => res.sendStatus(200))

    this.server.listen(this.Port, this.Host, () => {
      if (this.Host) Logger.info(`Listening on http://${this.Host}:${this.Port}`)
      else Logger.info(`Listening on port :${this.Port}`)
    })

    // Start listening for socket connections
    SocketAuthority.initialize(this)
  }

  async initializeServer(req, res) {
    Logger.info(`[Server] Initializing new server`)
    const newRoot = req.body.newRoot
    const rootUsername = newRoot.username || 'root'
    const rootPash = newRoot.password ? await this.auth.hashPass(newRoot.password) : ''
    if (!rootPash) Logger.warn(`[Server] Creating root user with no password`)
    await Database.createRootUser(rootUsername, rootPash, this.auth)

    res.sendStatus(200)
  }

  async filesChanged(fileUpdates) {
    Logger.info('[Server]', fileUpdates.length, 'Files Changed')
    await this.scanner.scanFilesChanged(fileUpdates)
  }

  // Remove unused /metadata/items/{id} folders
  async purgeMetadata() {
    const itemsMetadata = Path.join(global.MetadataPath, 'items')
    if (!(await fs.pathExists(itemsMetadata))) return
    const foldersInItemsMetadata = await fs.readdir(itemsMetadata)

    let purged = 0
    await Promise.all(foldersInItemsMetadata.map(async foldername => {
      const hasMatchingItem = Database.libraryItems.find(ab => ab.id === foldername)
      if (!hasMatchingItem) {
        const folderPath = Path.join(itemsMetadata, foldername)
        Logger.debug(`[Server] Purging unused metadata ${folderPath}`)

        await fs.remove(folderPath).then(() => {
          purged++
        }).catch((err) => {
          Logger.error(`[Server] Failed to delete folder path ${folderPath}`, err)
        })
      }
    }))
    if (purged > 0) {
      Logger.info(`[Server] Purged ${purged} unused library item metadata`)
    }
    return purged
  }

  // Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist
  async cleanUserData() {
    for (const _user of Database.users) {
      if (_user.mediaProgress.length) {
        for (const mediaProgress of _user.mediaProgress) {
          const libraryItem = Database.libraryItems.find(li => li.id === mediaProgress.libraryItemId)
          if (libraryItem && mediaProgress.episodeId) {
            const episode = libraryItem.media.checkHasEpisode?.(mediaProgress.episodeId)
            if (episode) continue
          } else {
            continue
          }

          Logger.debug(`[Server] Removing media progress ${mediaProgress.id} data from user ${_user.username}`)
          await Database.removeMediaProgress(mediaProgress.id)
        }
      }

      let hasUpdated = false
      if (_user.seriesHideFromContinueListening.length) {
        _user.seriesHideFromContinueListening = _user.seriesHideFromContinueListening.filter(seriesId => {
          if (!Database.series.some(se => se.id === seriesId)) { // Series removed
            hasUpdated = true
            return false
          }
          return true
        })
      }
      if (hasUpdated) {
        await Database.updateUser(_user)
      }
    }
  }

  // First time login rate limit is hit
  loginLimitReached(req, res, options) {
    Logger.error(`[Server] Login rate limit (${options.max}) was hit for ip ${req.ip}`)
    options.message = 'Too many attempts. Login temporarily locked.'
  }

  getLoginRateLimiter() {
    return rateLimit({
      windowMs: Database.serverSettings.rateLimitLoginWindow, // 5 minutes
      max: Database.serverSettings.rateLimitLoginRequests,
      skipSuccessfulRequests: true,
      onLimitReached: this.loginLimitReached
    })
  }

  logout(req, res) {
    if (req.body.socketId) {
      Logger.info(`[Server] User ${req.user ? req.user.username : 'Unknown'} is logging out with socket ${req.body.socketId}`)
      SocketAuthority.logout(req.body.socketId)
    }

    res.sendStatus(200)
  }

  async stop() {
    await this.watcher.close()
    Logger.info('Watcher Closed')

    return new Promise((resolve) => {
      this.server.close((err) => {
        if (err) {
          Logger.error('Failed to close server', err)
        } else {
          Logger.info('Server successfully closed')
        }
        resolve()
      })
    })
  }
}
module.exports = Server
