text: const moment = require('moment-timezone')
  const Supporter = require('../../../structs/db/Supporter.js')
  const Schedule = require('../../../structs/db/Schedule.js')
  const FailRecord = require('../../../structs/db/FailRecord.js')
  const { MenuEmbed, MenuVisual, MessageVisual } = require('discord.js-prompts')
  const ThemedEmbed = require('../common/utils/ThemedEmbed.js')
  const LocalizedPrompt = require('../common/utils/LocalizedPrompt.js')
  const Translator = require('../../../structs/Translator.js')
  const handlePaginationError = require('../common/utils/handlePaginationError.js')
  const ArticleRateLimiter = require('../../../structs/ArticleMessageRateLimiter.js')
  const getConfig = require('../../../config.js').get
  
  /**
   * @typedef {Object} Data
   * @property {import('../../../structs/db/Profile.js')} [profile]
   * @property {import('../../../structs/db/Feed.js')[]} feeds
   * @property {import('discord.js').TextChannel} [channel]
   * @property {string} [searchQuery]
   * @property {string} guildID
   */
  
  /**
   * @param {import('../../../structs/db/Feed.js')[]} feeds
   * @param {string} query
   */
  function queryFeeds (feeds, query) {
    if (!query) {
      return feeds
    }
    return feeds.filter(feed => {
      for (const key in feed) {
        const value = feed[key]
        if (typeof value === 'string' && value.toLowerCase().includes(query.toLowerCase())) {
          return true
        }
      }
    })
  }
  
  /**
   * Returns the set of channels within an array of feeds
   * that is at their limit
   *
   * @param {import('../../../structs/db/Feed.js')[]} feeds
   */
  async function getChannelsAtLimit (feeds) {
    // Get all channel ids with no dupes
    const channels = feeds.map(f => f.channel).filter((channel, index, array) => array.indexOf(channel) === index)
    // Check if each one is at its limit
    const promises = channels.map(channelId => ArticleRateLimiter.getLimiter(channelId).isAtDailyLimit())
    const limitResolves = await Promise.all(promises)
    const channelsAtLimit = new Set()
    // Only insert the channels at limit
    limitResolves.forEach((atLimit, index) => {
      const channelId = channels[index]
      if (atLimit) {
        channelsAtLimit.add(channelId)
      }
    })
    return channelsAtLimit
  }
  
  /**
   * @param {Data} data
   */
  async function listFeedVisual (data) {
    const { feeds, profile, guildID, channel, searchQuery } = data
    const [supporter, schedules, supporterGuilds] = await Promise.all([
      Supporter.getValidSupporterOfGuild(guildID),
      Schedule.getAll(),
      Supporter.getValidFastGuilds()
    ])
    const unqueriedFeeds = channel ? feeds.filter(f => f.channel === channel.id) : feeds
    const targetFeeds = queryFeeds(unqueriedFeeds, searchQuery)
    const translate = Translator.createProfileTranslator(profile)
    const channelsAtLimit = await getChannelsAtLimit(feeds)
    if (feeds.length === 0) {
      return new MessageVisual(translate('commands.list.noFeeds'))
    } else if (targetFeeds.length === 0) {
      return new MessageVisual(translate('commands.list.noFeedsChannel', {
        channel: `<#${channel.id}>${channelsAtLimit.has(channel.id) ? ` (${translate('commands.list.channelLimitReached')})` : ''}`
      }))
    }
  
    const config = getConfig()
    const failRecordsMap = {}
    const maxFeedsAllowed = supporter ? await supporter.getMaxFeeds() : config.feeds.max
  
    // Generate the info for each feed as an array, and push into another array
    const failRecords = await Promise.all(targetFeeds.map(feed => FailRecord.get(feed.url)))
    const fetchedSchedules = await Promise.all(targetFeeds.map(feed => feed.determineSchedule(schedules, supporterGuilds)))
  
    for (const record of failRecords) {
      if (record) {
        failRecordsMap[record._id] = record
      }
    }
    let vipDetails = ''
    if (supporter) {
      vipDetails += '**Patron Until:** '
      if (supporter.expireAt) {
        const expireAt = moment(supporter.expireAt)
        const daysLeft = Math.round(moment.duration(expireAt.diff(moment())).asDays())
        vipDetails += `${expireAt.format('D MMMM YYYY')} (${daysLeft} days)\n`
      } else {
        vipDetails += 'Ongoing\n'
      }
    } else {
      vipDetails = '\n'
    }
  
    const desc = maxFeedsAllowed === 0
      ? `${vipDetails}\u200b\n`
      : `${vipDetails}**${translate('commands.list.serverLimit')}:** ${targetFeeds.length}/${maxFeedsAllowed} [＋](https://www.patreon.com/monitorss)\n\n\u200b`
  
    const list = new ThemedEmbed()
      .setDescription(desc)
  
    const countString = targetFeeds.length === feeds.length ? targetFeeds.length : `${targetFeeds.length}/${feeds.length} total`
  
    if (!channel) {
      list.setAuthor(translate('commands.list.feedList') + ` (${countString})`)
    } else {
      list.setAuthor(translate('commands.list.feedListChannel', {
        channel: channel.name
      }) + ` (${countString})`)
    }
  
    if (supporter) {
      list.setFooter(`Patronage backed by ${supporter._id}`)
    }
  
    const menu = new MenuEmbed(list)
      .enablePagination(handlePaginationError)
  
    let someFailed = false
    targetFeeds.forEach((feed, index) => {
      // URL
      const url = feed.url.length > 500 ? translate('commands.list.exceeds500Characters') : feed.url
  
      // Title
      const title = feed.title
  
      // Channel
      const atLimit = channelsAtLimit.has(feed.channel)
      const channel = `<#${feed.channel}>${atLimit ? ` (${translate('commands.list.channelLimitReached')})` : ''}`
  
      // Status
      const failRecord = failRecordsMap[feed.url]
      let status = ''
      if (feed.disabled) {
        status = translate('commands.list.statusDisabled', { reason: feed.disabled })
      } else if (failRecord && failRecord.hasFailed()) {
        status = translate('commands.list.statusFailed')
        someFailed = true
      } else if (failRecord && !failRecord.hasFailed()) {
        // Determine hours between config spec and now, then calculate health
        const hours = (new Date().getTime() - new Date(failRecord.failedAt).getTime()) / 36e5
        const health = FailRecord.cutoff === 0 ? '(100% health)' : `(${100 - Math.ceil(hours / FailRecord.cutoff * 100)}% health)`
        status = translate('commands.list.statusOk', { failCount: health })
      } else {
        status = translate('commands.list.statusOk', { failCount: '(100% health)' })
      }
  
      // Title checks
      const titleChecks = feed.checkTitles === true
        ? translate('commands.list.titleChecksEnabled')
        : ''
  
      // Webhook
      const webhook = feed.webhook
        ? `${translate('commands.list.webhook')}: ${feed.webhook.id}\n`
        : ''
  
      // Refresh rate
      const schedule = fetchedSchedules[index]
      let refreshRate = failRecord && failRecord.hasFailed()
        ? 'N/A'
        : schedule.refreshRateMinutes < 1
          ? `${schedule.refreshRateMinutes * 60} ${translate('commands.list.seconds')}`
          : `${schedule.refreshRateMinutes} ${translate('commands.list.minutes')}`
      // : translate('commands.list.unknown')
  
      // Patreon link
      if (Supporter.enabled && !supporter) {
        refreshRate += ' [－](https://www.patreon.com/monitorss)'
      }
  
      const name = `${title.length > 200 ? title.slice(0, 200) + '[...]' : title}`
      const value = `${titleChecks}${status}${translate('commands.list.refreshRate')}: ${refreshRate}\n${translate('generics.channelUpper')}: ${channel}\n${webhook}${translate('commands.list.link')}: ${url}`
      const number = feeds.indexOf(feed) + 1
      menu.addOption(name, value, number)
    })
  
    if (someFailed) {
      const failAlert = translate('commands.list.failAlert', {
        prefix: profile && profile.prefix ? profile.prefix : config.bot.prefix
      })
      menu.embed.setDescription(`${menu.embed.description}${failAlert}\n\u200b`)
    }
  
    return new MenuVisual(menu)
  }
  
  const prompt = new LocalizedPrompt(listFeedVisual)
  
  exports.visual = listFeedVisual
  exports.prompt = prompt
  