{-# LANGUAGE TemplateHaskell #-}
-- |
-- Module      :  Web.Bot.Story.Internal
-- Copyright   :  Alexander Krupenkin 2016-2017
-- License     :  BSD3
--
-- Maintainer  :  mail@akru.me
-- Stability   :  experimental
-- Portability :  portable
--
-- Story bot implementation.
--
module Web.Bot.Story.Internal (storyBot) where

import Control.Concurrent (killThread, ThreadId)
import Data.IntMap.Strict as I
import Control.Concurrent.Chan
import Control.Concurrent.MVar
import Control.Monad (forever)
import qualified Data.Text as T
import Data.Map.Strict as M
import Data.Monoid ((<>))
import Data.Text (Text)
import Pipes

import Web.Bot.Platform
import Web.Bot.Metrics
import Web.Bot.Persist
import Web.Bot.Message
import Web.Bot.Story
import Web.Bot.User
import Web.Bot.Log

-- | 'Producer' from 'Chan' creator
fromChan :: MonadIO m => Chan b -> Producer b m ()
fromChan c = forever $ liftIO (readChan c) >>= yield

-- | Incoming messages will be sended
toSender :: (APIToken a, Persist a)
         => User
         -> (User -> Message -> Bot a ())
         -> Consumer Message (Bot a) ()
toSender u sender = forever $ do
    await >>= lift . sender u
    -- Metrics
    lift $ runDB $ upsertBy (StatUser $ userIdent u)
                            (UserStat (userIdent u) 0 1)
                            [UserStatMessageOut +=. 1]


-- | Chat ID based message splitter
storyHandler :: (Persist a, APIToken a, ToMessage help)
             => MVar (IntMap (Chan Message, ThreadId))
             -> Map Message (Story a)
             -> help
             -> User -> Message -> Bot a ()
storyHandler chats stories help user msg = do
    -- Get a chat id
    let newStory item = modifyMVar_ chats
                                    (return . I.insert (userChat user) item)
        deleteStory   = modifyMVar_ chats
                                    (return . I.delete (userChat user))
    -- Metrics
    runDB $ do
        upsertBy (StatUser $ userIdent user)
                 (UserStat (userIdent user) 0 1)
                 [UserStatMessageIn +=. 1]
        upsertBy (UserIdentity $ userIdent user)
                 user
                 [ UserName =. userName user
                 , UserChat =. userChat user ]

    chatMap <- liftIO (readMVar chats)
    -- Lookup chat id in the map
    case I.lookup (userChat user) chatMap of
        -- Chat exist => story is run now
        Just (chan, tid) ->
            -- Want to cancel it?
            case msg of
                "/cancel" -> do
                    $logDebugS "Story" ("Cancel request, story "
                                        <> T.pack (show tid) <> " killed.")
                    liftIO (killThread tid)
                    sendMessage user help

                _ -> liftIO (writeChan chan msg)

        -- Is no runned stories
        Nothing ->
            case M.lookup msg stories of
                -- Unknown story, try to help
                Nothing -> do
                    sendMessage user help
                    $logDebugS "Story" ("Unknown story "
                                        <> T.pack (show msg) <> ".")

                -- Story exist
                Just story -> do
                    -- Create chan
                    chan <- liftIO newChan
                    -- Story pipeline
                    let pipeline = fromChan chan
                                >-> (story user >>= yield)
                                >-> toSender user sendMessage
                    -- Run story in separate thread
                    tid <- forkFinallyBot (runEffect pipeline)
                                          (const deleteStory)
                    -- Update userMap
                    liftIO (newStory (chan, tid))

                    -- Log and update metrics
                    let sname = T.pack (show msg)
                    runDB $ upsertBy (StatStory sname)
                                     (StoryStat sname 1)
                                     [StoryStatCalls +=. 1]
                    $logDebugS "Story" ("Story " <> sname
                                        <> " spawned at "
                                        <> T.pack (show tid) <> ".")


-- | User story handler
storyBot :: (Persist a, APIToken a, ToMessage help)
         => help -> Map Message (Story a) -> Bot a ()
storyBot help stories = do
    -- Create map from user chat to it story
    chats <- liftIO (newMVar I.empty)
    -- Run update loop
    $logDebugS "Story" "Init success."
    messageHandler $ storyHandler chats stories help