const { Pool } = require('pg')
const fs = require('fs')
const achievements = require('./achievements')
const buyableItems = require('./buyableItems')
const { quackStore, getChaos } = require('./quackstore')

const dbPool = new Pool()

let jokes
let slackUsers
const setSlackUsers = users => {
  slackUsers = users
}
let upgrades
const setUpgrades = upg => {
  upgrades = upg
}

const saveFile = 'hvacoins.json'
const saveDir = '/hvacker-saves/'

const logError = msg => msg ? console.error('logError: ', msg) : () => { /* Don't log empty message */ }

const loadGame = () => {
  const game = parseOr(fs.readFileSync(saveDir + saveFile, 'utf-8'),
    () => ({
      users: {},
      nfts: [],
      squad: {},
      horrors: {}
    }))
  game.horrors ??= {}
  return game
}

const chaosFilter = (num, odds, user, max = Infinity, min = -Infinity) => {
  const userQuackgrades = user.quackUpgrades?.cps || []
  const hasChaos = userQuackgrades.includes('chaos')
  if (!hasChaos || Math.random() < odds || !num) {
    return num
  }
  const chaosed = num * getChaos(user)
  if (chaosed > max) {
    return max
  }
  if (chaosed < min) {
    return min
  }
  return chaosed
}

const parseOr = (parseable, fallback) => {
  try {
    if (typeof parseable === 'function') {
      parseable = parseable()
    }
    return JSON.parse(parseable)
  } catch (e) {
    logError(e)
    return fallback()
  }
}

let lastBackupTs = 0
const makeBackup = (force) => {
  const currentTs = Date.now()
  if (lastBackupTs > (currentTs - 60000) && !force) {
    return
  }
  lastBackupTs = currentTs
  const fileName = saveDir + 'backups/' + saveFile + new Date().toLocaleString().replace(/[^a-z0-9]/gi, '_')
  console.log(`Making backup file: ${fileName}`)
  fs.writeFileSync(fileName, JSON.stringify(game))
}

const saveUser = async (userId, user, after) => {
  return
  const name = user.name || userId
  if (after) {
    console.log(`SAVING ${name} after ${after}`)
  } else {
    console.log(`SAVING ${name}`, user)
  }
  await dbPool.query(`
    INSERT INTO hvacker_user (slack_id, name, data)
    VALUES ($1, $2, $3)
    ON CONFLICT (slack_id) DO UPDATE
    SET data = EXCLUDED.data`
    , [userId, user.name, user])
    .catch(console.error)
}

let saves = 0
const saveGame = (after, force = true, skipLog = false) => {
  if (saves % 20 === 0) {
    makeBackup()
  }
  saves += 1
  if (force || saves % 10 === 0) {
    if (!skipLog) {
      if (after) {
        console.log(`SAVING GAME after ${after}`)
      } else {
        console.log('SAVING GAME')
      }
    }
    // Object.entries(game.users).forEach(([userId, user]) => saveUser(userId, user))

    fs.writeFileSync(saveDir + saveFile, JSON.stringify(game, null, 2))
  }
}

const maybeNews = say => {
  const random = Math.random()
  if (random > 0.98) {
    const prefixedSay = msg => console.log(`Sent news update: '${msg}'`) || say('_Breaking news:_\n' + msg)
    setTimeout(() => jokes.newsAlert(prefixedSay).catch(logError), 3000)
  } else if (random > 0.96) {
    setTimeout(async () => say('_Say have you heard this one?_'), 3000)
    setTimeout(() => jokes.tellJoke(say).catch(logError), 4000)
  }
}

const idFromWord = word => {
  if (word?.startsWith('<#') && word.endsWith('>')) {
    return word.replace(/<#([^|]*)|.*/g, '$1')
  }
  if (!word?.startsWith('<@') || !word.endsWith('>')) {
    return getIdFromName(word)
  } else {
    return word.substring(2, word.length - 1)
  }
}

const getSeconds = () => new Date().getTime() / 1000

const bigNumberWords = [
  ['tredecillion', 1_000_000_000_000_000_000_000_000_000_000_000_000_000_000],
  ['duodecillion', 1_000_000_000_000_000_000_000_000_000_000_000_000_000],
  ['undecillion', 1_000_000_000_000_000_000_000_000_000_000_000_000],
  ['decillion', 1_000_000_000_000_000_000_000_000_000_000_000],
  ['nonillion', 1_000_000_000_000_000_000_000_000_000_000],
  ['octillion', 1_000_000_000_000_000_000_000_000_000],
  ['septillion', 1_000_000_000_000_000_000_000_000],
  ['sextillion', 1_000_000_000_000_000_000_000],
  ['quintillion', 1_000_000_000_000_000_000],
  ['quadrillion', 1_000_000_000_000_000],
  ['trillion', 1_000_000_000_000],
  ['billion', 1_000_000_000],
  ['million', 1_000_000],
  ['qt', 1_000_000_000_000_000_000],
  ['qd', 1_000_000_000_000_000],
  ['tr', 1_000_000_000_000],
  ['b', 1_000_000_000],
  ['m', 1_000_000],
]

const commas = (num, precise = false, skipWords = false) => {
  num = Math.round(num)
  if (num === 1) {
    return 'one'
  }
  const bigNum = bigNumberWords.find(([, base]) => num >= base)
  if (bigNum && !precise) {
    const [name, base] = bigNum
    const nummed = (num / base).toPrecision(3)
    if (skipWords) {
      return nummed
    }
    return `${nummed} ${name}`
  }
  return num.toLocaleString()
}

const parseAll = (str, allNum, user) => {
  if (!str) {
    return NaN
  }

  str = str?.toLowerCase()?.replace(/,/g, '') || '1'

  switch (str) {
    case 'all':
    case 'all in':
    case 'everything':
    case 'sugma':
    case 'ligma':
    case 'pulma':
    case 'deez':
    case 'max_int':
    case 'my soul':
      return allNum
    case 'sex':
    case 'sex number':
    case 'nice':
      return 69_000_000
    case ':maple_leaf:':
    case ':herb:':
    case 'weed':
    case 'weed number':
      return 420_000_000
    case 'a milli':
      return 1_000_000
    case 'a band':
      return 1000
    case ':100:':
    case 'one hunna':
      return 100
  }
  if (user && buyableItems[str]) {
    return calculateCost({ itemName: str, user, quantity: 1 })
  }

  console.log('STR', str)
  if (str.match(/^\d+$/)) {
    return parseInt(str)
  }
  if (allNum && str.match(/^some$/)) {
    return Math.floor(Math.random() * allNum)
  }
  if (allNum && str.match(/^\d+%$/)) {
    const percent = parseFloat(str) / 100
    if (percent > 1 || percent < 0) {
      return NaN
    }
    return Math.round(percent * allNum)
  }

  if (str.match(/^\d+\.\d+$/)) {
    return Math.round(parseFloat(str))
  }

  const bigNum = bigNumberWords.find(([name]) => str.endsWith(name))
  if (bigNum && str.match(/^\d+(\.\d+)?/)) {
    return Math.round(parseFloat(str) * bigNum[1])
  }

  return NaN
}

const calculateCost = ({ itemName, user, quantity = 1 }) => {
  let currentlyOwned = user.items[itemName] || 0
  let realCost = 0
  for (let i = 0; i < quantity; i++) {
    realCost += Math.ceil(buyableItems[itemName].baseCost * Math.pow(1.15, currentlyOwned || 0))
    currentlyOwned += 1
  }
  return realCost
}

const game = loadGame()
let { users, nfts, squad } = game

const getAllUsers = async () => {
  const result = await dbPool.query(`
    SELECT slack_id, data
    FROM hvacker_user
  `).catch(console.error)
  return Object.fromEntries(result.rows.map(
    ({ slack_id: slackId, data }) => [slackId, data]))
}
// getAllUsers().then(collection => {
//   game.users = (users = collection)
// })

const setHighestCoins = userId => {
  const prevMax = users[userId].highestEver || 0
  if (prevMax < users[userId].coins) {
    users[userId].highestEver = users[userId].coins
  }
}

const addAchievement = (user, achievementName, say) => {
  if (!achievements[achievementName]) {
    logError(`Achievement ${achievementName} does not exist!`)
    return
  }
  if (user.achievements[achievementName]) {
    return
  }
  setTimeout(async () => {
    user.achievements[achievementName] = true
    saveGame(`${user.name} earned ${achievementName}`)
    await say(`You earned the achievement ${achievements[achievementName].name}!`)
  }, 500)
}

const fuzzyMatcher = string => new RegExp((string?.toLowerCase() || '').split('').join('.*'), 'i')

let knownUsers = {}
const getIdFromName = name => {
  const matcher = fuzzyMatcher(name?.toLowerCase())
  const found = Object.entries(knownUsers).find(([id, knownName]) => matcher.test(knownName?.toLowerCase()))
  if (found) {
    return found[0]
  }
  return null;
}

const fetchUser = async (userId, updateCoins = false) => {
  const result = await dbPool.query(`
    SELECT data
    FROM hvacker_user
    WHERE slack_id = $1`
    , [userId])
    .catch(console.error)
  return result.rows[0]?.data
}

const getUser = async (userId, updateCoins = false) => {
  // users[userId] = await fetchUser(userId)
  // console.log('USER', users[userId])
  //users[userId] = await fetchUser(userId)
  return getUserSync(userId, updateCoins)
}

const getUserSync = (userId, updateCoins = false) => {
  users[userId] ??= {}
  users[userId].coins ??= 0
  users[userId].items ??= {}
  users[userId].upgrades ??= {}
  users[userId].achievements ??= {}
  users[userId].coinsAllTime ??= users[userId].coins
  users[userId].prestige ??= 0
  users[userId].startDate ??= new Date()
  // users[userId].name ??= slack.users[userId]
  if (updateCoins) {
    users[userId].coins = getCoins(userId, users[userId])
  }
  saveGame('getUserSync()', true, true)
  return users[userId]
}

const addCoins = (user, add) => {
  user.coins += add
  user.coinsAllTime += add
  user.coinsAllTime = Math.floor(user.coinsAllTime)
  user.coins = Math.floor(user.coins)
}

const getCoins = (userId, user) => {
  user = user || getUserSync(userId)
  const currentTime = getSeconds()
  const lastCheck = user.lastCheck || currentTime
  const secondsPassed = currentTime - lastCheck

  addCoins(user, getCPS(user) * secondsPassed)

  user.lastCheck = currentTime
  setHighestCoins(userId)
  //saveGame()
  return user.coins
}

const getCPS = user => {
  const userItems = user?.items || {}
  return Math.round(Object.keys(userItems).reduce((total, itemName) => total + getItemCps(user, itemName), 0))
}

const getItemCps = (user, itemName) => (user.items[itemName] || 0) * singleItemCps(user, itemName)

const squadUpgrades = {
  tastyKeyboards: {
    name: 'Tasty Keyboards',
    description: 'Delicious and sticky. Boosts CPS by 20% for everyone.',

    effect: cps => cps * 1.2,
    cost: 10_000_000_000_000,
    emoji: 'keyboard'
  },
  copyPasteMacro: {
    name: 'Copy-Paste Macro.',
    description: 'Don\'t actually use this. Boosts CPS by 20% for everyone.',
    effect: cps => cps * 1.2,
    cost: 100_000_000_000_000,
    emoji: 'printer'
  },
  discardHumanMorals: {
    name: 'Neglect human decency',
    description: `Unlocks a new tier of upgrades, but at what cost?`,
    effect: cps => cps * 1.1,
    cost: 100_000_000_000_000_000,
    emoji: 'hole'
  },
  redemption: {
    name: 'Redemption',
    description: 'Can you return from the depths of depravity and save your soul?',
    effect: cps => cps * 1.1,
    cost: 1_000_000_000_000_000_000,
    emoji: 'people_hugging'
  }
}

const squadHas = ([name]) => squad.upgrades[name] === true
const squadIsMissing = name => !squadHas(name)

const getCompletedSquadgrades = () =>
  Object.entries(squadUpgrades)
    .filter(squadHas)
    .map(([, upgrade]) => upgrade)

const getCompletedSquadgradeNames = () =>
  Object.entries(squadUpgrades)
    .filter(squadHas)
    .map(([name]) => name)

const prestigeMultiplier = user => 1 + ((user.prestige || 0) * 0.01)

const quackGradeMultiplier = user => {
  const userQuackgrades = user.quackUpgrades?.cps || []
  return userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), 1)
}

const petQuackGradeMultiplier = user => {
  const userQuackgrades = user.quackUpgrades?.pet || []
  return userQuackgrades.reduce((total, upgrade) => quackStore[upgrade].effect(total, user), petBoost())
}

const singleItemCps = (user, itemName) => {
  const baseCps = buyableItems[itemName].earning
  // console.log('')
  // console.log(`${itemName} CPS:`)
  // console.log('baseCps', baseCps)

  const itemUpgrades = (user.upgrades[itemName] || []).map(name => upgrades[name])
  const itemUpgradeCps = itemUpgrades.reduce((totalCps, upgrade) => upgrade.effect(totalCps, user), 1)
  // console.log('itemUpgradeCps', itemUpgradeCps)

  user.upgrades.general ??= []
  const userGeneralUpgrades = user.upgrades.general
  const generalUpgradeCps = Object.entries(userGeneralUpgrades).reduce((total, [, upgradeName]) => upgrades[upgradeName].effect(total, user), 1)
  // console.log('generalUpgradeCps', generalUpgradeCps)

  const achievementCount = Object.keys(user.achievements || {}).length
  const achievementMultiplier = Math.pow(1.01, achievementCount)
  // console.log('achievementMultiplier', achievementMultiplier)

  const quackGrade = quackGradeMultiplier(user)
  // console.log('quackgrade', quackGrade)

  const pMult = prestigeMultiplier(user)
  // console.log('prestigeMultiplier', pMult)

  const squadGradeMultiplier = getCompletedSquadgrades().reduce((cps, upgrade) => upgrade.effect(cps), 1)
  // console.log('squadGradeMultiplier', squadGradeMultiplier)

  const petMultiplier = petQuackGradeMultiplier(user)
  //console.log('petMultiplier', petMultiplier)

  const total =
    baseCps *
    achievementMultiplier *
    itemUpgradeCps *
    generalUpgradeCps *
    quackGrade *
    pMult *
    squadGradeMultiplier *
    petMultiplier

  // console.log('Single Item CPS:', total)

  return total
}

const shuffle = str => str.split('').sort(() => 0.5 - Math.random()).join('')

const shufflePercent = (str, percentOdds) => {
  const shuffled = shuffle(str)
  let partiallyShuffled = ''
  const shuffleChar = () => Math.random() < percentOdds

  let isEmoji = false
  for (let i = 0; i < str.length; i++) {
    if (str[i] === ':') {
      isEmoji = !isEmoji
    }
    if (isEmoji) { // Less likely to shuffle emojis
      partiallyShuffled += (shuffleChar() && shuffleChar()) ? shuffled[i] : str[i]
    } else {
      partiallyShuffled += shuffleChar() ? shuffled[i] : str[i]
    }
  }

  return partiallyShuffled
}

const definitelyShuffle = (str, percentOdds) => {
  if (!str || str.length === 1) {
    return str
  }
  if (!percentOdds) {
    percentOdds = 0.01
  }
  let shuffled = str
  while (shuffled === str) {
    shuffled = shufflePercent(str, percentOdds)
    console.log('Shuffling... "' + shuffled + '"')
  }
  return shuffled
}

const getRandomFromArray = array => array[Math.floor(Math.random() * array.length)]

/**
 * Adds reactions to the given message, in order.
 * If adding any reaction is a failure, it will continue on to the next.
 * 
 * @param app The slack bolt app
 * @param channelId The id of the channel the message is in
 * @param timestamp The timestamp of the message
 * @param reactions An array of reactions to add
 * @returns {Promise<void>}
 */
const addReactions = async ({ app, channelId, timestamp, reactions }) => {
  for (const reaction of reactions) {
    try {
      await app.client.reactions.add({
        channel: channelId,
        timestamp,
        name: reaction
      })
    } catch (e) {
      logError(e)
    }
  }
}

const removeReactions = async ({ app, channelId, timestamp, reactions }) => {
  for (const reaction of reactions) {
    try {
      await app.client.reactions.remove({
        channel: channelId,
        timestamp,
        name: reaction
      })
    } catch (e) {
      logError(e)
    }
  }
}


const daysSinceEpoch = () => {
  const today = new Date().getTime()
  const epoch = new Date(0).getTime()
  return Math.floor((today - epoch) / (1000 * 60 * 60 * 24))
}

const dayOfYear = () => {
  const date = new Date()
  return ((Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) - Date.UTC(date.getFullYear(), 0, 0)) / 24 / 60 / 60 / 1000)
}

game.stonkMarket ??= {
  lastDay: daysSinceEpoch(),
  stonks: {
    duk: {
      pattern: "duk",
      index: 0,
      price: 1_410_911_983_728
    },
    quak: {
      pattern: "quak",
      index: 0,
      price: 5_111_242_778_696
    },
    honk: {
      pattern: "honk",
      index: 0,
      price: 511_915_144_009
    },
  }
}

const userHasCheckedQuackgrade = (user, quackGrade) => (user.quackUpgrades?.checked || []).includes(quackGrade)

const petBoost = () => {
  // game.pet ??= makePet()
  const stats = Object.values(game.pet)
  const hasTerribleStat = stats.filter(value => value < 1).length > 0
  const averageStat = stats.reduce((total, current) => total + current, 0) / stats.length

  if (hasTerribleStat && averageStat < 3) {
    return 0.9
  }

  if (averageStat === 10) {
    return 1.3
  }

  if (!hasTerribleStat && averageStat > 8) {
    return 1.1
  }

  return 1
}

game.channelMaps ??= {}
let slackAppClientChatUpdate

/**
 *
 * @param name String name for this channel map
 * @param text String of to send. Passed into slack.app.client.chat.update
 * @param blocks Slack blocks object to send. Passed into slack.app.client.chat.update
 * @param channel An (optional) new channel to add to the given map
 * @param ts The timestamp of the message in the new channel to update
 */
const updateAll = async ({ name, text, blocks, add: { channel, ts } = {} }) => {
  const channelMap = (game.channelMaps[name] ??= {})
  // if (channel && ts && !channelMap[channel]) {
  // }
  if (channel && ts) {
    channelMap[channel] = ts
    console.log({ channelMap })
  }
  if (text || blocks) {
    await Promise.all(Object.entries(channelMap).map(async ([channel, ts]) =>
      slackAppClientChatUpdate({
        channel,
        ts,
        text,
        blocks
      }).catch(e => {
        console.error(e)
        if (e.toString().includes('message_not_found')) {
          delete channelMap[channel]
          saveGame(`removing message ${channel}::${ts} from the ${name} list`)
        }
      })
    ))
  }

  // // const alreadyHas = !!channelMap[channel]
  // if (channel && ts) {
  //   channelMap[channel] = ts
  //   console.log({ channelMap })
  // }
  // // return alreadyHas
}

const logMemoryUsage = name => {
  const formatMemoryUsage = (data) => `${Math.round(data / 1024 / 1024 * 100) / 100} MB`;

  const memoryData = process.memoryUsage();
  const formattedData = {
    rss: `${formatMemoryUsage(memoryData.rss)} -> Resident Set Size - total memory allocated for the process execution`,
    // heapTotal: `${formatMemoryUsage(memoryData.heapTotal)} -> total size of the allocated heap`,
    // heapUsed: `${formatMemoryUsage(memoryData.heapUsed)} -> actual memory used during the execution`,
    // external: `${formatMemoryUsage(memoryData.external)} -> V8 external memory`,
  }
  if (name) {
    console.log(name, formattedData)
  } else {
    console.log(formattedData)
  }
}

module.exports = {
  saveGame,
  saveUser,
  makeBackup,
  logError,
  parseOr,
  maybeNews,
  idFromWord,
  commas,
  setHighestCoins,
  addAchievement,
  getCoins,
  getUser,
  getUserSync,
  singleItemCps,
  getCPS,
  getItemCps,
  squadUpgrades,
  squadIsMissing,
  prestigeMultiplier,
  quackGradeMultiplier,
  shufflePercent,
  definitelyShuffle,
  parseAll,
  getRandomFromArray,
  chaosFilter,
  addReactions,
  removeReactions,
  getCompletedSquadgradeNames,
  game,
  dayOfYear,
  daysSinceEpoch,
  userHasCheckedQuackgrade,
  fuzzyMatcher,
  addCoins,
  calculateCost,
  setKnownUsers: users => knownUsers = users,
  petBoost,
  updateAll,
  setSlackAppClientChatUpdate: update => slackAppClientChatUpdate = update,
  setUpgrades,
  setSlackUsers,
  setJokes: _jokes => jokes = _jokes,
  logMemoryUsage
}
