const { DataTypes, Model } = require('sequelize')
const Logger = require('../Logger')
const oldLibraryItem = require('../objects/LibraryItem')
const { areEquivalent } = require('../utils/index')

module.exports = (sequelize) => {
  class LibraryItem extends Model {
    static async getAllOldLibraryItems() {
      let libraryItems = await this.findAll({
        include: [
          {
            model: sequelize.models.book,
            include: [
              {
                model: sequelize.models.author,
                through: {
                  attributes: []
                }
              },
              {
                model: sequelize.models.series,
                through: {
                  attributes: ['sequence']
                }
              }
            ]
          },
          {
            model: sequelize.models.podcast,
            include: [
              {
                model: sequelize.models.podcastEpisode
              }
            ]
          }
        ]
      })
      return libraryItems.map(ti => this.getOldLibraryItem(ti))
    }

    static getOldLibraryItem(libraryItemExpanded) {
      let media = null
      if (libraryItemExpanded.mediaType === 'book') {
        media = sequelize.models.book.getOldBook(libraryItemExpanded)
      } else if (libraryItemExpanded.mediaType === 'podcast') {
        media = sequelize.models.podcast.getOldPodcast(libraryItemExpanded)
      }

      return new oldLibraryItem({
        id: libraryItemExpanded.id,
        ino: libraryItemExpanded.ino,
        libraryId: libraryItemExpanded.libraryId,
        folderId: libraryItemExpanded.libraryFolderId,
        path: libraryItemExpanded.path,
        relPath: libraryItemExpanded.relPath,
        isFile: libraryItemExpanded.isFile,
        mtimeMs: libraryItemExpanded.mtime?.valueOf(),
        ctimeMs: libraryItemExpanded.ctime?.valueOf(),
        birthtimeMs: libraryItemExpanded.birthtime?.valueOf(),
        addedAt: libraryItemExpanded.createdAt.valueOf(),
        updatedAt: libraryItemExpanded.updatedAt.valueOf(),
        lastScan: libraryItemExpanded.lastScan?.valueOf(),
        scanVersion: libraryItemExpanded.lastScanVersion,
        isMissing: !!libraryItemExpanded.isMissing,
        isInvalid: !!libraryItemExpanded.isInvalid,
        mediaType: libraryItemExpanded.mediaType,
        media,
        libraryFiles: libraryItemExpanded.libraryFiles
      })
    }

    static async fullCreateFromOld(oldLibraryItem) {
      const newLibraryItem = await this.create(this.getFromOld(oldLibraryItem))

      if (oldLibraryItem.mediaType === 'book') {
        const bookObj = sequelize.models.book.getFromOld(oldLibraryItem.media)
        bookObj.libraryItemId = newLibraryItem.id
        const newBook = await sequelize.models.book.create(bookObj)

        const oldBookAuthors = oldLibraryItem.media.metadata.authors || []
        const oldBookSeriesAll = oldLibraryItem.media.metadata.series || []

        for (const oldBookAuthor of oldBookAuthors) {
          await sequelize.models.bookAuthor.create({ authorId: oldBookAuthor.id, bookId: newBook.id })
        }
        for (const oldSeries of oldBookSeriesAll) {
          await sequelize.models.bookSeries.create({ seriesId: oldSeries.id, bookId: newBook.id, sequence: oldSeries.sequence })
        }
      } else if (oldLibraryItem.mediaType === 'podcast') {
        const podcastObj = sequelize.models.podcast.getFromOld(oldLibraryItem.media)
        podcastObj.libraryItemId = newLibraryItem.id
        const newPodcast = await sequelize.models.podcast.create(podcastObj)

        const oldEpisodes = oldLibraryItem.media.episodes || []
        for (const oldEpisode of oldEpisodes) {
          const episodeObj = sequelize.models.podcastEpisode.getFromOld(oldEpisode)
          episodeObj.libraryItemId = newLibraryItem.id
          episodeObj.podcastId = newPodcast.id
          await sequelize.models.podcastEpisode.create(episodeObj)
        }
      }

      return newLibraryItem
    }

    static async fullUpdateFromOld(oldLibraryItem) {
      const libraryItemExpanded = await this.findByPk(oldLibraryItem.id, {
        include: [
          {
            model: sequelize.models.book,
            include: [
              {
                model: sequelize.models.author,
                through: {
                  attributes: []
                }
              },
              {
                model: sequelize.models.series,
                through: {
                  attributes: ['sequence']
                }
              }
            ]
          },
          {
            model: sequelize.models.podcast,
            include: [
              {
                model: sequelize.models.podcastEpisode
              }
            ]
          }
        ]
      })
      if (!libraryItemExpanded) return false

      let hasUpdates = false

      // Check update Book/Podcast
      if (libraryItemExpanded.media) {
        let updatedMedia = null
        if (libraryItemExpanded.mediaType === 'podcast') {
          updatedMedia = sequelize.models.podcast.getFromOld(oldLibraryItem.media)

          const existingPodcastEpisodes = libraryItemExpanded.media.podcastEpisodes || []
          const updatedPodcastEpisodes = oldLibraryItem.media.episodes || []

          for (const existingPodcastEpisode of existingPodcastEpisodes) {
            // Episode was removed
            if (!updatedPodcastEpisodes.some(ep => ep.id === existingPodcastEpisode.id)) {
              Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingPodcastEpisode.title}" was removed`)
              await existingPodcastEpisode.destroy()
              hasUpdates = true
            }
          }
          for (const updatedPodcastEpisode of updatedPodcastEpisodes) {
            const existingEpisodeMatch = existingPodcastEpisodes.find(ep => ep.id === updatedPodcastEpisode.id)
            if (!existingEpisodeMatch) {
              Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${updatedPodcastEpisode.title}" was added`)
              await sequelize.models.podcastEpisode.createFromOld(updatedPodcastEpisode)
              hasUpdates = true
            } else {
              const updatedEpisodeCleaned = sequelize.models.podcastEpisode.getFromOld(updatedPodcastEpisode)
              let episodeHasUpdates = false
              for (const key in updatedEpisodeCleaned) {
                let existingValue = existingEpisodeMatch[key]
                if (existingValue instanceof Date) existingValue = existingValue.valueOf()

                if (!areEquivalent(updatedEpisodeCleaned[key], existingValue, true)) {
                  Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingEpisodeMatch.title}" ${key} was updated from "${existingValue}" to "${updatedEpisodeCleaned[key]}"`)
                  episodeHasUpdates = true
                }
              }
              if (episodeHasUpdates) {
                await existingEpisodeMatch.update(updatedEpisodeCleaned)
                hasUpdates = true
              }
            }
          }
        } else if (libraryItemExpanded.mediaType === 'book') {
          updatedMedia = sequelize.models.book.getFromOld(oldLibraryItem.media)

          const existingAuthors = libraryItemExpanded.media.authors || []
          const existingSeriesAll = libraryItemExpanded.media.series || []
          const updatedAuthors = oldLibraryItem.media.metadata.authors || []
          const updatedSeriesAll = oldLibraryItem.media.metadata.series || []

          for (const existingAuthor of existingAuthors) {
            // Author was removed from Book
            if (!updatedAuthors.some(au => au.id === existingAuthor.id)) {
              Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`)
              await sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id)
              hasUpdates = true
            }
          }
          for (const updatedAuthor of updatedAuthors) {
            // Author was added
            if (!existingAuthors.some(au => au.id === updatedAuthor.id)) {
              Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`)
              await sequelize.models.bookAuthor.create({ authorId: updatedAuthor.id, bookId: libraryItemExpanded.media.id })
              hasUpdates = true
            }
          }
          for (const existingSeries of existingSeriesAll) {
            // Series was removed
            if (!updatedSeriesAll.some(se => se.id === existingSeries.id)) {
              Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${existingSeries.name}" was removed`)
              await sequelize.models.bookSeries.removeByIds(existingSeries.id, libraryItemExpanded.media.id)
              hasUpdates = true
            }
          }
          for (const updatedSeries of updatedSeriesAll) {
            // Series was added/updated
            const existingSeriesMatch = existingSeriesAll.find(se => se.id === updatedSeries.id)
            if (!existingSeriesMatch) {
              Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" was added`)
              await sequelize.models.bookSeries.create({ seriesId: updatedSeries.id, bookId: libraryItemExpanded.media.id, sequence: updatedSeries.sequence })
              hasUpdates = true
            } else if (existingSeriesMatch.bookSeries.sequence !== updatedSeries.sequence) {
              Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" sequence was updated from "${existingSeriesMatch.bookSeries.sequence}" to "${updatedSeries.sequence}"`)
              await existingSeriesMatch.bookSeries.update({ sequence: updatedSeries.sequence })
              hasUpdates = true
            }
          }
        }

        let hasMediaUpdates = false
        for (const key in updatedMedia) {
          let existingValue = libraryItemExpanded.media[key]
          if (existingValue instanceof Date) existingValue = existingValue.valueOf()

          if (!areEquivalent(updatedMedia[key], existingValue, true)) {
            Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${libraryItemExpanded.mediaType}.${key} updated from ${existingValue} to ${updatedMedia[key]}`)
            hasMediaUpdates = true
          }
        }
        if (hasMediaUpdates && updatedMedia) {
          await libraryItemExpanded.media.update(updatedMedia)
          hasUpdates = true
        }
      }

      const updatedLibraryItem = this.getFromOld(oldLibraryItem)
      let hasLibraryItemUpdates = false
      for (const key in updatedLibraryItem) {
        let existingValue = libraryItemExpanded[key]
        if (existingValue instanceof Date) existingValue = existingValue.valueOf()

        if (!areEquivalent(updatedLibraryItem[key], existingValue, true)) {
          Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${key} updated from ${existingValue} to ${updatedLibraryItem[key]}`)
          hasLibraryItemUpdates = true
        }
      }
      if (hasLibraryItemUpdates) {
        await libraryItemExpanded.update(updatedLibraryItem)
        hasUpdates = true
      }
      return hasUpdates
    }

    static updateFromOld(oldLibraryItem) {
      const libraryItem = this.getFromOld(oldLibraryItem)
      return this.update(libraryItem, {
        where: {
          id: libraryItem.id
        }
      }).then((result) => result[0] > 0).catch((error) => {
        Logger.error(`[LibraryItem] Failed to update libraryItem ${libraryItem.id}`, error)
        return false
      })
    }

    static getFromOld(oldLibraryItem) {
      return {
        id: oldLibraryItem.id,
        ino: oldLibraryItem.ino,
        path: oldLibraryItem.path,
        relPath: oldLibraryItem.relPath,
        mediaId: oldLibraryItem.media.id,
        mediaType: oldLibraryItem.mediaType,
        isFile: !!oldLibraryItem.isFile,
        isMissing: !!oldLibraryItem.isMissing,
        isInvalid: !!oldLibraryItem.isInvalid,
        mtime: oldLibraryItem.mtimeMs,
        ctime: oldLibraryItem.ctimeMs,
        birthtime: oldLibraryItem.birthtimeMs,
        lastScan: oldLibraryItem.lastScan,
        lastScanVersion: oldLibraryItem.scanVersion,
        libraryId: oldLibraryItem.libraryId,
        libraryFolderId: oldLibraryItem.folderId,
        libraryFiles: oldLibraryItem.libraryFiles?.map(lf => lf.toJSON()) || []
      }
    }

    static removeById(libraryItemId) {
      return this.destroy({
        where: {
          id: libraryItemId
        },
        individualHooks: true
      })
    }

    getMedia(options) {
      if (!this.mediaType) return Promise.resolve(null)
      const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaType)}`
      return this[mixinMethodName](options)
    }
  }

  LibraryItem.init({
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    ino: DataTypes.STRING,
    path: DataTypes.STRING,
    relPath: DataTypes.STRING,
    mediaId: DataTypes.UUIDV4,
    mediaType: DataTypes.STRING,
    isFile: DataTypes.BOOLEAN,
    isMissing: DataTypes.BOOLEAN,
    isInvalid: DataTypes.BOOLEAN,
    mtime: DataTypes.DATE(6),
    ctime: DataTypes.DATE(6),
    birthtime: DataTypes.DATE(6),
    lastScan: DataTypes.DATE,
    lastScanVersion: DataTypes.STRING,
    libraryFiles: DataTypes.JSON
  }, {
    sequelize,
    modelName: 'libraryItem'
  })

  const { library, libraryFolder, book, podcast } = sequelize.models
  library.hasMany(LibraryItem)
  LibraryItem.belongsTo(library)

  libraryFolder.hasMany(LibraryItem)
  LibraryItem.belongsTo(libraryFolder)

  book.hasOne(LibraryItem, {
    foreignKey: 'mediaId',
    constraints: false,
    scope: {
      mediaType: 'book'
    }
  })
  LibraryItem.belongsTo(book, { foreignKey: 'mediaId', constraints: false })

  podcast.hasOne(LibraryItem, {
    foreignKey: 'mediaId',
    constraints: false,
    scope: {
      mediaType: 'podcast'
    }
  })
  LibraryItem.belongsTo(podcast, { foreignKey: 'mediaId', constraints: false })

  LibraryItem.addHook('afterFind', findResult => {
    if (!findResult) return

    if (!Array.isArray(findResult)) findResult = [findResult]
    for (const instance of findResult) {
      if (instance.mediaType === 'book' && instance.book !== undefined) {
        instance.media = instance.book
        instance.dataValues.media = instance.dataValues.book
      } else if (instance.mediaType === 'podcast' && instance.podcast !== undefined) {
        instance.media = instance.podcast
        instance.dataValues.media = instance.dataValues.podcast
      }
      // To prevent mistakes:
      delete instance.book
      delete instance.dataValues.book
      delete instance.podcast
      delete instance.dataValues.podcast
    }
  })

  LibraryItem.addHook('afterDestroy', async instance => {
    if (!instance) return
    const media = await instance.getMedia()
    if (media) {
      media.destroy()
    }
  })

  return LibraryItem
}