{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections     #-}
module Kurita.Bot where

import qualified Data.Aeson                 as Aeson
import           Text.Megaparsec
import           Text.Megaparsec.Char       (string)
import           Text.Megaparsec.Char.Lexer (decimal)

import           Control.Monad.Except
import           Data.Foldable              (toList)
import           Data.Map                   (Map)
import qualified Data.Map                   as Map
import           Data.Sequence              (Seq)
import qualified Data.Sequence              as Seq
import           Data.Set                   (Set)
import qualified Data.Set                   as Set
import qualified Data.SortedList            as SL
import           Data.Text                  (Text)

import           Data.Bifunctor
import qualified Data.ByteString.Lazy       as BSL
import           Data.Either
import           Data.Int
import qualified Data.Text                  as Text
import qualified Data.Text.Encoding         as Text
import qualified Data.Text.IO               as Text

import           Control.Concurrent.STM

import           Kurita.Bot.Types
import           Kurita.Prompt
import           Kurita.Prompt.Internal
import           Kurita.Protocol

import           Network.Linklater

import           Data.ByteString (ByteString)
import           Data.Word

data KuritaBot c = KuritaBot {
  kbotAddPrompt   :: Prompt -> IO Int
, kbotPrompts     :: IO (Seq Prompt)
, kbotAddTerm     :: Text -> Text -> IO ()
, kbotTerms       :: IO (Map Text (Set Text))
, kbotBracket     :: IO (Bracket c Int64 KuritaGame)
, kbotSetPrompt   :: Text -> IO ()
, kbotSlackConfig :: BotConfig
, kbotHash        :: ByteString -> Word64
}

loadKBotConfig :: FilePath -> FilePath -> (ByteString -> Word64) -> BotConfig -> IO (Bracket c Int64 KuritaGame) -> (Text -> IO ()) -> IO (KuritaBot c)
loadKBotConfig promptFile termsFile hash botCfg getBracket setPrompt = do
  promptResults <- fmap parsePrompt . Text.lines <$> Text.readFile promptFile
  promptsVar <- newTMVarIO $ Seq.fromList $ rights promptResults

  let
    addPrompt p = do
      prompts <- atomically $ takeTMVar promptsVar
      Text.appendFile promptFile $ (displayPrompt p) <> "\n"
      let
        prompts' = prompts Seq.|> p
      atomically $ putTMVar promptsVar $ prompts'
      pure $ Seq.length prompts' - 1
    getPrompts = atomically $ readTMVar promptsVar

  termResults <- fmap parseTerm . Text.lines <$> Text.readFile termsFile
  termsVar <- newTMVarIO $ foldr addToTermMap Map.empty $ rights termResults

  let
    addTerm k v = do
      terms <- atomically $ takeTMVar termsVar
      Text.appendFile termsFile $ (displayTerm k v) <> "\n"
      atomically $ putTMVar termsVar $ addToTermMap (k, v) terms
    getTerms = atomically $ readTMVar termsVar

  pure $ KuritaBot {
    kbotAddPrompt = addPrompt
  , kbotPrompts = getPrompts
  , kbotAddTerm = addTerm
  , kbotTerms = getTerms
  , kbotBracket = getBracket
  , kbotSetPrompt = setPrompt
  , kbotSlackConfig = botCfg
  , kbotHash = hash
  }

addToTermMap :: (Ord k, Ord t) => (k, t) -> Map k (Set t) -> Map k (Set t)
addToTermMap (key, val) terms =
  Map.alter (Just . maybe (Set.singleton val) (Set.insert val)) key terms

parseTerm :: Text -> Either Text (Text, Text)
parseTerm = first Text.pack . Aeson.eitherDecode . BSL.fromStrict . Text.encodeUtf8

displayTerm :: Text -> Text -> Text
displayTerm k v = Text.decodeUtf8 $ BSL.toStrict $ Aeson.encode (k, v)

runBotEvent :: Ord c =>  (c -> Text) -> KuritaBot c -> BotEvent -> IO ()
runBotEvent toText kbot (EventMessage message) =
  case parseKBotMessage $ messageText message of
    Right cmd -> runBotCommand toText kbot cmd
    Left pErr  ->
      when ("!" `Text.isPrefixOf` messageText message)
        $ kuritaSend (kbotSlackConfig kbot) $ "Uh oh that's a parse error: " <> Text.pack (show pErr)
runBotEvent _toText _kbot (EventMessageReply _reply) = pure ()
runBotEvent _toText _kbot (ReactionAdded _reaction) = pure ()
runBotEvent _toText _kbot (ReactionRemoved _reaction) = pure ()

runBotCommand :: Ord c => (c -> Text) -> KuritaBot c -> BotCommand -> IO ()
runBotCommand _toText kbot (AddPrompt prompt) = do
  promptIndex <- kbotAddPrompt kbot prompt
  kuritaSend (kbotSlackConfig kbot) $
    "New prompt ID: "
      <> (Text.pack $ show promptIndex)
      <> " added - \"" <> displayPrompt prompt <> "\""
runBotCommand _toText kbot (List i j) = do
  prompts <- kbotPrompts kbot
  let
    indexSeq = Seq.fromList $ [0..Seq.length prompts]
    promptsWithIndex = Seq.zip indexSeq $ displayPrompt <$> prompts
    prompts' = Seq.drop i $ Seq.take j promptsWithIndex
  kuritaSend (kbotSlackConfig kbot) $
    Text.intercalate "\n" $ (\(index,v) -> Text.pack (show index) <> " - " <> v) <$> toList prompts'
runBotCommand toText kbot (CurrentBattle) = do
  current <- kbotBracket kbot
  case _bCurrent current of
    Nothing -> kuritaSend (kbotSlackConfig kbot) "I wasn't able to find a running duel for some reason"
    Just game ->
      kuritaSend (kbotSlackConfig kbot) $ Text.concat [
          Text.intercalate " vs. " (displayVotes <$> SL.fromSortedList (_gameSorted game)), "\n"
        , Text.intercalate "\n" (displayCommentary <$> (_kgCommentary $ _gameExtra game))
        ]
  where
    displayVotes (voteCount, competitor) =
      toText competitor <> " (" <> Text.pack (show voteCount) <> ")"
    displayCommentary (time, prompt) =
      Text.pack (show time) <> " - " <> prompt
runBotCommand _toText kbot (AddData var val) = do
  kbotAddTerm kbot var val
  terms <- kbotTerms kbot
  maybe
    (kuritaSend (kbotSlackConfig kbot) $ "There was an error inserting " <> var <> " -> " <> val)
    (\vals -> kuritaSend (kbotSlackConfig kbot) $ var <> " now contains: " <> (Text.pack $ show $ Set.toList vals))
    $ Map.lookup var terms
runBotCommand _toText kbot (RemoveData _var _val) =
  kuritaSend (kbotSlackConfig kbot) "I'm sorry dave I can't do that"
runBotCommand _toText kbot (SearchPrompts term) = do
  prompts <- kbotPrompts kbot
  let
    indexSeq = Seq.fromList $ [0..Seq.length prompts]
    promptsWithIndex = Seq.zip indexSeq $ displayPrompt <$> prompts
    filtered = Seq.take 25 $ Seq.filter (\(_, v) -> term `Text.isInfixOf` v) promptsWithIndex
  kuritaSend (kbotSlackConfig kbot) $ Text.intercalate "\n" $ (\(i,v) ->  (Text.pack (show i) <> " - " <> v)) <$> toList filtered
runBotCommand toText kbot (TestPrompt i)  = do
  prompts <- kbotPrompts kbot
  meta <- kbotTerms kbot
  br <- kbotBracket kbot
  case renderPrompt toText (kbotHash kbot) meta br <$> Seq.lookup i prompts of
    Nothing -> kuritaSend (kbotSlackConfig kbot) $ "Unable to find prompt at index " <> Text.pack (show i)
    Just (Left err) -> kuritaSend (kbotSlackConfig kbot) $ "Error rendering prompt: " <> Text.pack err
    Just (Right prompt) -> kuritaSend (kbotSlackConfig kbot) prompt
runBotCommand toText kbot (SetPrompt i)  = do
  prompts <- kbotPrompts kbot
  meta <- kbotTerms kbot
  br <- kbotBracket kbot
  case renderPrompt toText (kbotHash kbot) meta br <$> Seq.lookup i prompts of
    Nothing -> kuritaSend (kbotSlackConfig kbot) $ "Unable to find prompt at index " <> Text.pack (show i)
    Just (Left err) -> kuritaSend (kbotSlackConfig kbot) $ "Error rendering prompt: " <> Text.pack err
    Just (Right prompt) -> do
      kbotSetPrompt kbot prompt
      kuritaSend (kbotSlackConfig kbot) $ "Prompt set to \"" <> prompt <> "\""
runBotCommand toText kbot (SetLive pr)  = do
  meta <- kbotTerms kbot
  br <- kbotBracket kbot
  case renderPrompt toText (kbotHash kbot) meta br pr of
    Left err -> kuritaSend (kbotSlackConfig kbot) $ "Error rendering prompt: " <> Text.pack err
    Right prompt -> do
      kbotSetPrompt kbot prompt
      kuritaSend (kbotSlackConfig kbot) $ "Prompt set to \"" <> prompt <> "\""
runBotCommand toText kbot NextUp  = do
  br <- kbotBracket kbot
  case _bUpcoming br of
    [a, b]:_ -> do
      kuritaSend (kbotSlackConfig kbot) $ "Up next, "<>(toText a)<>" vs. "<>(toText b)<>", I can't wait!"
    _ -> pure ()
runBotCommand toText kbot LastRoundWinners  = do
  br <- kbotBracket kbot
  case (map (map (toText . last . map snd . toList . _gameSorted)) . _bPlayed $ br) of
    _:lr:_ -> kuritaSend (kbotSlackConfig kbot) $ "The winners of the last round were: " <>
             (Text.intercalate ", " $ map (Text.pack . show) lr)
    _ -> pure ()

parseKBotMessage :: Text -> Either Text BotCommand
parseKBotMessage t =
  first (Text.pack . show) $ runParser parsers "" t
  where
    parsers =
          parseAdd
      <|> parseLive
      <|> parseList
      <|> parseCurrentBattle
      <|> parseAddData
      <|> parseSearch
      <|> parseTestPrompt
      <|> parseSet
      <|> parseNext
      <|> parseWinners
    parseAdd = do
      _ <- try $ string "!add "
      prompt <- promptParser
      pure $ AddPrompt prompt
    parseLive = do
      _ <- try $ string "!live "
      prompt <- promptParser
      pure $ SetLive prompt
    parseList = do
      _ <- try $ string "!list "
      i <- decimal
      _ <- string " "
      j <- decimal
      pure $ List i j
    parseCurrentBattle = do
      _ <- try $ string "!battle"
      pure CurrentBattle
    parseAddData = do
      _ <- try $ string "!meta "
      var <- takeWhile1P Nothing (\c -> c /= ' ')
      _ <- string " -> "
      val <- takeRest
      pure $ AddData var val
    parseSearch = do
      _ <- try $ string "!search "
      val <- takeRest
      pure $ SearchPrompts val
    parseTestPrompt = do
      _ <- try $ string "!test "
      i <- decimal
      pure $ TestPrompt i
    parseSet = do
      _ <- try $ string "!set "
      i <- decimal
      pure $ SetPrompt i
    parseNext = do
      _ <- try $ string "!next"
      pure $ NextUp
    parseWinners = do
      _ <- try $ string "!lastwinners"
      pure $ LastRoundWinners

kuritaSend :: BotConfig -> Text -> IO ()
kuritaSend botCfg msg = do
  err <- runExceptT $ say (SimpleMessage (EmojiIcon ":robot:") "kurita_bot" ch msg) cfg
  case err of
    Left e -> print e
    _      -> pure ()
  where
    (ch, cfg) = botConfigHook botCfg