const { DataTypes, Model } = require('sequelize')
const oldFeed = require('../objects/Feed')
const areEquivalent = require('../utils/areEquivalent')

class Feed extends Model {
  constructor(values, options) {
    super(values, options)

    /** @type {UUIDV4} */
    this.id
    /** @type {string} */
    this.slug
    /** @type {string} */
    this.entityType
    /** @type {UUIDV4} */
    this.entityId
    /** @type {Date} */
    this.entityUpdatedAt
    /** @type {string} */
    this.serverAddress
    /** @type {string} */
    this.feedURL
    /** @type {string} */
    this.imageURL
    /** @type {string} */
    this.siteURL
    /** @type {string} */
    this.title
    /** @type {string} */
    this.description
    /** @type {string} */
    this.author
    /** @type {string} */
    this.podcastType
    /** @type {string} */
    this.language
    /** @type {string} */
    this.ownerName
    /** @type {string} */
    this.ownerEmail
    /** @type {boolean} */
    this.explicit
    /** @type {boolean} */
    this.preventIndexing
    /** @type {string} */
    this.coverPath
    /** @type {UUIDV4} */
    this.userId
    /** @type {Date} */
    this.createdAt
    /** @type {Date} */
    this.updatedAt
  }

  static async getOldFeeds() {
    const feeds = await this.findAll({
      include: {
        model: this.sequelize.models.feedEpisode
      }
    })
    return feeds.map((f) => this.getOldFeed(f))
  }

  /**
   * Get old feed from Feed and optionally Feed with FeedEpisodes
   * @param {Feed} feedExpanded
   * @returns {oldFeed}
   */
  static getOldFeed(feedExpanded) {
    const episodes = feedExpanded.feedEpisodes?.map((feedEpisode) => feedEpisode.getOldEpisode())
    return new oldFeed({
      id: feedExpanded.id,
      slug: feedExpanded.slug,
      userId: feedExpanded.userId,
      entityType: feedExpanded.entityType,
      entityId: feedExpanded.entityId,
      entityUpdatedAt: feedExpanded.entityUpdatedAt?.valueOf() || null,
      coverPath: feedExpanded.coverPath || null,
      meta: {
        title: feedExpanded.title,
        description: feedExpanded.description,
        author: feedExpanded.author,
        imageUrl: feedExpanded.imageURL,
        feedUrl: feedExpanded.feedURL,
        link: feedExpanded.siteURL,
        explicit: feedExpanded.explicit,
        type: feedExpanded.podcastType,
        language: feedExpanded.language,
        preventIndexing: feedExpanded.preventIndexing,
        ownerName: feedExpanded.ownerName,
        ownerEmail: feedExpanded.ownerEmail
      },
      serverAddress: feedExpanded.serverAddress,
      feedUrl: feedExpanded.feedURL,
      episodes: episodes || [],
      createdAt: feedExpanded.createdAt.valueOf(),
      updatedAt: feedExpanded.updatedAt.valueOf()
    })
  }

  static removeById(feedId) {
    return this.destroy({
      where: {
        id: feedId
      }
    })
  }

  /**
   * Find all library item ids that have an open feed (used in library filter)
   * @returns {Promise<string[]>} array of library item ids
   */
  static async findAllLibraryItemIds() {
    const feeds = await this.findAll({
      attributes: ['entityId'],
      where: {
        entityType: 'libraryItem'
      }
    })
    return feeds.map((f) => f.entityId).filter((f) => f) || []
  }

  /**
   * Find feed where and return oldFeed
   * @param {Object} where sequelize where object
   * @returns {Promise<oldFeed>} oldFeed
   */
  static async findOneOld(where) {
    if (!where) return null
    const feedExpanded = await this.findOne({
      where,
      include: {
        model: this.sequelize.models.feedEpisode
      }
    })
    if (!feedExpanded) return null
    return this.getOldFeed(feedExpanded)
  }

  /**
   * Find feed and return oldFeed
   * @param {string} id
   * @returns {Promise<oldFeed>} oldFeed
   */
  static async findByPkOld(id) {
    if (!id) return null
    const feedExpanded = await this.findByPk(id, {
      include: {
        model: this.sequelize.models.feedEpisode
      }
    })
    if (!feedExpanded) return null
    return this.getOldFeed(feedExpanded)
  }

  static async fullCreateFromOld(oldFeed) {
    const feedObj = this.getFromOld(oldFeed)
    const newFeed = await this.create(feedObj)

    if (oldFeed.episodes?.length) {
      for (const oldFeedEpisode of oldFeed.episodes) {
        const feedEpisode = this.sequelize.models.feedEpisode.getFromOld(oldFeedEpisode)
        feedEpisode.feedId = newFeed.id
        await this.sequelize.models.feedEpisode.create(feedEpisode)
      }
    }
  }

  static async fullUpdateFromOld(oldFeed) {
    const oldFeedEpisodes = oldFeed.episodes || []
    const feedObj = this.getFromOld(oldFeed)

    const existingFeed = await this.findByPk(feedObj.id, {
      include: this.sequelize.models.feedEpisode
    })
    if (!existingFeed) return false

    let hasUpdates = false

    // Remove and update existing feed episodes
    for (const feedEpisode of existingFeed.feedEpisodes) {
      const oldFeedEpisode = oldFeedEpisodes.find((ep) => ep.id === feedEpisode.id)
      // Episode removed
      if (!oldFeedEpisode) {
        feedEpisode.destroy()
      } else {
        let episodeHasUpdates = false
        const oldFeedEpisodeCleaned = this.sequelize.models.feedEpisode.getFromOld(oldFeedEpisode)
        for (const key in oldFeedEpisodeCleaned) {
          if (!areEquivalent(oldFeedEpisodeCleaned[key], feedEpisode[key])) {
            episodeHasUpdates = true
          }
        }
        if (episodeHasUpdates) {
          await feedEpisode.update(oldFeedEpisodeCleaned)
          hasUpdates = true
        }
      }
    }

    // Add new feed episodes
    for (const episode of oldFeedEpisodes) {
      if (!existingFeed.feedEpisodes.some((fe) => fe.id === episode.id)) {
        await this.sequelize.models.feedEpisode.createFromOld(feedObj.id, episode)
        hasUpdates = true
      }
    }

    let feedHasUpdates = false
    for (const key in feedObj) {
      let existingValue = existingFeed[key]
      if (existingValue instanceof Date) existingValue = existingValue.valueOf()

      if (!areEquivalent(existingValue, feedObj[key])) {
        feedHasUpdates = true
      }
    }

    if (feedHasUpdates) {
      await existingFeed.update(feedObj)
      hasUpdates = true
    }

    return hasUpdates
  }

  static getFromOld(oldFeed) {
    const oldFeedMeta = oldFeed.meta || {}
    return {
      id: oldFeed.id,
      slug: oldFeed.slug,
      entityType: oldFeed.entityType,
      entityId: oldFeed.entityId,
      entityUpdatedAt: oldFeed.entityUpdatedAt,
      serverAddress: oldFeed.serverAddress,
      feedURL: oldFeed.feedUrl,
      coverPath: oldFeed.coverPath || null,
      imageURL: oldFeedMeta.imageUrl,
      siteURL: oldFeedMeta.link,
      title: oldFeedMeta.title,
      description: oldFeedMeta.description,
      author: oldFeedMeta.author,
      podcastType: oldFeedMeta.type || null,
      language: oldFeedMeta.language || null,
      ownerName: oldFeedMeta.ownerName || null,
      ownerEmail: oldFeedMeta.ownerEmail || null,
      explicit: !!oldFeedMeta.explicit,
      preventIndexing: !!oldFeedMeta.preventIndexing,
      userId: oldFeed.userId
    }
  }

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

  /**
   * Initialize model
   *
   * Polymorphic association: Feeds can be created from LibraryItem, Collection, Playlist or Series
   * @see https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
   *
   * @param {import('../Database').sequelize} sequelize
   */
  static init(sequelize) {
    super.init(
      {
        id: {
          type: DataTypes.UUID,
          defaultValue: DataTypes.UUIDV4,
          primaryKey: true
        },
        slug: DataTypes.STRING,
        entityType: DataTypes.STRING,
        entityId: DataTypes.UUIDV4,
        entityUpdatedAt: DataTypes.DATE,
        serverAddress: DataTypes.STRING,
        feedURL: DataTypes.STRING,
        imageURL: DataTypes.STRING,
        siteURL: DataTypes.STRING,
        title: DataTypes.STRING,
        description: DataTypes.TEXT,
        author: DataTypes.STRING,
        podcastType: DataTypes.STRING,
        language: DataTypes.STRING,
        ownerName: DataTypes.STRING,
        ownerEmail: DataTypes.STRING,
        explicit: DataTypes.BOOLEAN,
        preventIndexing: DataTypes.BOOLEAN,
        coverPath: DataTypes.STRING
      },
      {
        sequelize,
        modelName: 'feed'
      }
    )

    const { user, libraryItem, collection, series, playlist } = sequelize.models

    user.hasMany(Feed)
    Feed.belongsTo(user)

    libraryItem.hasMany(Feed, {
      foreignKey: 'entityId',
      constraints: false,
      scope: {
        entityType: 'libraryItem'
      }
    })
    Feed.belongsTo(libraryItem, { foreignKey: 'entityId', constraints: false })

    collection.hasMany(Feed, {
      foreignKey: 'entityId',
      constraints: false,
      scope: {
        entityType: 'collection'
      }
    })
    Feed.belongsTo(collection, { foreignKey: 'entityId', constraints: false })

    series.hasMany(Feed, {
      foreignKey: 'entityId',
      constraints: false,
      scope: {
        entityType: 'series'
      }
    })
    Feed.belongsTo(series, { foreignKey: 'entityId', constraints: false })

    playlist.hasMany(Feed, {
      foreignKey: 'entityId',
      constraints: false,
      scope: {
        entityType: 'playlist'
      }
    })
    Feed.belongsTo(playlist, { foreignKey: 'entityId', constraints: false })

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

      if (!Array.isArray(findResult)) findResult = [findResult]
      for (const instance of findResult) {
        if (instance.entityType === 'libraryItem' && instance.libraryItem !== undefined) {
          instance.entity = instance.libraryItem
          instance.dataValues.entity = instance.dataValues.libraryItem
        } else if (instance.entityType === 'collection' && instance.collection !== undefined) {
          instance.entity = instance.collection
          instance.dataValues.entity = instance.dataValues.collection
        } else if (instance.entityType === 'series' && instance.series !== undefined) {
          instance.entity = instance.series
          instance.dataValues.entity = instance.dataValues.series
        } else if (instance.entityType === 'playlist' && instance.playlist !== undefined) {
          instance.entity = instance.playlist
          instance.dataValues.entity = instance.dataValues.playlist
        }

        // To prevent mistakes:
        delete instance.libraryItem
        delete instance.dataValues.libraryItem
        delete instance.collection
        delete instance.dataValues.collection
        delete instance.series
        delete instance.dataValues.series
        delete instance.playlist
        delete instance.dataValues.playlist
      }
    })
  }
}

module.exports = Feed