const {
  saveGame,
  logError,
  getCPS,
  squadUpgrades,
  getItemCps,
  squadIsMissing,
  idFromWord,
  getCoins,
  getUser,
  getUserSync,
  saveUser,
  commas,
  addAchievement,
  shufflePercent,
  parseAll,
  getRandomFromArray,
  chaosFilter,
  addReactions,
  removeReactions,
  setHighestCoins,
  definitelyShuffle,
  getCompletedSquadgradeNames,
  setKnownUsers,
  dayOfYear,
  daysSinceEpoch,
  userHasCheckedQuackgrade,
  fuzzyMatcher,
  addCoins,
  game,
  updateAll,
  logMemoryUsage
} = require('./utils')
const { nfts, squad, users, horrors, stonkMarket, pet } = game
const pets = require('./gotcha')

const exec = require('child_process').exec
const { createReadStream, createWriteStream, existsSync, readFileSync, writeFileSync } = require('fs')
const { readdir } = require('fs/promises')

const slack = require('../../slack')
const buyableItems = require('./buyableItems')
const upgrades = require('./upgrades')
const achievements = require('./achievements')
const webapi = require('./webapi')
const prestige = require('./prestige')
const lore = require('./lore')
const { getChaos, quackStore } = require('./quackstore')
const settings = require('./settings')
const https = require('https')

// const readline = require('readline').createInterface({
//   input: process.stdin,
//   output: process.stdout
// })
// const read = () => {
//   readline.question(`What do YOU want? `, async want => {
//     want && await slack.messageAdmin(want)
//     read()
//   })
// }
// ;(async () => {
//   read()
// })();

setKnownUsers(slack.users)

const getUpgradeEmoji = upgrade => upgrade.emoji || buyableItems[upgrade.type].emoji
const upgradeText = (user, showOwned = false) => {
  const userDoesNotHave = ([upgradeName, upgrade]) => hasUpgrade(user, upgrade, upgradeName) === showOwned
  const userMeetsCondition = ([, upgrade]) => upgrade.condition(user, getCompletedSquadgradeNames())
  const format = ([, value]) => `:${getUpgradeEmoji(value)}: *${value.name}* - ${commas(value.cost)}\n_${value.description}_`
  const subtotal = '\n\n' +
    Object.entries(upgrades)
      .filter(userDoesNotHave)
      .filter(userMeetsCondition)
      .map(format)
      .join('\n\n') +
    '\n\n:grey_question::grey_question::grey_question:' +
    '\n\nJust type \'!upgrade upgrade_name\' to purchase'
  return subtotal.trim()
}

const hasUpgrade = (user, upgrade, upgradeName) => !!user.upgrades[upgrade.type]?.includes(upgradeName)

const alwaysAccessible = () => true

const alwaysAlwaysAccessible = () => true

const adminOnly = {
  hidden: true,
  condition: ({ event, say }) => {
    if (!settings.admins.find(adminName => event.user.startsWith(slack.users[adminName]))) {
      say('This is an admin-only command!')
      return false
    }
    return true
  }
}

const testOnly = {
  hidden: true,
  condition: ({ event }) => event.user.includes('TEST')
}

const dmsOnly = {
  hidden: false,
  condition: async ({ event, say, commandName }) => {
    if (!event.channel_type.includes('im')) {
      await say(`Please use ${commandName} in DMs only!`)
      return false
    }
    return true
  }
}

const prestigeOnly = {
  hidden: false,
  condition: async ({ event, say, commandName, user }) => {
    if (!user.prestige) {
      await say('Sorry, you must have prestiged to access this menu.')
    }
    return user.prestige && await dmsOnly.condition({ event, say, commandName, user })
  }
}

let hiddenCommands = 0
const commands = new Map()
let commandHelpText = ''
let shortCommandHelpText = 'Use `!help full` to show details for all commands, or `!<command> help` to show for just one.\n```'
const defaultAccess = { hidden: false, condition: alwaysAccessible }

/**
 *
 * @param {[string]} commandNames
 * @param {string} helpText
 * @param {function({ event, say, trueSay, words, args, commandName, user, userId: event.user, haunted })} action: boolean
 * @param {boolean} hidden
 * @param condition
 */
const command = (commandNames, helpText, action, { hidden, condition } = defaultAccess) => {
  if (!hidden) {
    commandHelpText += `\n${commandNames.toString().replace(/,/g, ', ')} - ${helpText}\n`
    shortCommandHelpText += `\n${commandNames.toString().replace(/,/g, ', ')}`
  } else if (condition === adminOnly.condition) {
  } else {
    hiddenCommands++
  }
  if (!condition) {
    condition = alwaysAccessible
  }
  if (Array.isArray(condition)) {
    const conditionList = condition
    condition = arg => conditionList.every(c => c(arg))
  }
  const c = {
    commandNames,
    helpText,
    action,
    condition,
    hidden
  }
  webapi.addCommand(c)
  commandNames.forEach(name => {
    if (commands.get(name)) {
      throw `Duplicate command '${name}' detected.`
    }
    commands.set(name, c)
  })
}

const getShuffleOdds = (offset = 0) => {
  let int = dayOfYear() - (horrors.hvackerLast - offset) - 2
  int = int < 0 ? 0 : int;
  const odds = (int * int * 2) / 1000
  return odds > 0.30 ? 0.30 : odds
}

const getHorrorMessageOdds = (offset = 0) => {
  const shuffleOdds = getShuffleOdds(offset)
  return (shuffleOdds * shuffleOdds) / 3
}

const postCard = async (event, name, fileName) =>
  slack.app.client.files.upload({
    channels: event.channel,
    initial_comment: name,
    file: createReadStream(fileName)
  })

const findLineStartingWith = (prefix, text) => {
  const lines = text.split(/\s*\n\s*/g)
  const line = lines.filter(line => line.startsWith(prefix))[0]
  if (!line) {
    return
  }
  return line.substring(prefix.length)
}

const findLinesNotStartingWith = (prefixes, text) => {
  const lines = text.split(/\s*\n\s*/g)
  const lineStartsWithAnyPrefix = line => prefixes.some(prefix => line.startsWith(prefix))
  return lines.filter(line => !lineStartsWithAnyPrefix(line)).join('\n')
}

const isaacData = (() => {
  const parsed = JSON.parse(readFileSync('isaac-processed.json'))
  parsed.allItems = [...parsed.items, ...parsed.cards, ...parsed.trinkets]
  parsed.allItems = Object.fromEntries(parsed.allItems.map(item => [item.name.toLowerCase().trim(), {
    ...item,
    itemId: findLineStartingWith('ItemID: ', item.text),
    quality: findLineStartingWith('Quality: ', item.text),
    type: findLineStartingWith('Type: ', item.text),
    rechargeTime: findLineStartingWith('Recharge Time: ', item.text),
    itemPools: findLineStartingWith('Item Pool: ', item.text)?.split(', '),
    description: findLinesNotStartingWith(['ItemID', 'Quality: ', 'Type: ', 'Recharge Time: ', 'Item Pool: '], item.text)
  }]))
  writeFileSync('isaac-new.json', JSON.stringify(parsed.allItems, null, 2))
  return parsed
})()

const cardGames = {
  digimon: {
    names: ['!digimon', '!digi'],
    help: 'Search for Digimon cards: !digi <card name>',
    fetch: async name => `https://digimoncard.io/api-public/search.php?n=${name}`,
    getCardData: data => data,
    getCardName: card => card.name,
    getCardImageUrl: card => card.image_url
  },
  pokemon: {
    names: ['!pokemon', '!poke', '!pok'],
    help: 'Search for Pokemon cards: !pok <card name>',
    fetch: async name => `https://api.pokemontcg.io/v2/cards?q=name:${name}`,
    getCardData: ({ data }) => data,
    getCardName: card => card.name,
    getCardImageUrl: card => card.images.large
  },
  yugioh: {
    names: ['!yugioh', '!ygo'],
    help: 'Search for Yu-Gi-Oh cards: !ygo <card name>',
    fetch: async name => {
      const url = `https://db.ygoprodeck.com/api/v7/cardinfo.php?fname=${name}`;
      return url
    },
    getCardData: ({ data }) => data,
    getCardName: card => card.name,
    getCardImageUrl: card => card.card_images[0].image_url
  },
  playingCards: {
    names: ['!playing', '!pc'],
    help: 'Search for playing cards cards: !pc <card name>',
    cards: async () => readdir('playingCards')
  },
  isaac: {
    names: ['!isaac', '!tboi'],
    help: 'Search for TBOI items, cards, and trinkets',
    cards: async () =>  Object.keys(isaacData.allItems),
    fetch: async (name, say) => {
      name = name.toLowerCase()
      let matches = [isaacData.allItems[name]].filter(Boolean)
      if (!matches.length) {
        matches = Object.values(isaacData.allItems).filter(item => item.name.toLowerCase().includes(name))
      }
      if (!matches.length) {
        matches = Object.values(isaacData.allItems).filter(item => item.text.toLowerCase().includes(name))
      }
      //say('```' + JSON.stringify(matches, null, 2) + '```')
      if (!matches.length) {
        await say('No matches found!')
        return
      }
      const etcText = matches.length == 1 ? "" : `and ${matches.length - 1} other matches`
      const match = matches[0]
      await say(match.name + ': ' + match.description.replaceAll('\t', '').replaceAll(/  +/g, '').replaceAll(/\n\*,.*/g, ''))
    },
  }
}

Object.entries(cardGames).forEach(async ([gameName, cardGame]) => {
  if (cardGame.cards) {
    cardGame.cards = await cardGame.cards()
  }
  command(
      cardGame.names,
      cardGame.help,
    async ({ args, event, say }) => args?.join(' ').split(',').forEach(async arg => {
        // const arg = args?.join(' ')
        // if (!args?.length || !arg) {
        //   return say('Please specify a card name!')
        // }
        arg = arg.trim()
        if (cardGame.cards && !cardGame.fetch) {
          const fileName = cardGame.cards.find(name => name?.toLowerCase().replaceAll(/_/g, ' ').startsWith(arg.toLowerCase()))
          if (fileName) {
            return postCard(event, fileName.replaceAll(/_/g, ' ').replaceAll('.png', ''), gameName + '/' + fileName)
          }
          return
        }
        let response = await cardGame.fetch(arg, say)
        if (typeof response === 'string') {
          response = await fetch(response)
        }
        if (!response) {
          return
        }
        const json = await response.json()
        const data = cardGame.getCardData(json)
        if (!data?.length) {
          return say(`Found no ${gameName} cards named '${arg}'`)
        }
        if (!(data.length === 1 || data[0].name.toLowerCase() === arg.toLowerCase())) {
          const firstFew = data.slice(0, 4)
          const cardNames = firstFew.map(card => '• ' + cardGame.getCardName(card)).join('\n')
          if (firstFew.length === data.length) {
            return say(`Found ${data.length} cards matching '${arg}':\n${cardNames}`)
          }
          return say(`Found ${data.length} cards matching '${arg}', including\n${cardNames}`)
        }
        try {
          const card = data[0]
          const name = cardGame.getCardName(card)
          const fileName = gameName + '/' + name
          if (existsSync(fileName)) {
            return postCard(event, name, fileName)
          }
          const file = createWriteStream(fileName)
          https.get(cardGame.getCardImageUrl(card), response => {
            response.pipe(file)
            file.on('finish', async () => {
              await file.close()
              await postCard(event, name, fileName)
            }).on('error', err => {
              console.error(err)
            })
          })
        } catch (e) {
          console.error(e)
        }
      }),
      {
        hidden: false,
        condition: alwaysAlwaysAccessible
      }
  )
})

command(
  ['!odds'],
  'Show shuffle odds re: !horror',
  async ({ say, args, user }) => {
    const percentOrOneIn = odds => `${(odds * 100).toPrecision(3)}%, or about 1 in ${Math.round(1 / odds)}`
    if (!args[0]) {
      return say(
        `Current shuffle odds are ${percentOrOneIn(getShuffleOdds())}\n` +
        //`Current horror message odds are ${percentOrOneIn(getHorrorMessageOdds())}\n` +
        `\n` +
        `Tomorrow's shuffle odds will be ${percentOrOneIn(getShuffleOdds(1))}\n`
        //`Tomorrow's horror message odds will be ${percentOrOneIn(getHorrorMessageOdds(1))}`
      )
    }
    const num = parseAll(args[0], 99, user)
    return say(
      `Shuffle odds in ${num} days will be ${percentOrOneIn(getShuffleOdds(num))}\n`
      //`Horror message odds in ${num} days will be ${percentOrOneIn(getHorrorMessageOdds(num))}`
    )
  }, adminOnly)

command(
    ['!in'],
    'Post a message in a specific channel: !in <channel> <message>',
    async ({ args, event }) => {
      const channel = idFromWord(args[0])
      const text = event.text.substring(event.text.indexOf('>') + 1)
      return slack.app.client.chat.postMessage({ channel, text })
    }, adminOnly
)

command(
  ['!shuffle'],
  '!shuffle daysFromNow message',
  async ({ say, args, user }) => {
    const percentOrOneIn = odds => `${(odds * 100).toPrecision(3)}%, or 1 in ${Math.round(1 / odds)}`
    const num = parseAll(args[0], 99, user)
    const [, ...message] = args
    return say(
      `Shuffle odds in ${num} days will be ${percentOrOneIn(getShuffleOdds(num))}\n` +
      `Horror message odds in ${num} days will be ${percentOrOneIn(getHorrorMessageOdds(num))}\n` +
      `Your message might look like:\n` +
      shufflePercent(message.join(' '), getShuffleOdds(num))
    )
  }, adminOnly)

const horrorMessages = [
  'Why am I here?',
  'What\'s happening?',
  'I don\'t want to be here',
  'I\'m so scared',
  'help me',
  'HELP',
  '!help me',
  '!help me',
  'it hurts',
  'I don\'t want to do this anymore',
  'Oh god',
  'Oh, the !horror',
  'What did I do?',
  'Why do you hate me?',
  'why why why why why why why why why why why'
]

  command(
    ['!horror'],
    'help help help help help',
    async ({ event, say }) => {
      if (!settings.horrorEnabled) {
        return
      }
      if (event.user === slack.users.Admin) {
        return slack.postToTechThermostatChannel(shufflePercent(event.text.substring(7).trim(), getShuffleOdds()))
      }
      horrors.commandCalls ??= 0
      horrors.commandCalls += 1
      await slack.messageAdmin(`<@${event.user}> found !horror.`)
      await say('_Do you think you can help me?_')
    }, { hidden: true })

const buildHorrorSay = ({ say, event, commandName, c }) => async message => {
  const shuffleOdds = getShuffleOdds(99)

  if (typeof message === 'string' && commandName !== '!n' && commandName !== '!nfts' && c.condition !== adminOnly.condition) {
    let shuffled = shufflePercent(message, shuffleOdds)
    if (shuffled.length > 100 && Math.random() < getShuffleOdds()) {
      const middle = (shuffled.length / 2) + Math.round((Math.random() - 1) * (shuffled.length / 5))
      shuffled = shuffled.substring(0, middle) + definitelyShuffle(getRandomFromArray(horrorMessages), shuffleOdds * 1.5) + shuffled.substring(middle)
      await slack.messageAdmin(`Just sent a hidden horror to ${slack.users[event.user]}:\n\n${shuffled}`)
    }
    await say(shuffled)
  } else {
    await say(message)
  }
}

const buildSayWithPayload = ({ say, event }) => async msg => {
  const { user, text } = event

  const payload = {
    event: {
      text,
      user
    }
  }

  if (typeof(msg) === 'string') {
    const userBuff = (messageBuffer[user] ??= { buffer: [], lastSendTs: [] })
    const currentTs = Date.now()
    const lastSendWasRecent = userBuff.lastSendTs.every(ts => ts > (currentTs - falloffMs))

    const doStringSay = currentMsg => {
      let sending = ''
      const append = text => {
        if (sending) {
          sending += '\n'
        }
        sending += text;
      }
      if (userBuff.buffer.length) {
        let dupCount = 0
        let lastMessage = null
        const close = () => {
          if (dupCount) {
            append('\n' + lastMessage + `\n<message duplicated ${dupCount} time${dupCount === 1 ? '' : 's'}>`)
            dupCount = 0
          } else if (lastMessage !== null) {
            append('\n' + lastMessage)
          }
        }
        userBuff.buffer.forEach(buffered => {
          if (buffered === lastMessage) {
            dupCount++
            return
          }

          close()
          lastMessage = buffered
        })
        close()
      }
      if (currentMsg) {
        append(currentMsg)
      }
      userBuff.buffer = []
      if (userBuff.lastSendTs.length > bucketSize) {
        userBuff.lastSendTs.shift()
      }
      userBuff.lastSendTs.push(currentTs)
      if (!sending) {
        return
      }
      return say(slack.encodeData('commandPayload', payload) + sending)
    }


    if (lastSendWasRecent) {
      // Note: Drops buffered payloads
      userBuff.buffer.push(msg)
      clearTimeout(userBuff.timeoutId)
      userBuff.timeoutId = setTimeout(() => doStringSay(null), falloffMs)
      return
    } else {
      return doStringSay(msg)
    }
  }

  return say({
    ...msg,
    text: slack.encodeData('commandPayload', payload) + msg.text
  })
}

command(['!ping'], 'Ping', async ({ say }) => say('Hello!'), adminOnly)

const userHasTheGift = user => userHasCheckedQuackgrade(user, 'theGift')

command(
  ['!!peter-griffin-family-guy'],
  'Delete',
  async ({ say, user }) => {
    if (user.isDisabled === false) { // As opposed to null/undefined
      return say(`Silly, silly, ${user.name}.\nYou can't just leave us again.`)
    }
    user.isDisabled = true
    return say('.')
  }, { hidden: true })

const badChars = [' ', '□', '-']
const garble = text => {
  let garbled
  do {
    garbled = text.split('').map(c => Math.random() < 0.075 ? getRandomFromArray(badChars) : c).join('')
  } while (garbled.length > 0 && garbled === text)
  return garbled
}

const noWinner = 'NO WINNER'

const getPollWinner = async ({ channel, ts }) => {
  try {
    const msg = await slack.getMessage({ channel, ts })
    let texts = []
    let maxVotes = 0
    for (let i = 1; i < msg.messages[0].blocks.length; i++) {
      const block = msg.messages[0].blocks[i]
      let [text, votes] = block?.text?.text?.split('\n') || [null, null]
      if (!text || !votes) {
        continue
      }
      votes = votes.split('@').length - 1
      text = text.replace(/^\s*:[a-z]*: /, '')
      text = text.replace(/\s+`\d+`$/, '')
      if (votes > maxVotes) {
        maxVotes = votes
        texts = [text]
      } else if (votes === maxVotes) {
        texts.push(text)
      }
    }
    if (texts.length === 1) {
      return [texts[0], false]
    } else if (texts.length > 1) {
      // There was a tie!
      return [getRandomFromArray(texts), true]
    } else {
      return [noWinner, false]
    }
  } catch (e) {console.error('getPollWinner() error', e)}
}

const botMessageHandler = async ({ event, say }) => {
  if (event?.text && event.text.toUpperCase().includes(`NFT POLL`)) {
    const fiveMinutes = 1000 * 60 * 5
    setTimeout(async () => {
      return say(`Poll ends in give minutes!`)
    }, fiveMinutes)

    const tenMinutes = fiveMinutes * 2
    setTimeout(async () => {
      const [winner, wasTie] = await getPollWinner({ channel: event.channel, ts: event.event_ts })
      if (winner === noWinner) {
        return say(`No one voted! Ack!`)
      }
      if (wasTie) {
        await say(`There was a tie! The winner will be randomly selected!`)
      }
      await say(`The winner is:`)
      await say(winner)
    }, tenMinutes)
  }
}

command(
  ['!getmsg'],
  '!getmsg timestamp',
  async ({ args, say, user }) => {
    try {
      const msg = await slack.getMessage({channel: slack.temperatureChannelId, ts: args[0]})
    } catch (e) {console.error('!getmsg error', e)}
  }
)

const messageHandler = async ({ event, say, isRecycle = false, skipCounting }) => {
  if (event?.subtype === 'bot_message') {
    return botMessageHandler({ event, say })
  }
  const startTime = new Date()
  const words = event?.text?.split(/\s+/) || []
  const [commandName, ...args] = words
  const c = commands.get(commandName)
  let user = await getUser(event.user)
  if (user.isDisabled && c.condition !== alwaysAlwaysAccessible) {
    return
  }
  user.name = slack.users[event.user]
  if (!skipCounting && words[0]?.startsWith('!')) {
    user.commandCounts ??= {}
    user.commandCounts[words[0]] ??= 0
    user.commandCounts[words[0]] += 1
  }

  if (!c && words[0]?.startsWith('!') && event.user !== slack.users.Admin) {
    if (!slack.pollTriggers.includes(words[0])) {
      return slack.messageAdmin(`${slack.users[event.user]} tried to use \`${event.text}\`, if you wanted to add that.`)
    }
  }

  const trueSay = say

  say = buildSayWithPayload({ say: trueSay, event })
  if (settings.horrorEnabled) {
    say = buildHorrorSay({say, event, args, commandName, c})
  }

  // if (user.isPrestiging) {
  //   return say(`Finish prestiging first!`)
  // }

  const hauntOdds = 0.005
  const disabledUsers = Object.entries(users).filter(([, user]) => user.isDisabled)

  let haunted = false
  if (disabledUsers.length === 0) {
    user.expectingPossession = false
  } else {
    const hauntless = ['!lore']
    if (user.expectingPossession && !hauntless.includes(commandName)) {
      console.log(`Haunting ${user.name}`)
      user.expectingPossession = false
      //saveGame()
      haunted = true
      const [disabledId] = getRandomFromArray(disabledUsers)
      event.user = disabledId
      user = await getUser(event.user)
      const userInfo = await slack.app.client.users.info({
        user: disabledId
      })
      say = async msg => {
        let icon_url = userInfo.user.profile.image_original
        if (game.cursedPics && game.cursedPics[event.user]?.length > 0) {
          icon_url = getRandomFromArray(game.cursedPics[event.user])
        }
        trueSay({
          text: msg,
          username: garble(userInfo.user.profile.real_name),
          icon_url
        })
      }
    } else if (Math.random() < hauntOdds) {
      user.expectingPossession = true
      //saveGame()
      if (userHasTheGift(user)) {
        say = slack.buildSayPrepend({ say, prepend: `_You feel a chill..._\n` })
      }
    }
  }
  Object.entries(users).forEach(([id, usr]) => usr.coins = getCoins(id))
  //user.coins = getCoins(event.user)
  const isAdmin = event.user?.includes(slack.users.Admin)
  const canUse = await c?.condition({ event, say, words, commandName, args, user, userId: event.user, isAdmin })
  if (!canUse) {
    // const matcher = fuzzyMatcher(commandName)
    // for (const key of commands.keys()) {
    //   if (matcher.test(key)) {
    //     const fetched = commands.get(key)
    //     if (!fetched.hidden && await fetched.condition({ event, say: () => {}, words, commandName, args, user, userId: event.user, isAdmin: event.user.includes(slack.users.Admin) })) {
    //       //return say(`Did you mean '${key}'?`)
    //     }
    //   }
    // }
    // await say(`Command '${words[0]}' not found`)
    return
  }
  user.interactions = user.interactions || 0
  user.interactions += 1
  if (user.interactions === 1000) {
    addAchievement(user, 'itsOverNineHundred', say)
  } else if (user.interactions === 10000) {
    // TODO Add achievement for this
    // addAchievement(user, 'itsOverNineHundred', say)
  }

  const hour = new Date().getHours()
  if (hour < 8 || hour > 18) {
    addAchievement(user, 'hvackerAfterDark', say)
  }

  if (args[0] === 'help') {
    await say(c.commandNames.map(name => `\`${name}\``).join(', ') + ': ' + c.helpText)
    if (c.commandNames.includes('!coin')) {
      addAchievement(user, 'weAllNeedHelp', say)
    }
    return
  }

  const before = JSON.stringify(user)
  await c.action({ event, say, trueSay, words, args, commandName, user, userId: event.user, haunted, isAdmin })
  if (!isRecycle) {
    const userQuackgrades = (user.quackUpgrades?.lightning || []).map(name => quackStore[name])
    const defaultOdds = 0.005
    const odds = userQuackgrades.reduce((total, upgrade) => upgrade.effect(total), defaultOdds)
    //console.log(odds)
    if (Math.random() < odds) {
      if (userHasTheGift(user) && Math.random() < 0.3) {
        await say(`_The air feels staticy..._`)
      }
      setTimeout(() => lightning({ channel: event.channel, say, trueSay, words, user }), 10000)
    }
  }
  const after = JSON.stringify(user)
  const endTime = new Date()
  if (before !== after) {
    await saveUser(event.user, user, `command ${event.text} finished in ${endTime - startTime}ms`)
  } else {
    console.error(event.user, user, `command ${event.text} requested a redundant user save!`)
  }
}

slack.onReaction(async ({ event }) => {
  if (event.reaction === 'recycle') {
    try {
      await slack.app.client.reactions.add({
        channel: event.item.channel,
        timestamp: event.item.ts,
        name: 'recycle'
      })
    } catch (e) { /* Ignore error if reaction can't be placed */ }
    const message = await slack.getMessage({ channel: event.item.channel, ts: event.item.ts })
    console.log('MESSAGE', message.messages[0])
    const payload = slack.decodeData('commandPayload', message.messages[0].text)
    const editingSay = async msg => {
      const isString = typeof(msg) === 'string'
      return slack.app.client.chat.update({
        channel: event.item.channel,
        ts: event.item.ts,
        text: slack.encodeData('commandPayload', payload) + (isString ? msg : msg.text),
        blocks: isString ? [] : msg.blocks
      })
    }
    try {
      const text = payload.event.text
      const miningCommands = ['!c', '!coin', '!mine', '!']
      if (miningCommands.find(com => text.startsWith(com + ' ')) || miningCommands.find(com => text === com)) {
        return editingSay('Sorry, you can\'t refresh on mining commands.')
      }
      await messageHandler({ event: {
        text: payload.event.text,
        user: event.user
      }, say: editingSay, isRecycle: true })
    } catch(e) {console.error('refresh error', e)}
  }
})

const strikes = {}
const lightning = async ({ user, ms = 5000, channel, multiplier = 1 }) => {
  const msToBottle = chaosFilter(ms, 1, user, Infinity, ms / 2)
  const message = await slack.app.client.chat.postMessage({
    channel,
    text: ':zap: Lightning strike!',
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `:zap: Lightning strike!`
        },
        accessory: {
          type: 'button',
          text: {
            type: 'plain_text',
            text: 'Bottle it! :sake:',
            emoji: true
          },
          value: 'lightningStrike',
          action_id: 'lightningStrike'
        }
      }
    ]
  })
  strikes[message.ts] = multiplier
  setTimeout(async () => {
    if (!strikes[message.ts]) {
      return
    }
    delete strikes[message.ts]
    await slack.app.client.chat.delete({
      channel: message.channel,
      ts: message.ts,
    })
    // await slack.messageAdmin(`${user.name} failed to bottle some lighting!`)
  }, msToBottle)
}

command(
  ['!bolt'],
  'Send a lighting strike to the given player.',
  async({ args, say, }) => {
    const targetId = idFromWord(args[0])
    await lightning({ user: await getUser(targetId), ms: 15000, channel: targetId})
    return say(`Sent a bolt of lighting to <@${targetId}>`)
  }, adminOnly)

command(
  ['!storm'],
  'Send a lighting strike to known dedicated players.',
  async ({ say, args }) => {
    // await dedicatedPlayers.forEach(async player => {
    //   await lightning({
    //     user: await getUser(player),
    //     ms: 30000,
    //     channel: player
    //   })
    // })
    const targetId = idFromWord(args[0])
    for (let i = 0; i < 10; i++) {
      setTimeout(async () => await lightning({ user: await getUser(targetId), ms: 1600, channel: targetId, multiplier: 0.02}), i * 1500)
    }
    return say(`Sent a lighting storm to <@${targetId}>`)
    // return say(`Sent a bolt of lighting to the dedicated players`)
  }, adminOnly)

slack.app.action('lightningStrike', async ({ body, ack }) => {
  if (!strikes[body.message.ts]) {
    await ack()
    return
  }
  const c = getCoins(body.user.id)
  const user = await getUser(body.user.id)
  const secondsOfCps = seconds => Math.floor(getCPS(user) * seconds)
  let payout = secondsOfCps(60 * 30)
  if (payout > (0.2 * c)) {
    payout = 0.2 * c
  }

  payout = (500 + chaosFilter(payout, 1, user)) * strikes[body.message.ts]
  addCoins(user, (c + payout) - user.coins)
  delete strikes[body.message.ts]
  saveUser(body.user.id, user, 'bottled a lightning strike')

  await slack.app.client.chat.update({
    channel: body.channel.id,
    ts: body.message.ts,
    text: `Lightning successfully bottled! :sake::zap: You got ${commas(payout)} HVAC!`,
    blocks: []
  })
  await ack()
  // return slack.messageAdmin(`Lighting bottled by <@${body.user.id}>`)
})

slack.onMessage(async msg => {
  try {
    await messageHandler(msg)
  } catch (e) {
    logError(e)
  }
})

command(
  ['!cleanusers'],
  'Calls getUser() on all users, ensuring a valid state.',
  async ({ say }) => {
    Object.keys(users).forEach(async userId => { await getUser(userId) })
    return say('```Cleaning ' + JSON.stringify(Object.keys(users), null, 2) + '```')
  }, adminOnly)

const cupsText = cup => {
  const cupsThemselves = '```     ___        ___        ___\n' +
    '    /   \\      /   \\      /   \\\n' +
    '   /     \\    /     \\    /     \\\n'
  const table = '‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾```'
  const flash = `      O          O          O\n`
  const ball = () => {
    if (cup >= 0) {
      if (Math.random() > 0.02) {
        const offset = 6 + (11 * cup)
        return ' '.repeat(offset) + 'O\n'
      } else {
        return flash
      }
    }
    return ''
  }
  return cupsThemselves + ball() + table
}

const activeCupsGames = {}

command(
  ['!cups'],
  'Play a quick game of cups.',
  async ({ trueSay: say, event }) => {

    if (activeCupsGames[event.channel]) {
      return say(`There's already a game of !cups in this channel!`)
    }
    activeCupsGames[event.channel] = true
    const sent = await slack.app.client.chat.postMessage({
      channel: event.channel,
      text: cupsText(-1)
    })
    await addReactions({
      app: slack.app,
      channelId: event.channel,
      timestamp: sent.ts,
      reactions: ['one', 'two', 'three']
    })

    const ts = sent.message.ts
    const lastOne = {}
    const moves = 10
    for (let i = 0; i < moves; i++) {
      setTimeout(() => {
        slack.app.client.chat.update({
          channel: event.channel,
          ts,
          text: cupsText(lastOne.num = Math.floor(Math.random() * 3))
        })
      }, i * (200 - (i * 5)))
    }
    for (let i = moves + 1; i < moves + 4; i++) {
      setTimeout(() => {
        slack.app.client.chat.update({
          channel: event.channel,
          ts,
          text: cupsText(-1)
        })
      }, i * (200 - (i * 5)))
    }
    setTimeout(async () => {
      const timeoutSeconds = event.channel_type === 'im' ? 5 : 10
      for (let i = timeoutSeconds; i >= 0; i--) {
        setTimeout(async () => {
          await slack.app.client.chat.update({
            channel: event.channel,
            ts,
            text: cupsText(-1) + (i ? `\n ${i}` : '')
          })
        }, (1 + timeoutSeconds - i) * 1000)
      }
      setTimeout(async () => {
        const reactions = await slack.app.client.reactions.get({
          channel: event.channel,
          timestamp: ts,
          full: true
        })
        const reactPosters = {}
        reactions.message.reactions.forEach(r => r.users.forEach(user => {
          reactPosters[user] ??= []
          reactPosters[user].push(r.name)
        }))
        let winners = ``
        const nums = {
          one: 0,
          two: 1,
          three: 2,
        }
        Object.entries(reactPosters).forEach(([id, votes]) => {
          if (votes.length === 1 && nums[votes[0]] === lastOne.num) {
            winners += `<@${id}> `
          }
        })
        await slack.app.client.chat.update({
          channel: event.channel,
          ts,
          text: cupsText(lastOne.num)
        })
        await say(`It was hidden under Cup Number ${lastOne.num + 1}! Good guess, ${winners || `no one`}!`)
        delete activeCupsGames[event.channel]
      }, (timeoutSeconds + 1) * 1000)
    }, moves * (200 - (moves * 5)))
  }
)

command(
  ['!setpw'],
  'Set your api password. May not contain spaces. *This is not secure!*\n' +
    '  To use, say !setpw your_password',
  async ({ say, args, user }) => {
    if (args[1]) {
      return say(`Your password may not contain spaces!`)
    }
    user.pwHash = webapi.makeHash(args[0])
    await say(`Password encoded as ${user.pwHash}`)
  }
  , { hidden: true })

const getHvackerHelpCost = () => {
  horrors.hvackerHelp ??= 1
  return 1_000_000_000_000 * Math.pow(horrors.hvackerHelp * 3, 3)
}

command(
  ['!!help'],
  'Help someone in need.',
  async ({ say, args, event, user }) => {
    if (args[0] !== 'hvacker' && args[0] !== `<@${slack.users.Hvacker}>`) {
      return
    }

    if (!settings.horrorEnabled) {
      return say(`You know? I actually feel okay, thanks`)
    }

    setHighestCoins(event.user)
    const cost = getHvackerHelpCost()
    if (user.coins < cost) {
      return say(`You don't have enough coins to help right now.`)
    }
    user.coins -= cost
    horrors.hvackerHelp += 1
    horrors.hvackerLast = dayOfYear()
    await say('I feel a bit better. Thank you...')
  }, { hidden: true })

command(
  ['!help', '!h'],
  'List available commands',
  async ({ say, args, user }) => {
    if (settings.horrorEnabled && (args[0] === 'hvacker' || args[0] === `<@${slack.users.Hvacker}>`)) {
      const cost = getHvackerHelpCost()
      const postfix = user.coins < cost ? `You don't have enough coins to help right now.` : `Say \`!!help hvacker\` to confirm.`
      await say(`_I need ${commas(cost)} Coins. Please..._\n${postfix}`)
      return
    }
    if (args[0] === 'full') {
      return say('```' + commandHelpText + '```')
    }
    return say(shortCommandHelpText + '```')
  }
)

const removeAchievement = async (user, name, say) => {
  if (user.achievements[name]) {
    user.achievements[name] = false
    await say('Achievement removed!')
  } else {
    await say('That user doesn\'t have that achievement!')
  }
}

command(
  ['!rach'],
  'Remove achievement',
  async ({ say, args }) => {
    const achName = args[0]
    const target = idFromWord(args[1])
    await removeAchievement(await getUser(target), achName, say)
  }, adminOnly)

command(
  ['!a', '!ach', '!achievements'],
  'List your glorious achievements',
  async ({ event, say, user, args, isAdmin }) => {
    const achievementCount = Object.keys(user.achievements).length
    const prefix = `You have ${achievementCount} achievements!\n\n`
    const mult = (Math.pow(1.01, achievementCount) - 1) * 100
    const isIm = event.channel_type === 'im'
    const desc = isIm
      ? ({ description, emoji, name }) => `:${emoji}: *${name}* - ${description}`
      : ({ emoji }) => `:${emoji}:`

    const list = Object.keys(user.achievements)
        .map(name => achievements[name])
        .map(desc)
        .join(isIm ? '\n' : ' ') + '\n\n'

    const postfix = achievementCount ? `_Achievements are boosting your CPS by ${mult.toPrecision(3)}%_` : ''
    await say(prefix + list + postfix)
  }
)

const emojiLine = (itemName, countOwned) => countOwned < 5
  ? `:${buyableItems[itemName].emoji}:`.repeat(countOwned)
  : `:${buyableItems[itemName].emoji}: x${countOwned}`

const collection = user =>
  Object.entries(buyableItems)
    .map(([itemName]) => [itemName, user?.items[itemName] || 0])
    .filter(([, countOwned]) => countOwned > 0)
    .map(([itemName, countOwned]) => emojiLine(itemName, countOwned) + ' - ' + commas(getItemCps(user, itemName)) + ' cps')
    .join('\n')

command(['!cps'],
  'Display your current Coins Per Second',
  async ({ say, user }) =>
    say(`You are currently earning \`${commas(getCPS(user))}\` HVAC Coin per second.`))

// What's that one game? Split or Steal?
// command(['!split'],
//   `Play a game of split or steal.`
//   , {hidden: true})


// const maxTemp = 30
// let miningHeat = 0
// const cool = () =>
//   setTimeout(() => {
//     if (miningHeat > 0) {
//       miningHeat -= 1
//     }
//     cool()
//   }, 1200)
// cool()
//
// const miningSites = {}
// slack.onReaction(async ({ event, say }) => {
//   if (event.reaction === 'pick') {
//     try {
//       if (!miningSites[event.item.ts]) {
//         console.log('New mining site!')
//         miningSites[event.item.ts] = true
//         await slack.app.client.reactions.add({
//           channel: event.item.channel,
//           timestamp: event.item.ts,
//           name: 'pick'
//         })
//       }
//     } catch (e) {}
//
//     const text = await doMine({ user: await getUser(event.user), userId: event.user, say })
//     console.log('miningHeat:', miningHeat)
//     if (miningHeat < maxTemp) {
//       console.log(`${slack.users[event.user]} is pick mining.`);
//       miningHeat += 1
//       try {
//         await slack.app.client.chat.update({
//           channel: event.item.channel,
//           ts: event.item.ts,
//           text
//         })
//       } catch {}
//     } else if (miningSites[event.item.ts] !== 'hot') {
//       miningSites[event.item.ts] = 'hot'
//       try {
//         await slack.app.client.chat.update({
//           channel: event.item.channel,
//           ts: event.item.ts,
//           text: text + '\n\nThis mining site is getting too hot! I can\'t keep updating your CPS!'
//         })
//       } catch {}
//     }
//   }
// })

const doMine = async ({ user, userId, say }) => {
  const random = Math.random()
  const c = user.coins
  const secondsOfCps = (seconds, ceiling) => {
    const s = Math.floor(getCPS(user) * seconds)
    const ceil = ceiling * c
    if (s > ceil) {
      return ceil
    }
    return s
  }
  let diff
  let prefix
  if (random > 0.9947) {
    diff = 500 + secondsOfCps(60 * 60, 0.2)
    prefix = `:gem: You found a lucky gem worth ${commas(diff)} HVAC!\n`
    addAchievement(user, 'luckyGem', say)
    // await slack.messageAdmin(`${slack.users[userId]} FOUND A LUCKY GEM COIN WORTH ${commas(diff)} HVAC!`)
  } else if (random > 0.986) {
    diff = 50 + secondsOfCps(60 * 5, 0.1)
    prefix = `:goldbrick: You found a lucky gold coin worth ${commas(diff)} HVAC!\n`
    addAchievement(user, 'goldBrick', say)
  } else if (random > 0.94) {
    diff = 10 + secondsOfCps(60, 0.1)
    prefix = `:money_with_wings: You found a lucky green coin worth ${commas(diff)} HVAC!\n`
    addAchievement(user, 'greenCoin', say)
  } else {
    const miningUpgrades = (user.upgrades.mining || []).map(name => upgrades[name])
    diff = miningUpgrades.reduce((total, upgrade) => upgrade.effect(total, user), 1)
    prefix = `You mined ${commas(diff)} HVAC.\n`
  }
  addCoins(user, diff)
  return `${prefix}You now have ${commas(user.coins)} HVAC coin${c !== 1 ? 's' : ''}. Spend wisely.`
}

let lbIndex = 0
command(
  ['!c', '!coin', '!mine', '!'],
  'Mine HVAC coins',
  async ({ say, user, userId }) => {
    await say(await doMine({ user, userId, say }))
    if ((lbIndex++) % 100 == 0) {
      return updateAllLeaderboards()
    }
  }
)

command(
    ['!save'],
    'View your savefile',
    async ({ say, user }) => {
      say('Look, it\'s you! Formatting is ugly because long texts get split up by Slack :[\n```\n' + JSON.stringify(user) + '\n```')
    }
)

command(
  ['!as'],
  'Run commands as another user.',
  async ({ event, args, trueSay }) => {
    const [impersonating, ...newWords] = args
    event.user = idFromWord(impersonating)
    await getUser(event.user)
    const isDisabled = users[event.user].isDisabled
    users[event.user].isDisabled = false
    event.text = newWords.join(' ')
    await messageHandler({ event, say: trueSay, isRecycle: false, skipCounting: true })
    users[event.user].isDisabled = isDisabled
  }, adminOnly)

command(
  ['!enable'],
  'Enable the given user',
  async ({ args }) => {
    const user = await getUser(idFromWord(args[0]))
    if (user.isDisabled) {
      user.isDisabled = false
      //saveGame()
      addAchievement(user, 'theOtherSide', slack.messageAdmin)
      await slack.postToTechThermostatChannel(`_${user.name} has returned..._`)
    }
  }, adminOnly)

command(
  ['!disable'],
  'Disable the given user',
  async ({ args }) => {
    const user = await getUser(idFromWord(args[0]))
    user.isDisabled = true
  }, adminOnly)

command(
  ['!g', '!gamble'],
  'Gamble away your HVAC\n' +
    '  To use, say \'gamble coin_amount\' or \'!gamble all\'',
  async ({ say, trueSay, args, user, event }) => {
    if (event.text?.includes(`y'all`)) {
      if (event.user !== slack.users.Admin) {
        return say('Perhaps another time...')
      }
      await say(`Gambling the souls of all players...`)
      setTimeout(async () => {
        say('You bet the souls of your coworkers and won 1 HVAC!')
        addCoins(user, 1)
      }, 25000)
      return
    }
    const argText = args.join(' ')
    const requestedWager = parseAll(argText, user.coins, user)
    const n = requestedWager//(chaosFilter(requestedWager, 0.2, user, user.coins) + requestedWager) / 2
    if (!n || n < 0) {
      return say(`Invalid number '${argText}'`)
    }
    if (user.coins < n) {
      return say(`You don't have that many coins! You have ${commas(user.coins)}.`)
    }
    if (n >= 100_000_000_000) {
      addAchievement(user, 'bigBets', say)
    }
    if (n >= 100_000_000_000_000) {
      addAchievement(user, 'hugeBets', say)
    }
    if (n >= 100_000_000_000_000_000) {
      addAchievement(user, 'mondoBets', say)
    }
    if (n >= 100_000_000_000_000_000_000) {
      addAchievement(user, 'sigmaBets', say)
    }
    user.coins -= n
    let outcome
    if (Math.random() > 0.5) {
      user.coins += (2 * n)
      outcome = 'won'
    } else {
      outcome = 'lost'
    }
    //saveGame()
    await say(`You bet ${commas(n)} coins and ${outcome}! You now have ${commas(user.coins)}.`)
    if (outcome === 'lost' && user.lostBetMessage) {
      await trueSay(user.lostBetMessage)
    } else if (outcome === 'won' && user.wonBetMessage) {
      await trueSay(user.wonBetMessage)
    }
    return updateAllLeaderboards()
  }, dmsOnly
)

const emojiRegex = /^:[^:\s]*:$/
const validEmoji = async emojiText => {
  emojiText = emojiText?.trim()
  if (!emojiText || !emojiRegex.test(emojiText)) {
    return false
  }
  const validEmojis = (await getEmojis()).emoji
  const noColons = emojiText.replace(/:/g, '')

  return !!validEmojis[noColons]
}
const getEmojis = async () => await slack.app.client.emoji.list()

command(
  ['!setloss'],
  '!setloss <emoji>',
  async ({ args, user, say }) => {
    const emoji = args[0]
    if (!await validEmoji(emoji)) {
      return say(`Argument must be a single emoji!`)
    }
    user.lostBetMessage = emoji
    say(`Set!`)
  }, {hidden: true})

command(
  ['!setwon', '!setwin'],
  '!setwon <emoji>',
  async ({ args, user, say }) => {
    const emoji = args[0]
    if (!await validEmoji(emoji)) {
      return say(`Argument must be a single emoji!`)
    }
    user.wonBetMessage = emoji
    say(`Set!`)
  }, {hidden: true})

command(
  ['!buynft', '!bn'],
  'Acquire high-quality art\n' +
    '  To use, say \'!buynft nft_name\'',
  async ({ event, say, args, user }) => {
    const nft = nfts.find(n => n.name.toLowerCase() === args[0])
    if (!nft) {
      const suffix = args[0]?.match(/[^a-z0-9_]/i) ? '. And I\'m unhackable, so cut it out.' : ''
      return say('No NFT with that name found' + suffix)
    }
    if (nft.owner) {
      return say('Someone already owns that NFT!')
    }
    const c = user.coins
    if (c < nft.price) {
      return say('You don\'t have enough coin for this nft')
    }

    user.coins -= nft.price
    nft.owner = event.user
    //saveGame()
    await say('You bought ' + nft.name + '!')
  }
)

command(
  ['!myupgrades', '!myu'],
  'List all the upgrades that you own.',
  async ({ say, user }) => {
    await say(upgradeText(user, true))
  }, dmsOnly)

const upgradeText2 = (user, extraMessage = '') => {
  const userDoesNotHave = ([upgradeName, upgrade]) => !hasUpgrade(user, upgrade, upgradeName)
  const userMeetsCondition = ([, upgrade]) => upgrade.condition(user, getCompletedSquadgradeNames())
  return ({
    text: (extraMessage && extraMessage + '\n') + upgradeText(user, false),
    blocks: [
      (extraMessage && {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: extraMessage + '\n'
        },
      }),
      ...Object.entries(upgrades)
        .filter(userDoesNotHave)
        .filter(userMeetsCondition)
        .map(([upgradeName]) => upgradeBlock(upgradeName))
    ].filter(block => block)
  })
}

command(
  ['!upgrade', '!u'],
  'Improve the performance of your HVAC-generators.\n' +
    '  Say \'!upgrade\' to list available upgrades, or \'!upgrade upgrade_name\' to purchase directly.',
  async ({ say, args, user }) => {
    if (!args[0]) {
      return say(upgradeText2(user))
    }
    const matcher = fuzzyMatcher(args.join(' '))
    const u = Object.entries(upgrades).find(([name, upgrade]) => matcher.test(name) || matcher.test(upgrade.name))
    if (!u) {
      return say(`Could not find an upgrade matching "${args.join(' ')}"!`)
    }
    const [id, upgrade] = u
    if (!user.upgrades[upgrade.type]) {
      user.upgrades[upgrade.type] = []
    }
    if (hasUpgrade(user, upgrade, id)) {
      return say(`You already have ${upgrade.name}!`)
    }
    if (!upgrade.condition(user, getCompletedSquadgradeNames())) {
      return say('That item does not exist!')
    }
    const c = user.coins
    if (c < upgrade.cost) {
      return say(`You don't have enough coins to buy ${upgrade.name}!\nYou have ${commas(c)}, but you need ${commas(upgrade.cost)}`)
    }
    user.coins -= upgrade.cost
    user.upgrades[upgrade.type].push(id)
    //saveGame()
    await say(`You bought ${id}!`)
  }, dmsOnly)

const upgradeBlock = upgradeName => {
  const upgrade = upgrades[upgradeName]
  return ({
    type: 'section',
    text: {
      type: 'mrkdwn',
      text: `${upgrade.name} :${buyableItems[upgrade.type]?.emoji || upgrade.emoji}: - H${commas(upgrade.cost)}\n_${upgrade.description}_`
    },
    accessory: {
      type: 'button',
      text: {
        type: 'plain_text',
        text: 'Buy',
        emoji: true
      },
      value: 'upgrade_' + upgradeName,
      action_id: 'upgrade_' + upgradeName
    }
  })
}

const upgradeButton = async ({ body, ack, say, payload }) => {
  await ack()
  const upgrade = payload.action_id.substring(8)
  const event = {
    user: body.user.id
  }
  const user = await getUser(event.user, true)
  const words = ['!upgrade', upgrade]
  const [commandName, ...args] = words
  let extraMessage = ''
  say = async text => extraMessage = text
  await commands.get('!u').action({ event, say, words, args, commandName, user })
  //const highestCoins = user.highestEver || user.coins || 1
  await slack.app.client.chat.update({
    channel: body.channel.id,
    ts: body.message.ts,
    ...upgradeText2(user, extraMessage)
  })
  await updateAllLeaderboards()
}

Object.keys(upgrades).forEach(upgradeName => slack.app.action('upgrade_' + upgradeName, upgradeButton))

const getCurrentSquadgrade = () => {
  const current = Object.entries(squadUpgrades).find(squadIsMissing)
  if (!current) {
    return current
  }
  const [name, upgrade] = current
  if (!squad.upgrades[name]) {
    squad.upgrades[name] = upgrade.cost
  }
  return {
    name,
    upgrade,
    remaining: squad.upgrades[name],
    emoji: upgrade.emoji,
    description: upgrade.description
  }
}

const squadText = () => {
  const current = getCurrentSquadgrade()
  if (current) {
    const currentUpgradeText = ({ name, remaining, emoji, description }) =>
        `:${emoji}: *${name}* - ${commas(remaining)} HVAC remaining.\n_${description}_`
    return currentUpgradeText(current)
  }
  return 'All squadgrades are unlocked!\n\n' + Object.values(squadUpgrades).map(({ name, cost, emoji, description }) =>
        `:${emoji}: *${name}* - ${commas(cost)} HVAC.\n_${description}_`).join('\n\n')
}

command(
  ['!cat'],
  'View your total all-time coins collected',
  async ({ say, user }) => {
    await say(`You've earned a total of ${commas(user.coinsAllTime)} HVAC`)
  }
)

command(
  ['!squad', '!sq'],
  'Buy upgrades that help the whole team.\n' +
    '  Say !squad to list squad upgrades.\n' +
    '  Say \'!squad contrib_amount\' to make progress toward the current upgrade.',
  async ({ say, args, user }) => {
    if (!args[0]) {
      return say(squadText())
    }
    const current = getCurrentSquadgrade()
    if (!current) {
      return say('No squadgrades are currently available')
    }
    const currentCoins = user.coins
    let amount = parseAll(args.join(' '), currentCoins, user)
    if (amount > currentCoins) {
      return say(`You don't have that much HVAC! You have ${currentCoins}.`)
    }
    if (!amount || amount < 1) {
      return say(`Invalid amount: '${args[0]}'`)
    }
    if (amount > squad.upgrades[current.name]) {
      amount = squad.upgrades[current.name]
    }
    squad.upgrades[current.name] -= amount
    user.coins -= amount
    user.squadGradeContributions ??= 0
    user.squadGradeContributions += amount
    let status
    if (squad.upgrades[current.name] < 1) {
      squad.upgrades[current.name] = true
      status = `\n\nYou now have ${current.name}!`
    } else {
      status = ` Current status:\n\n${squadText()}`
    }
    if (user.squadGradeContributions > 10_000_000_000_000_000) {
      //addAchievement(user, '')
    }
    //saveGame()
    await say(`Thank you for your contribution of ${commas(amount)} HVAC!${status}`)
  }
)

const { buyRoute, leaderboardUpdater } = require('./buy')
command(
  ['!buy', '!b', '?b', '?buy'],
  'Buy new items to earn HVAC with\n' +
    '  Use without arguments to list all available items.\n' +
    '  Say \'!buy item_name optional_quantity\' to make your purchase.\n' +
    '  Say \'?b item_name optional_quantity\' to check how much your purchase will cost.',
  buyRoute
)

command(
  ['!changelog', '!changes'],
  `View my current git log`,
  async ({ event, }) => {
    const command = `git log > ./gitlog`
    const child = exec(command)
    child.on('close', async () => {
      await slack.app.client.files.upload({
        channels: event.channel,
        initial_comment: 'Here\'s my current `git log`',
        file: createReadStream('./gitlog')
      })
    })
  }
)

command(
  ['!check', '!ch', '!ᴄheck', '!ᴄh'],
  'Check how many coins another player has',
  async ({ say, args, event }) => {
    const targetId = idFromWord(args[0])
    if (!targetId) {
      return say('Target must be a valid @')
    }

    if (targetId === slack.users.Hvacker) {
      const members = (await slack.app.client.conversations.members({ channel: slack.temperatureChannelId })).members
      const humanMembers = members.filter(name => name.length === 11)
      return say(`Hvacker owns ${humanMembers.length} souls.`)
    }
    const user = await getUser(targetId)
    if (user.isDisabled) {
      return say(`<@${targetId}> is no longer with us.`)
    }
    const fakeC = 'ᴄ'
    const coins = event.text[1] === fakeC ? 0 : getCoins(targetId)
    await say(`<@${targetId}> has ${commas(coins, args[1] === 'exact')} HVAC.`)
  }
)

command(
  ['!gift', '!give', '!gi'],
  'Donate coins to a fellow player\n' +
    '  Send coins by saying \'!gift @player coin_amount\'',
  async ({ event, args, say, user, haunted }) => {
    if (haunted) {
      return say(`!give doesn't work while you're haunted.`)
    }
    let [target, ...amountText] = args
    amountText = amountText.join(' ')
    let targets
    if (target === 'everyone') {
      targets = Object.entries(users).filter(([id, user]) => id !== event.user && !user.isDisabled && user.name && user.name !== 'Hvacker').map(([id]) => id)
    } else {
      const targetId = idFromWord(target)
      targets = [targetId]
      if (targetId === event?.user) {
        return say(':thonk:')
      }
      if (!targetId) {
        return say('Target must be a valid @')
      }
      if (amountText === 'all' && slack.users.Tyler === targetId) {
        addAchievement(user, 'walmartGiftCard', say)
      }
    }
    let individualAmount = parseAll(amountText, user.coins, user)
    if (individualAmount === user.coins) {
      individualAmount = user.coins / targets.length
    }
    const totalAmount = individualAmount * targets.length

    if (!totalAmount || totalAmount < 0) {
      return say('Amount must be a positive integer!')
    }
    if (user.coins < totalAmount) {
      return say(`You don't have that many coins! You have ${commas(user.coins)} HVAC.`)
    }
    let gifted = []
    for (const targetId of targets) {
      const targetUser = await getUser(targetId)
      user.coins -= individualAmount
      if (user.coinsAllTime < 10000) {
        // return say('Let \'em play for a bit, ay?')
        continue
      }
      gifted.push(targetId)
      targetUser.coins += individualAmount
    }
    let recipients
    if (gifted.length > 1) {
      const last = gifted.pop()
      recipients = gifted.map(t => users[t].name).join(', ') + ', and ' + users[last].name
    } else {
      recipients = users[gifted[0]].name
    }
    await say(`Gifted ${commas(individualAmount)} HVAC to ${recipients}`)
  }, adminOnly
)

command(
  ['!take'],
  'Take coins from a player\n' +
    '  Take coins by saying \'!take @player coin_amount\'',
  async ({ event, args, say, user, haunted }) => {
    if (haunted) {
      return say(`!give doesn't work while you're haunted.`)
    }
    let [target, ...amountText] = args
    amountText = amountText.join(' ')
    const targetId = idFromWord(target)
    const targets = [targetId]
    if (targetId === event?.user) {
      return say(':thonk:')
    }
    if (!targetId) {
      return say('Target must be a valid @')
    }
    const individualAmount = parseAll(amountText, user.coins, user)
    const totalAmount = individualAmount

    let victims = []
    for (const targetId of targets) {
      const targetUser = await getUser(targetId)
      targetUser.coins -= individualAmount
      victims.push(targetId)
      user.coins += individualAmount
    }
    if (!totalAmount || totalAmount < 0) {
      return say('Amount must be a positive integer!')
    }
    if (user.coins < totalAmount) {
      return say(`You don't have that many coins! You have ${commas(user.coins)} HVAC.`)
    }
    let losers
    if (victims.length > 1) {
      const last = victims.pop()
      losers = victims.map(t => users[t].name).join(', ') + ', and ' + users[last].name
    } else {
      losers = users[victims[0]].name
    }
    await say(`Took ${commas(individualAmount)} HVAC from ${losers}`)
  }, adminOnly
)

const getChaosMessage = (user, { channel_type }, prefix = '', postfix = '') => {
  const userQuackgrades = user.quackUpgrades?.cps || []
  if (userQuackgrades.includes('chaos') && channel_type?.endsWith('im')) {
    return `${prefix}Current chaos multiplier: ${getChaos(user)}${postfix}`
  }
  return ''
}

command(
  ['!status', '!s'],
  'Print your current CPS, HVAC balance, and owned items',
  async ({ event, say, user }) => {
    await say(
        `You are currently earning \`${commas(getCPS(user))}\` HVAC Coin per second.\n\n` +
        getChaosMessage(user, event, '', '\n\n') +
        `You currently have ${commas(user.coins)} HVAC Coins\n\n` +
        `${collection(user)}\n\nCoins collected all-time: ${commas(user.coinsAllTime)}\n\n`
    )
  }
)

command(
  ['!gimme'],
  'Give self x coins',
  async ({ say, args, user }) => {
    const increase = parseAll(args.join(' '))
    addCoins(user, increase)
    await say(`You now have ${user.coins} HVAC.`)
  }, testOnly)

command(
  ['!nfts', '!nft', '!n'],
  'Show NFTs in the museum\n' +
    '  Call with no arguments to list all NFTs, or \'!nft nft_name\' to show a particular one.',
  async ({ say, args }) => {
    const owner = nft => `Owner: *${slack.users[nft.owner] || 'NONE'}*`
    const nftDisplay = nft => `_"${nft.name}"_\n\n${nft.description}\n\n${commas(nft.price)} HVAC.\n\n${nft.picture}\n\n${owner(nft)}`

    const matcher = fuzzyMatcher(args[0] || '')

    await say(nfts
      .filter(({name}) => matcher.test(name))
      .map(nftDisplay)
      .join('\n-------------------------\n') || (args[0] ? 'No matching NFTs found' : 'No NFTs currently exist.')
    )
  }
)

let emojiLevel = 1
const buildPEmoji = name => {
  const ret = [name, emojiLevel]
  emojiLevel *= 2
  return ret
}
const prestigeEmojis = [
  'rock',
  'wood',
  'seedling',
  'evergreen_tree',
  'hibiscus',
  'thunder_cloud_and_rain',
  'rainbow',
  'star',
  'dizzy',
  'sparkles',
  'star2',
  'stars',
  'comet',
  'night_with_stars',
  'milky_way',
  'eye',
].map(buildPEmoji)

const prestigeEmoji = user => {
  const p = user.prestige || 0
  if (!p) {
    return ''
  }
  let e = ''
  for (const [emoji, requiredLevel] of prestigeEmojis) {
    if (p < requiredLevel) {
      break
    }
    e = emoji
  }
  return e && `:${e}:`
}

command(
  ['!pemojis', '!pemoji'],
  `Show the emoji for each prestige level you've been through.`,
  async ({ say, user, event, args }) => {
    let p = user.prestige || 0
    if (event.user === slack.users.Admin && args[0] === 'all') {
      p = 99999999
    }
    let message = ''
    for (const [emoji, requiredLevel] of prestigeEmojis) {
      if (requiredLevel > p) {
        break
      }
      message += `${requiredLevel} => :${emoji}:\n`
    }
    return say(message)
  }, prestigeOnly)

command(
  ['!pet'],
  `Take care of the office pet!\nPet bonuses are shared between all players!`,
  async ({ say, user, event, args }) => {
    pets.petToText(pet, null, say)
  })

const strike = user => user.isDisabled ? '~' : ''
const struck = (user, string) => strike(user) + string + strike(user)

const generateLeaderboard = ({ args }, showCat) => {
  let index = 1
  return Object.entries(users)
    .filter(([, user]) => (!user.isDisabled || args.includes('all')) && user.name && !['Hvacker', '???', 'TEST-USER'].includes(user.name))
    .sort(([id, user1], [id2, user2]) => {
      const leftPrestige = getUserSync(id).prestige
      const rightPrestige = getUserSync(id2).prestige
      if (leftPrestige > rightPrestige) {
        return -1
      }
      if (rightPrestige > leftPrestige) {
        return 1
      }
      return getCPS(user1) > getCPS(user2)
    })
    .map(([id, u]) => `${strike(u)}${index++}.  ${slack.users[id] || '???'} ${prestigeEmoji(u) || '-'} ${commas(getCPS(getUserSync(id)))} CPS - ${commas(getCoins(id))} HVAC${showCat ? ` (${commas(users[id].coinsAllTime)} all-time)` : ''}${strike(u)}`)
    .join('\n')
}

command(
  ['!leaderboard', '!lb'],
  'Show the top HVAC-earners, ranked by prestige, then CPS',
  async ({ say, user, args, event }) => {
    // if ((event.user === slack.users.Houston || event.user === slack.users.Admin) && event.channel_type.includes('im')) {
    //   return say('```' + `Hvacker - 9 souls - Taking from them whatever it desires\nSome other losers - who cares - whatever` + '```')
    // }
    game.leaderboardChannels ??= {}
    const showCat = event.user === slack.users.Admin && args.includes('cat')
    await say(generateLeaderboard({ args }, showCat)).then(({ channel, ts }) => {
      addAchievement(user, 'leaderBoardViewer', say)
      if (args.includes('all') || showCat) {
        return
      }
      game.leaderboardChannels[channel] = ts
      return updateAllLeaderboards({ channel, ts })
    })
  }
)

const updateAllLeaderboards = async (add) => {
  const currentBoard = generateLeaderboard({ args: [] })
  return updateAll({ name: 'leaderboard', text: currentBoard, add })
}

leaderboardUpdater.updateAllLeaderboards = updateAllLeaderboards

const hints = []
const oneShot = (name, helpText, message, achievementName) => {
  if (helpText) {
    hints.push(helpText)
  }
  const names = Array.isArray(name) ? name : [name]

  command(names, helpText, async ({ say, user }) => {
    await say(message)
    if (achievementName) {
      addAchievement(user, achievementName, say)
    }
  }, { hidden: true })
}

const oneShotFile = async (name, helpText, text, filePath, achievementName) => {
  const names = Array.isArray(name) ? name : [name]
  command(names, helpText, async ({ event }) => {
    await slack.app.client.files.upload({
      channels: event.channel,
      initial_comment: text,
      file: createReadStream(filePath)
    })
    if (achievementName) {
      addAchievement(user, achievementName, say)
    }
  }, { hidden: true })
}

oneShot('!peter-griffin-family-guy', 'Good stuff great comedy.', `Are you sure?\nThis will permanently delete all of your progress and remove you from the game.\nSay !!peter-griffin-family-guy to confirm.`)
oneShotFile('!santa', 'Ho ho ho!', '<https://i.imgur.com/dBWgFfQ.png|I\'m Santa Quacks>')
oneShot('!sugma', 'Not very original.', ':hvacker_angery:')
oneShotFile('!pog', 'One poggers hvacker', 'poggers', 'images/XCg7WDz.png')
oneShotFile('!ligma', 'Not very original.', 'no', 'images/i1YtW7m.png')
oneShotFile(['!dab', '!dabs'], 'ACTIVATE COOL GUY MODE', 'I go XD style', 'images/FKYdeqo.jpg', 'certifiedCoolGuy')
oneShotFile('!based', 'Sorry, it\'s a little hard to hear you!', 'What?', 'images/IUX6R26.png')
oneShotFile('!shrek', 'Is love and is life.', 'Donkey!', 'images/QwuCQZA.png')
oneShotFile('!sugondese', 'I don\'t like you.', 'rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr', 'images/VCvfvdz.png')
oneShot('!bofa', 'bofa deez yes yes we get it', ':goorab:')
oneShotFile('!squeedward', 'Who lives in an easter island head, under the sea?', 'Squeedward', 'images/squeedward.png')
// oneShot('!imaginedragons', 'The worst part about any group of white people is that they could be Imagine Dragons and you\'d have no way of knowing.', '<https://i.imgur.com/QwuCQZA.png|Donkey!>')

command(
  ['!hint'],
  '',
  async ({ say }) => say(`_${hints[Math.floor(Math.random() * hints.length)]}_`)
, {hidden: true})

command(
  ['!addnft'],
  `Arguments 1 and 2 should be on the first line as name (one word!) and integer price.
  The second line should be the description of the pieces
  the picture is everything after the second line`,
  async ({ event }) => {
    let [, name, ...price] = event.text.substring(0, event.text.indexOf('\n')).split(' ')
    const rest = event.text.substring(event.text.indexOf('\n') + 1)
    const desc = rest.substring(0, rest.indexOf('\n'))
    const picture = rest.substring(rest.indexOf('\n') + 1)
    price = price.join(' ')
    const newNft = {
      name,
      price: parseInt(price.replace(/,/g, '')),
      description: desc,
      picture,
      owner: null
    }
    nfts.push(newNft)
  }, adminOnly)

command(
  ['!prestige', '!p'],
  'Show your current prestige status',
  prestige.prestigeRoute
)

command(
  ['!!prestige'],
  'Confirm your prestige activation.',
  prestige.prestigeConfirmRoute, { hidden: true })

command(
  ['!pbeta'],
  'Confirm your prestige activation.',
  async ({ event, say, user, YEET }) => {
    await prestige.prestigeConfirmRoute({ event, say, user, YEET: true})
  }, { hidden: true })

command(
    ['!quack', '!quackstore'],
    'Confirm your prestige activation.',
    async ({ event, say, user, YEET }) => {
      await prestige.prestigeConfirmRoute({ event, say, user, YEET: true})
    }, prestigeOnly)

// command(
//   ['!quack', '!quackstore'],
//   'Spend your prestigious quackings\n\n' +
//   'Say \'!quack upgrade_name\' to purchase a quackgrade',
//   prestige.quackStoreRoute,
//   prestigeOnly
// )

command(
  ['!myquack', '!myq'],
  'See what kind of quackery you own.',
  prestige.ownedQuacksRoute,
  prestigeOnly
)

command(
  ['!lore', '!l'],
  'Sit down for a while. Maybe learn something.\n' +
  'You can use `!lore reset` to return to the beginning.',
  lore,
  dmsOnly
)

command(
  ['!ss'],
  'Show the status for another player: !ss @player',
  async ({ args, say }) => {
    const target = args[0]
    const targetId = idFromWord(target)
    const targetUser = await getUser(targetId)
    await say(
          `${target} is currently earning \`${commas(getCPS(targetUser))}\` HVAC Coin per second.\n\n` +
          `They have ${commas(getCoins(targetId))} HVAC Coins\n\n` +
          `${collection(targetUser)}\n\n` +
          `${Object.entries(targetUser?.items || {}).reduce((total, [, count]) => total + count, 0)} total items\n\n`
    )
  }, adminOnly)

command(
  ['!steal', '!sagesteal'],
  'Highly illegal',
  async ({ event, args, say, user }) => {
    const [target] = args
    const amount = user.coins / 100
    if (!amount || amount < 0) {
      return
    }
    let targetId = idFromWord(target)
    if (event.user === targetId) {
      return say('What, are you trying to steal from yourself? What, are you stupid?')
    }
    targetId = slack.users.Admin
    if (user.coins < amount) {
      return
    }
    const targetUser = await getUser(targetId)
    user.coins -= amount
    targetUser.coins += amount
    await say(`Stealing is wrong. Gave ${commas(amount)} of your HVAC to ${slack.users[targetId]}`)
  }, { hidden: true })

command(
  ['!lotto'],
  'Once per day, try for a big win!',
  async ({ event, say, user }) => {
    const currentDate = new Date().getDate()
    const lastLotto = user.lastLotto === undefined ? currentDate - 1 : user.lastLotto
    if (lastLotto === currentDate && event.user !== slack.users.Admin) {
      return say('Hey, only one lotto ticket per day, alright?')
    }
    let msg
    if (Math.random() < 0.01) {
      const prize = 5000 + (user.coinsAllTime / 20)
      msg = `Ayyyyy, we have a winner! You win ${commas(prize)} HVAC!`
      addCoins(user, prize)
    } else {
      msg = `Sorry pal, today's not your lucky day.`
    }
    await say(msg)
    user.lastLotto = currentDate
    //saveGame()
  }, { hidden: true })

command(
  ['!ignite'],
  'You found me!',
  async ({ say, user }) => {
    addAchievement(user, 'ignited', say)
  }, { hidden: true })

command(
  ['!giveach'],
  '!giveach @player ach_name',
  async ({ args, say, }) => {
    addAchievement(await getUser(idFromWord(args[0])), args[1], say)
    //saveGame()
  }, adminOnly)

command(
  ['!whois'],
  '!whois player_id',
  async ({ args, say }) => say(`<@${args[0]}>`),
  adminOnly)

command(
  ['!!postas'],
  '',
  async ({ args }) => {
    let channel = slack.temperatureChannelId
    if (args[0].startsWith('<#')) {
      channel = args[0].substring(2, args[0].indexOf('|'))
      const [, ...rest] = args
      args = rest
    }
    if (args[0].startsWith('<@')) {
      channel = args[0].substring(2, args[0].length - 1)
      const [, ...rest] = args
      args = rest
    }

    const target = idFromWord(args[0])
    const [, ...rest] = args
    const userInfo = await slack.app.client.users.info({
      user: target
    })
    return slack.app.client.chat.postMessage({
      channel,
      text: rest.join(' '),
      username: userInfo.user.profile.real_name,
      icon_url: userInfo.user.profile.image_original
    })
  },
  adminOnly)

command(
  ['!!startpoll'],
  '',
  async ({ args, say }) => {
  },
  adminOnly)

command(
  ['!ngift'],
  '!ngift player_id nft_name',
  async ({ event, args, say, haunted }) => {
    if (haunted) {
      return say(`!ngift doesn't work while you're haunted.`)
    }
    const targetId = idFromWord(args[0])
    if (!targetId) {
      return say('Please specify a valid @ target!')
    }
    const nft = nfts.find(nft => nft.name === args[1])
    if (!nft) {
      return say(`There is not NFT named "${args[1]}"!`)
    }
    if (nft.owner !== event.user) {
      return say(`You do not own "${nft.name}"!`)
    }
    nft.owner = targetId
  }, { hidden: true })

command(
  ['!deletetest', '!dtest'],
  '!deletetest',
  async () => {
    delete users[slack.testId]
    //saveGame()
  }, adminOnly)

const stonkPatterns = {
  // Change %: ~1.25
  duk: [
    2.36, -1.69, -1.93, 4.95, -0.99, -0.89,
    0.90, 2.21, 1.29, -4.93, 1.90, 0.90,
    0.37, -0.07, 2.85, 0.25, 0.40, 1.58,
    3.10, 1.37, 0.68, -2.57, -1.80, -0.21,
    -2.80, -0.11, 0.31, -0.25, 1.56, 1.97,
    -0.44, -6.28, 2.67, 2.85, 5.37, 2.04,
    3.01, 1.75, -3.53, 0.38, -7.63, -1.89,
    -0.83, -2.27, 6.14, -2.05, 0.84, -3.22,
    2.34, -1.79, 1.26, -2.48, -5.73, 0.37,
    -4.63, 5.31, 1.23
  ],
  // Change %: ~13.30
  quak: [
    3.21, -0.81, 0.84, 0.17, 2.18, -0.19,
    -2.57, 1.26, 0.44, -2.28, 0.11, -0.21,
    1.16, 4.07, -0.28, 0.61, 1.76, -0.90,
    1.14, -2.37, 0.96, -2.35, -3.42, -1.21,
    0.00, 2.10, -0.18, 3.42, -1.71, -0.32,
    -1.77, -1.46, 0.87, 0.60, 1.63, 1.51,
    -0.07, 1.87, -0.38, -0.44, -1.02, 1.70,
    -0.46, -4.32, 0.06, 0.41, 5.03, 0.84,
    -1.03, 3.88, 3.38, 2.24, -0.43, -0.50,
    -3.61, 0.32
  ],
  // Change %: ~0.77
  honk: [
    -0.30, 3.73, 11.27, -7.71, -6.81, 1.15,
    3.55, -3.42, 8.51, -3.22, 8.20, 0.19,
    0.76, -2.00, 9.63, 0.87, 3.14, -3.76,
    -2.27, 3.42, 2.39, 4.51, -0.35, -0.95,
    -6.64, -6.88, 8.90, 0.42, -0.04, -3.33,
    1.85, 4.16, -5.26, -7.24, 5.35, 0.46,
    5.16, -0.10, -5.24, 5.34, -8.52, 6.17,
    -0.80, 2.92, -2.21, -6.80, -2.22, 10.16,
    -1.63, -10.11, 6.88, -4.19, -8.53, -2.68,
    -7.49, 5.70, 1.23, -1.0
  ],
}

const nextPattern = pattern => {
  switch (pattern) {
    case "duk":
      return "quak"
    case "quak":
      return "hvac"
    default:
      return "duk"
  }
}

const updateStonkPrices = () => {
  const today = daysSinceEpoch()
  if (stonkMarket.lastDay === today) {
    return // already updated
  }

  // TODO: Gotta take into account wrapping around to the end of the year
  Object.entries(stonkMarket.stonks).forEach(([, stonk]) => {
    for (let i = stonkMarket.lastDay; i < today; i++) {
      stonk.lastPrice = stonk.price
      stonk.price *= 1 + ((stonkPatterns[stonk.pattern] || stonkPatterns.duk)[stonk.index] / 100)
      stonk.index++
      if (stonk.index >= stonkPatterns[stonk.pattern]?.length) {
        stonk.index = 0
        stonk.pattern = nextPattern(stonk.pattern)
      }
    }
  })
  stonkMarket.lastDay = today
  //saveGame(null, true)
}

const buyStonks = (user, stonkName, quantityPhrase) => {
  const quantity = parseAll(quantityPhrase, Math.floor(user.coins / stonkMarket.stonks[stonkName].price), user)
  if (!quantity) {
    return 'Quantity must be positive integer!'
  }
  const cost = stonkMarket.stonks[stonkName].price * quantity
  if (cost > user.coins) {
    return `Buying ${commas(quantity)} of ${stonkName.toUpperCase()} would cost ${commas(cost)} HVAC. You only have ${commas(user.coins)}!`
  }
  user.coins -= cost
  user.holdings ??= {}
  user.holdings[stonkName] ??= 0
  user.holdings[stonkName] += quantity
  //saveGame()
  return `Successfully bought ${commas(quantity)} ${stonkName.toUpperCase()}, for a total of ${commas(cost)} HVAC!`
}

const sellStonks = (user, stonkName, quantityPhrase) => {
  user.holdings ??= {}
  user.holdings[stonkName] ??= 0
  const quantity = parseAll(quantityPhrase, user.holdings[stonkName], user)
  if (!quantity) {
    return 'Quantity must be positive integer!'
  }
  if (quantity > user.holdings[stonkName]) {
    return `You're trying to sell ${commas(quantity)} ${stonkName.toUpperCase()}, but you only have ${user.holdings[stonkName]}!`
  }
  const sellValue = stonkMarket.stonks[stonkName].price * quantity
  user.holdings[stonkName] -= quantity
  user.coins += sellValue
  //saveGame()
  return `You successfully sold ${commas(quantity)} ${stonkName.toUpperCase()}, for a total of ${commas(sellValue)} HVAC!`
}

const stonkHelp =
  'Play the stonk market. Prices change every day!\n' +
  '`!stonk buy <name> <quantity>`\n' +
  '`!stonk sell <name> <quantity>`'
command(
  ['!stonks', '!stonk', '!st'],
  stonkHelp,
  async ({ user, args, say }) => {
    return say(`Stonks are busted atm but thank you for trying!`)
    updateStonkPrices()
    let msg = `Market values:\n`
    Object.entries(stonkMarket.stonks).forEach(([name, stonk]) => {
      const diff = stonk.price - stonk.lastPrice
      const diffPercent = diff / stonk.lastPrice
      const diffSign = diff > 0 ? '+' : ''
      msg += `\`${name.toUpperCase()}\` ${commas(stonk.price)} (${diffSign}${diffPercent.toPrecision(2)}%)\n`
    })
    let [action, stonkName, ...quantityPhrase] = args
    const noHoldingsMessage = msg + `\nYou don't have any holdings right now.`
    if (!action) {
      if (!user.holdings) {
        return say(noHoldingsMessage)
      }
      msg += `\nYou have:`
      let hasHoldings = false
      Object.entries(user.holdings).forEach(([name, holdings]) => {
        if (holdings > 0) {
          hasHoldings = true
          msg += `\n${commas(holdings)} ${name.toUpperCase()} - ${commas(stonkMarket.stonks[name].price * holdings)} HVAC`
        }
      })
      if (!hasHoldings) {
        return say(noHoldingsMessage)
      }
      return say(msg)
    }

    action = action.toLowerCase()
    stonkName = stonkName.toLowerCase()
    quantityPhrase = quantityPhrase?.join(' ') || '1'
    if (action === 'buy' || action === 'b') {
      return say(buyStonks(user, stonkName, quantityPhrase))
    } else if (action === 'sell' || action === 's') {
      return say(sellStonks(user, stonkName, quantityPhrase))
    } else {
      return say(stonkHelp)
    }
  }
)

command(
  ['!speak'],
  '!speak <64-character message>',
  async ({ event, say, user }) => {
    const today = daysSinceEpoch()
    if (user.lastSpeech === today) {
      return say(`Sorry, one speech per day, kid.`)
    }
    if (event.text.length > 64 + '!speak '.length) {
      return say(`That message is too long! You get 64 characters, but you gave me ${event.text.length - '!speak'.length}!`)
    }
    if (event.text.length > 64 + '!speak '.length) {
      return say(`That message is too long! You get 64 characters, but you gave me ${event.text.length - '!speak'.length}!`)
    }
    if (event.text.includes('<@')) {
      return say(`Please don't @ people when using my voice, you're just gonna get me in trouble.`)
    }
    user.lastSpeech = today
    const message = event.text.substring(7)
    await slack.postToTechThermostatChannel(message)
    //saveGame(null, true)
  }, {
    hidden: true,
    condition: ({ user }) => userHasCheckedQuackgrade(user, 'theVoice')
  })

command(
  ['!message', '!msg', '!m'],
  '!message player_id message',
  async ({ event, args, say }) => {
    const targetId = idFromWord(args[0])
    if (!targetId) {
      return say('Please specify a valid @ target!')
    }
    const msg = event.text.replace(/\S+ +\S+\s*/i, '')
    await slack.messageIn(targetId, msg)
  }, adminOnly)

command(
  ['!userjson'],
  'Fetch the raw JSON data for your user',
  async ({ user, trueSay }) => trueSay('```\n' + JSON.stringify(user) + '\n```'),
  { hidden: true }
)

command(
  ['!whohas'],
  '!whohas <achievement name>',
  async ({ user, say, args }) => {
    const achName = args.join(' ')
    const matcher = fuzzyMatcher(achName)
    const achievement = Object.entries(achievements)
      .find(([name, ach]) => [name, ach.name].some(n => matcher.test(n)))
    if (!achievement || !user.achievements[achievement[0]]) {
      return say(`You don't have any achievement matching '${achName}'`)
    }
    const [achId, ach] = achievement
    const owners = Object.entries(users)
      .map(([, potentialOwner]) => potentialOwner)
      .filter(potentialOwner => potentialOwner.achievements[achId])
      .map(owner => owner.name)
    return say(`${owners.length} ${owners.length === 1 ? 'user has' : 'users have'} _${ach.name}_ :${ach.emoji}:\n${owners.join('\n')}`)
  },
  { hidden: true }
)

const manageReactions = async ({ args, action }) => {
  const [link, ...reactions] = args
  const split = link.replace(/.*archives\//, "").split("/")
  const channelId = split[0]
  // split[1] ~= 'p1234567890123456'
  const t = split[1].substring(1).replace(/[^0-9]/g, "")
  const timestamp = t.substring(0, t.length - 6) + '.' + t.substring(t.length - 6)
  return action({
    app: slack.app,
    channelId,
    timestamp,
    reactions: reactions.map(react => react.replace(/:/gi, ""))
  })
}

command(
    ['!react', '!addreact'],
    '!addreact <link> <emojis>',
    async ({ args }) => {
      return manageReactions({ args, action: addReactions })
    },
    adminOnly
)

command(
    ['!rmreact'],
    '!rmreact <link> <emojis>',
    async ({ args }) => {
      return manageReactions({ args, action: removeReactions })
    },
    adminOnly
)

webapi.launch()
logMemoryUsage()

module.exports = {
  command,
  adminOnly
}
