module Network.Slack.Message
       (
         Message(..),
         MessageRaw(..),
         convertRawMessage,
         TimeStamp(..),
         timeStampToString,
         channelHistory,
         channelHistoryBefore,
         channelHistoryAll,
         channelHistoryRecent,
         messagesByUser,
         postMessage
       )
       where

import Network.Slack.Prelude

import Network.Slack.Types (SlackResponseName(..), parseStrippedPrefix, Slack(..), request)

import Network.Slack.User (User(..), userFromId)
import Network.Slack.Channel (Channel(..))

import Data.Time.Clock (UTCTime, getCurrentTime, addUTCTime)
import Data.Time.Format (parseTime, formatTime)
import System.Locale (defaultTimeLocale)

import qualified Data.Traversable as T
import qualified Data.Map as M

-- | Fixed point number with 12 decimal places of precision
newtype TimeStamp = TimeStamp {
  utcTime :: UTCTime
  } deriving (Show, Eq, Ord)

-- | Converts a TimeStamp to the timestamp format the Slack API expects
timeStampToString :: TimeStamp -> String
timeStampToString = formatTime defaultTimeLocale "%s%Q" . utcTime

instance FromJSON TimeStamp where
  parseJSON (String s) = do
    let maybeTime = parseTime defaultTimeLocale "%s%Q" (unpack s):: Maybe UTCTime
    case maybeTime of
     Nothing     -> fail "Incorrect timestamp format."
     Just (time) -> return (TimeStamp time)

  parseJSON _ = fail "Expected a timestamp string"

instance SlackResponseName TimeStamp where
  slackResponseName _ = "ts"

-- | A message sent on a channel. Message can also mean things like user joined or a message was edited
-- TODO: Make this into a sum type of different message types, instead of using Maybe
data MessageRaw = MessageRaw {
  _messageType :: String,
  _messageUser :: Maybe String, -- user ID
  _messageText :: String,
  _messageTs :: TimeStamp
} deriving (Show, Generic)

instance FromJSON MessageRaw where
  parseJSON = parseStrippedPrefix "_message"

instance SlackResponseName [MessageRaw] where 
  slackResponseName _ = "messages"

-- | A nicer version of MessageRaw, with the user id converted to a User
data Message = Message {
  messageType :: String,
  messageUser :: Maybe User,
  messageText :: String,
  messageTimeStamp :: TimeStamp
  } deriving (Show, Eq) 

-- | Converts a MessageRaw into a Message
convertRawMessage :: MessageRaw -> Slack Message
convertRawMessage (MessageRaw mtype muid mtext mts) = do
  -- This converts a Maybe (Slack User) to a Slack (Maybe User)
  user <- T.sequence (userFromId <$> muid)
  return (Message mtype user mtext mts)
   
-- | List of the past n messages in the given channel
-- n must be no greater than 1000
channelHistory :: Int -> Channel -> Slack [Message]
channelHistory n chan = mapM convertRawMessage =<< request "channels.history" args
  where
    args = M.fromList [
      ("channel", channelId chan),
      ("count", show n)
      ]

-- | Gets the n messages occuring before the given time
channelHistoryBefore :: Int -> TimeStamp -> Channel -> Slack [Message]
channelHistoryBefore n ts chan = mapM convertRawMessage =<< request "channels.history" args
  where
    args = M.fromList [
      ("channel", channelId chan),
      ("count", show n),
      ("latest", timeStampToString ts)
      ]
-- | Retrieves the entire channel history
channelHistoryAll :: Channel -> Slack [Message]
channelHistoryAll chan = do
  latest <- channelHistory 1000 chan
  let
    older = go . messageTimeStamp . last $ latest
    -- Recursively fetch older and older messages, until Slack returns an empty list
    go :: TimeStamp -> Slack [Message]
    go ts = do
      messages <- channelHistoryBefore 1000 ts chan
      case messages of
       -- No more messages!
       [] -> return []
       -- Return the retreived messages ++ the messages older than the oldest retrieved message
       _  -> (messages ++) <$> (go . messageTimeStamp . last $ messages)
  (latest ++) <$> older

-- | Retrieves a list of the most recent messages within the last n seconds
channelHistoryRecent :: Int -> Channel -> Slack [Message]
channelHistoryRecent n chan = do
  now <- liftIO getCurrentTime
  let
    args = M.fromList [
      ("channel", channelId chan),
      ("count", "1000"),
      ("oldest", timeStampToString . TimeStamp $ addUTCTime nSecsAgo now)
      ]
    -- Convert to NominalDiffTime
    nSecsAgo = fromInteger (- (toInteger n))
  mapM convertRawMessage =<< request "channels.history" args
    
                              
-- | Retrieves the messages by the given user
messagesByUser :: User -> [Message] -> [Message]
messagesByUser user = filter (byUser . messageUser)
  where
    -- Messages with no users are excluded
    byUser Nothing = False
    byUser (Just u) = u == user

-- | Posts a message as the given user to the given channel.
-- Returns the timestamp of the message, if successful
postMessage :: String -> String -> Channel -> Slack TimeStamp
postMessage uname text chan = request "chat.postMessage" args
  where
    args = M.fromList [
      ("channel", channelId chan),
      ("username", uname),
      ("text", text)
      ]