const { Request, Response, NextFunction } = require('express')
const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority')
const Database = require('../Database')

const Playlist = require('../objects/Playlist')

/**
 * @typedef RequestUserObject
 * @property {import('../models/User')} user
 *
 * @typedef {Request & RequestUserObject} RequestWithUser
 */

class PlaylistController {
  constructor() {}

  /**
   * POST: /api/playlists
   * Create playlist
   *
   * @param {RequestWithUser} req
   * @param {Response} res
   */
  async create(req, res) {
    const oldPlaylist = new Playlist()
    req.body.userId = req.user.id
    const success = oldPlaylist.setData(req.body)
    if (!success) {
      return res.status(400).send('Invalid playlist request data')
    }

    // Create Playlist record
    const newPlaylist = await Database.playlistModel.createFromOld(oldPlaylist)

    // Lookup all library items in playlist
    const libraryItemIds = oldPlaylist.items.map((i) => i.libraryItemId).filter((i) => i)
    const libraryItemsInPlaylist = await Database.libraryItemModel.findAll({
      where: {
        id: libraryItemIds
      }
    })

    // Create playlistMediaItem records
    const mediaItemsToAdd = []
    let order = 1
    for (const mediaItemObj of oldPlaylist.items) {
      const libraryItem = libraryItemsInPlaylist.find((li) => li.id === mediaItemObj.libraryItemId)
      if (!libraryItem) continue

      mediaItemsToAdd.push({
        mediaItemId: mediaItemObj.episodeId || libraryItem.mediaId,
        mediaItemType: mediaItemObj.episodeId ? 'podcastEpisode' : 'book',
        playlistId: oldPlaylist.id,
        order: order++
      })
    }
    if (mediaItemsToAdd.length) {
      await Database.createBulkPlaylistMediaItems(mediaItemsToAdd)
    }

    const jsonExpanded = await newPlaylist.getOldJsonExpanded()
    SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded)
    res.json(jsonExpanded)
  }

  /**
   * GET: /api/playlists
   * Get all playlists for user
   *
   * @param {RequestWithUser} req
   * @param {Response} res
   */
  async findAllForUser(req, res) {
    const playlistsForUser = await Database.playlistModel.findAll({
      where: {
        userId: req.user.id
      }
    })
    const playlists = []
    for (const playlist of playlistsForUser) {
      const jsonExpanded = await playlist.getOldJsonExpanded()
      playlists.push(jsonExpanded)
    }
    res.json({
      playlists
    })
  }

  /**
   * GET: /api/playlists/:id
   *
   * @param {RequestWithUser} req
   * @param {Response} res
   */
  async findOne(req, res) {
    const jsonExpanded = await req.playlist.getOldJsonExpanded()
    res.json(jsonExpanded)
  }

  /**
   * PATCH: /api/playlists/:id
   * Update playlist
   *
   * @param {RequestWithUser} req
   * @param {Response} res
   */
  async update(req, res) {
    const updatedPlaylist = req.playlist.set(req.body)
    let wasUpdated = false
    const changed = updatedPlaylist.changed()
    if (changed?.length) {
      await req.playlist.save()
      Logger.debug(`[PlaylistController] Updated playlist ${req.playlist.id} keys [${changed.join(',')}]`)
      wasUpdated = true
    }

    // If array of items is passed in then update order of playlist media items
    const libraryItemIds = req.body.items?.map((i) => i.libraryItemId).filter((i) => i) || []
    if (libraryItemIds.length) {
      const libraryItems = await Database.libraryItemModel.findAll({
        where: {
          id: libraryItemIds
        }
      })
      const existingPlaylistMediaItems = await updatedPlaylist.getPlaylistMediaItems({
        order: [['order', 'ASC']]
      })

      // Set an array of mediaItemId
      const newMediaItemIdOrder = []
      for (const item of req.body.items) {
        const libraryItem = libraryItems.find((li) => li.id === item.libraryItemId)
        if (!libraryItem) {
          continue
        }
        const mediaItemId = item.episodeId || libraryItem.mediaId
        newMediaItemIdOrder.push(mediaItemId)
      }

      // Sort existing playlist media items into new order
      existingPlaylistMediaItems.sort((a, b) => {
        const aIndex = newMediaItemIdOrder.findIndex((i) => i === a.mediaItemId)
        const bIndex = newMediaItemIdOrder.findIndex((i) => i === b.mediaItemId)
        return aIndex - bIndex
      })

      // Update order on playlistMediaItem records
      let order = 1
      for (const playlistMediaItem of existingPlaylistMediaItems) {
        if (playlistMediaItem.order !== order) {
          await playlistMediaItem.update({
            order
          })
          wasUpdated = true
        }
        order++
      }
    }

    const jsonExpanded = await updatedPlaylist.getOldJsonExpanded()
    if (wasUpdated) {
      SocketAuthority.clientEmitter(updatedPlaylist.userId, 'playlist_updated', jsonExpanded)
    }
    res.json(jsonExpanded)
  }

  /**
   * DELETE: /api/playlists/:id
   * Remove playlist
   *
   * @param {RequestWithUser} req
   * @param {Response} res
   */
  async delete(req, res) {
    const jsonExpanded = await req.playlist.getOldJsonExpanded()
    await req.playlist.destroy()
    SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_removed', jsonExpanded)
    res.sendStatus(200)
  }

  /**
   * POST: /api/playlists/:id/item
   * Add item to playlist
   *
   * @param {RequestWithUser} req
   * @param {Response} res
   */
  async addItem(req, res) {
    const oldPlaylist = await Database.playlistModel.getById(req.playlist.id)
    const itemToAdd = req.body

    if (!itemToAdd.libraryItemId) {
      return res.status(400).send('Request body has no libraryItemId')
    }

    const libraryItem = await Database.libraryItemModel.getOldById(itemToAdd.libraryItemId)
    if (!libraryItem) {
      return res.status(400).send('Library item not found')
    }
    if (libraryItem.libraryId !== oldPlaylist.libraryId) {
      return res.status(400).send('Library item in different library')
    }
    if (oldPlaylist.containsItem(itemToAdd)) {
      return res.status(400).send('Item already in playlist')
    }
    if ((itemToAdd.episodeId && !libraryItem.isPodcast) || (libraryItem.isPodcast && !itemToAdd.episodeId)) {
      return res.status(400).send('Invalid item to add for this library type')
    }
    if (itemToAdd.episodeId && !libraryItem.media.checkHasEpisode(itemToAdd.episodeId)) {
      return res.status(400).send('Episode not found in library item')
    }

    const playlistMediaItem = {
      playlistId: oldPlaylist.id,
      mediaItemId: itemToAdd.episodeId || libraryItem.media.id,
      mediaItemType: itemToAdd.episodeId ? 'podcastEpisode' : 'book',
      order: oldPlaylist.items.length + 1
    }

    await Database.createPlaylistMediaItem(playlistMediaItem)
    const jsonExpanded = await req.playlist.getOldJsonExpanded()
    SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_updated', jsonExpanded)
    res.json(jsonExpanded)
  }

  /**
   * DELETE: /api/playlists/:id/item/:libraryItemId/:episodeId?
   * Remove item from playlist
   *
   * @param {RequestWithUser} req
   * @param {Response} res
   */
  async removeItem(req, res) {
    const oldLibraryItem = await Database.libraryItemModel.getOldById(req.params.libraryItemId)
    if (!oldLibraryItem) {
      return res.status(404).send('Library item not found')
    }

    // Get playlist media items
    const mediaItemId = req.params.episodeId || oldLibraryItem.media.id
    const playlistMediaItems = await req.playlist.getPlaylistMediaItems({
      order: [['order', 'ASC']]
    })

    // Check if media item to delete is in playlist
    const mediaItemToRemove = playlistMediaItems.find((pmi) => pmi.mediaItemId === mediaItemId)
    if (!mediaItemToRemove) {
      return res.status(404).send('Media item not found in playlist')
    }

    // Remove record
    await mediaItemToRemove.destroy()

    // Update playlist media items order
    let order = 1
    for (const mediaItem of playlistMediaItems) {
      if (mediaItem.mediaItemId === mediaItemId) continue
      if (mediaItem.order !== order) {
        await mediaItem.update({
          order
        })
      }
      order++
    }

    const jsonExpanded = await req.playlist.getOldJsonExpanded()

    // Playlist is removed when there are no items
    if (!jsonExpanded.items.length) {
      Logger.info(`[PlaylistController] Playlist "${jsonExpanded.name}" has no more items - removing it`)
      await req.playlist.destroy()
      SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_removed', jsonExpanded)
    } else {
      SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_updated', jsonExpanded)
    }

    res.json(jsonExpanded)
  }

  /**
   * POST: /api/playlists/:id/batch/add
   * Batch add playlist items
   *
   * @param {RequestWithUser} req
   * @param {Response} res
   */
  async addBatch(req, res) {
    if (!req.body.items?.length) {
      return res.status(400).send('Invalid request body')
    }
    const itemsToAdd = req.body.items

    const libraryItemIds = itemsToAdd.map((i) => i.libraryItemId).filter((i) => i)
    if (!libraryItemIds.length) {
      return res.status(400).send('Invalid request body')
    }

    // Find all library items
    const libraryItems = await Database.libraryItemModel.findAll({
      where: {
        id: libraryItemIds
      }
    })

    // Get all existing playlist media items
    const existingPlaylistMediaItems = await req.playlist.getPlaylistMediaItems({
      order: [['order', 'ASC']]
    })

    const mediaItemsToAdd = []

    // Setup array of playlistMediaItem records to add
    let order = existingPlaylistMediaItems.length + 1
    for (const item of itemsToAdd) {
      const libraryItem = libraryItems.find((li) => li.id === item.libraryItemId)
      if (!libraryItem) {
        return res.status(404).send('Item not found with id ' + item.libraryItemId)
      } else {
        const mediaItemId = item.episodeId || libraryItem.mediaId
        if (existingPlaylistMediaItems.some((pmi) => pmi.mediaItemId === mediaItemId)) {
          // Already exists in playlist
          continue
        } else {
          mediaItemsToAdd.push({
            playlistId: req.playlist.id,
            mediaItemId,
            mediaItemType: item.episodeId ? 'podcastEpisode' : 'book',
            order: order++
          })
        }
      }
    }

    let jsonExpanded = null
    if (mediaItemsToAdd.length) {
      await Database.createBulkPlaylistMediaItems(mediaItemsToAdd)
      jsonExpanded = await req.playlist.getOldJsonExpanded()
      SocketAuthority.clientEmitter(req.playlist.userId, 'playlist_updated', jsonExpanded)
    } else {
      jsonExpanded = await req.playlist.getOldJsonExpanded()
    }
    res.json(jsonExpanded)
  }

  /**
   * POST: /api/playlists/:id/batch/remove
   * Batch remove playlist items
   *
   * @param {RequestWithUser} req
   * @param {Response} res
   */
  async removeBatch(req, res) {
    if (!req.body.items?.length) {
      return res.status(400).send('Invalid request body')
    }

    const itemsToRemove = req.body.items
    const libraryItemIds = itemsToRemove.map((i) => i.libraryItemId).filter((i) => i)
    if (!libraryItemIds.length) {
      return res.status(400).send('Invalid request body')
    }

    // Find all library items
    const libraryItems = await Database.libraryItemModel.findAll({
      where: {
        id: libraryItemIds
      }
    })

    // Get all existing playlist media items for playlist
    const existingPlaylistMediaItems = await req.playlist.getPlaylistMediaItems({
      order: [['order', 'ASC']]
    })
    let numMediaItems = existingPlaylistMediaItems.length

    // Remove playlist media items
    let hasUpdated = false
    for (const item of itemsToRemove) {
      const libraryItem = libraryItems.find((li) => li.id === item.libraryItemId)
      if (!libraryItem) continue
      const mediaItemId = item.episodeId || libraryItem.mediaId
      const existingMediaItem = existingPlaylistMediaItems.find((pmi) => pmi.mediaItemId === mediaItemId)
      if (!existingMediaItem) continue
      await existingMediaItem.destroy()
      hasUpdated = true
      numMediaItems--
    }

    const jsonExpanded = await req.playlist.getOldJsonExpanded()
    if (hasUpdated) {
      // Playlist is removed when there are no items
      if (!numMediaItems) {
        Logger.info(`[PlaylistController] Playlist "${req.playlist.name}" has no more items - removing it`)
        await req.playlist.destroy()
        SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_removed', jsonExpanded)
      } else {
        SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_updated', jsonExpanded)
      }
    }
    res.json(jsonExpanded)
  }

  /**
   * POST: /api/playlists/collection/:collectionId
   * Create a playlist from a collection
   *
   * @param {RequestWithUser} req
   * @param {Response} res
   */
  async createFromCollection(req, res) {
    const collection = await Database.collectionModel.findByPk(req.params.collectionId)
    if (!collection) {
      return res.status(404).send('Collection not found')
    }
    // Expand collection to get library items
    const collectionExpanded = await collection.getOldJsonExpanded(req.user)
    if (!collectionExpanded) {
      // This can happen if the user has no access to all items in collection
      return res.status(404).send('Collection not found')
    }

    // Playlists cannot be empty
    if (!collectionExpanded.books.length) {
      return res.status(400).send('Collection has no books')
    }

    const oldPlaylist = new Playlist()
    oldPlaylist.setData({
      userId: req.user.id,
      libraryId: collection.libraryId,
      name: collection.name,
      description: collection.description || null
    })

    // Create Playlist record
    const newPlaylist = await Database.playlistModel.createFromOld(oldPlaylist)

    // Create PlaylistMediaItem records
    const mediaItemsToAdd = []
    let order = 1
    for (const libraryItem of collectionExpanded.books) {
      mediaItemsToAdd.push({
        playlistId: newPlaylist.id,
        mediaItemId: libraryItem.media.id,
        mediaItemType: 'book',
        order: order++
      })
    }
    await Database.createBulkPlaylistMediaItems(mediaItemsToAdd)

    const jsonExpanded = await newPlaylist.getOldJsonExpanded()
    SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded)
    res.json(jsonExpanded)
  }

  /**
   *
   * @param {RequestWithUser} req
   * @param {Response} res
   * @param {NextFunction} next
   */
  async middleware(req, res, next) {
    if (req.params.id) {
      const playlist = await Database.playlistModel.findByPk(req.params.id)
      if (!playlist) {
        return res.status(404).send('Playlist not found')
      }
      if (playlist.userId !== req.user.id) {
        Logger.warn(`[PlaylistController] Playlist ${req.params.id} requested by user ${req.user.id} that is not the owner`)
        return res.sendStatus(403)
      }
      req.playlist = playlist
    }

    next()
  }
}
module.exports = new PlaylistController()