{-# LANGUAGE DeriveDataTypeable         #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE FunctionalDependencies     #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE TemplateHaskell            #-}
{-| A client library for the Flowdock API. Currently only implements
    the push API.
 -}
module Chat.Flowdock (
  -- * Using the client
  -- $client
  FlowdockClient,
  withFlowdockClient,
  -- ** Authentication types
  -- $auth
  Push(..),
  User(..),
  -- ** General types
  Flow(..),
  flow,
  organization,
  -- ** REST API
  MessageType,
  Message,
  MessageId(..),
  message,
  comment,
  flow,
  event,
  tags,
  externalUserName,
  uuid,
  Chat,
  content,
  FileUpload,
  content,
  contentType,
  Status,
  content,
  fileName,
  sendMessage,
  Comment,
  content,
  messageId,
  sendComment,
  -- ** Pushing messages to the inbox
  pushToInbox,
  newInboxPushMessage,
  MessageResponse,
  messageId,
  sent,
  app,
  -- ** Pushing messages to the chatroom
  pushToChat,
  Content,
  ExternalUserName,
  newChatPushMessage,
  -- ** Streaming events
  FlowFilter,
  allFlows,
  justFlows,
  StreamQuery,
  user,
  Event,
  active,
  streamOptions,
  streamFlow,
  streamFlows,
  -- ** Constructing messages
  -- *** InboxPushMessage fields
  InboxPushMessage,
  source,
  fromAddress,
  subject,
  fromName,
  replyTo,
  project,
  InboxPushFormat(..),
  format,
  link,
  -- *** ChatPushMessage fields
  ChatPushMessage,
  externalUserName,
  messageId,
  -- *** Common fields
  content,
  Tag(..),
  tags,
  -- *** Exception types
  JSONError(..),
  -- *** Lens field classes
  HasActive,
  HasApp,
  HasContent,
  HasContentType,
  HasEvent,
  HasExternalUserName,
  HasFileName,
  HasFlow,
  HasFormat,
  HasFromAddress,
  HasFromName,
  HasLink,
  HasMessageId,
  HasOrganization,
  HasProject,
  HasReplyTo,
  HasSent,
  HasSource,
  HasSubject,
  HasTags,
  HasUser,
  HasUuid
) where
import           Control.Applicative
import           Control.Exception
import           Control.Lens           hiding ((.=))
import           Control.Lens.TH
import           Control.Monad
import           Data.Aeson
import           Data.Aeson.TH
import           Data.ByteString        (ByteString)
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8  as BC
import           Data.ByteString.Lazy   (fromChunks)
import           Data.HashMap.Strict    (HashMap)
import qualified Data.HashMap.Strict    as HM
import           Data.Maybe
import           Data.Monoid
import           Data.Text              (Text)
import qualified Data.Text              as T
import           Data.Text.Encoding     (decodeUtf8, encodeUtf8)
import           Data.Typeable
import           Data.UUID
import qualified Network.Socket         as S
import           Pipes
import qualified Pipes.Aeson            as A
import           Pipes.HTTP
import           Pipes.Parse

import           Chat.Flowdock.Internal
-- REST API
{-
listUserFlows
listAllFlows
getFlow
getFlowById
createFlow
updateFlow
-}

-- wrapper to work around orphan instance for ToJSON
newtype WrapUUID = Wrap UUID

instance ToJSON WrapUUID where
  toJSON (Wrap u) = String $ decodeUtf8 $ toASCIIBytes u

data Flow
  = QualifiedFlow
    { flowOrganization :: Text
    , flowFlow         :: Text
    }
  | FlowId Text

data Tag = UserTag Text | HashTag Text
  deriving (Read, Show)

instance ToJSON Tag where
  toJSON (UserTag t) = String ("@" <> t)
  toJSON (HashTag t) = String ("#" <> t)

data Message a = Message
  { messageFlow             :: Flow
  , messageEvent            :: a
  , messageTags             :: [Tag]
  , messageExternalUserName :: Maybe Text
  , messageUuid             :: Maybe UUID
  }

data Chat = Chat
  { chatContent :: Text
  }

data FileUpload = FileUpload
  { fileUploadContent     :: ByteString
  , fileUploadContentType :: Text
  , fileUploadFileName    :: Text
  }

newtype MessageId = MessageId Int
  deriving (Show, ToJSON, FromJSON)

data Comment = Comment
  { commentContent   :: Text
  , commentMessageId :: MessageId
  }

message qf e = Message qf e [] Nothing Nothing

comment = Comment

data Status = Status
  { statusContent :: Text
  }

type Event = Object

newtype FlowFilter = FlowFilter (Maybe [Flow])

data StreamQuery a = StreamQuery
  { streamQuerySource :: a
  , streamQueryUser   :: Maybe Bool
  , streamQueryActive :: Maybe Bool
  }

data MessageResponse c = MessageResponse
  { messageResponseMessageId :: MessageId
  , messageResponseSent      :: Int
  , messageResponseApp       :: Text
  -- , _mr
  } deriving (Show)

instance FromJSON (MessageResponse r) where
  parseJSON (Object o) = MessageResponse <$> (MessageId <$> o .: "id") <*> o .: "sent" <*> o .: "app"
  parseJSON _ = mzero

makeFields ''Flow
makeFields ''Message
makeFields ''Chat
makeFields ''FileUpload
makeFields ''Comment
makeFields ''Status
makeFields ''StreamQuery
makeFields ''MessageResponse

class StreamParams a where
  streamParams :: a -> [(ByteString, Maybe ByteString)]

instance StreamParams Flow where
  streamParams = const []

instance StreamParams FlowFilter where
  streamParams (FlowFilter s) = [("filter", Just $ encodeUtf8 $ T.intercalate "," $ s ^.. _Just . traverse . to (\f -> (f ^. organization) <> "/" <> (f ^. flow)))]


class MessageType a where
  messageJSON :: Message a -> Value

instance MessageType Chat where
  messageJSON m = Object . HM.insert "content" (m ^. event . content . to toJSON) $ baseMessage "message" m

instance ToJSON (Message Comment) where
  toJSON m = let e = m ^. event in Object
    . HM.insert "content" (e ^. content . to toJSON)
    . HM.insert "message" (e ^. messageId . to toJSON)
    $ baseMessage "comment" m

instance MessageType Status where
  messageJSON m = Object . HM.insert "content" (m ^. event . content . to toJSON) $ baseMessage "status" m

instance MessageType FileUpload where
  messageJSON m = Object . HM.insert "content" co $ baseMessage "file" m
    where
      e = m ^. event
      co = object [ "data" .= decodeUtf8 (B64.encode (e ^. content))
                  , "content_type" .= (e ^. contentType)
                  , "file_name" .= (e ^. fileName)
                  ]

data InboxPushFormat = Html
  deriving (Read, Show)

instance ToJSON InboxPushFormat where
  toJSON = const $ String "html"

data InboxPushMessage = InboxPushMessage
  { inboxPushMessageSource      :: Text
  , inboxPushMessageFromAddress :: Text
  , inboxPushMessageSubject     :: Text
  , inboxPushMessageContent     :: Text
  , inboxPushMessageFromName    :: Maybe Text
  , inboxPushMessageReplyTo     :: Maybe Text
  , inboxPushMessageProject     :: Maybe Text
  , inboxPushMessageFormat      :: Maybe InboxPushFormat
  , inboxPushMessageTags        :: Maybe [Tag]
  , inboxPushMessageLink        :: Maybe Text
  } deriving (Read, Show)

data ChatPushMessage = ChatPushMessage
  { chatPushMessageContent          :: Text
  , chatPushMessageExternalUserName :: Text
  , chatPushMessageTags             :: Maybe [Tag]
  , chatPushMessageMessageId        :: Maybe Text
  } deriving (Read, Show)

data JSONError = JSONError String
  deriving (Show, Typeable)

instance Exception JSONError

addPath :: ByteString -> Request -> Request
addPath p r = r { path = path r <> p }

flowdockRestBaseRequest = let (Just r) = parseUrl "https://api.flowdock.com/" in r { requestHeaders = ("Content-Type", "application/json") : requestHeaders r }
flowdockPushBaseRequest = let (Just r) = parseUrl "https://api.flowdock.com/v1/" in r { requestHeaders = ("Content-Type", "application/json") : requestHeaders r }

flowdockStreamBaseRequest = let (Just r) = parseUrl "https://stream.flowdock.com/" in r { requestHeaders = ("Content-Type", "application/json") : requestHeaders r }

-- Push API
newtype Push = Push { pushFlowApiToken :: Text }

newtype User = User { userAccessToken :: Text }

class ClientManagerSettings a where
  managerSettings :: a -> ManagerSettings

instance ClientManagerSettings Push where
  managerSettings = const tlsManagerSettings

instance ClientManagerSettings User where
  managerSettings = const (tlsManagerSettings { managerResponseTimeout = Nothing
                                              , managerRawConnection = rawConnectionModifySocket (\s -> S.setSocketOption s S.KeepAlive 1) })

data FlowdockClient a = FlowdockClient
  { clientAuth    :: a
  , clientManager :: Manager
  }

type Content = Text
type ExternalUserName = Text

baseMessage :: Text -> Message a -> HashMap Text Value
baseMessage e m = HM.fromList (("event", String e) : ("tags", m ^. tags . to toJSON) : catMaybes [mk externalUserName "external_user_name", mk (uuid . _Just . to Wrap) "uuid"])
  where
    mk l k = preview (l . jk k) m
    jk k = to (\x -> (k, toJSON x))



{-
listMessages
showMessage
editMessage
deleteMessage
listPrivateConversations
getPrivateConversation
updatePrivateConversation
sendPrivateMessage
listPrivateMessage
showPrivateMessage
listUsers
listFlowUsers
getUser
updateUser
addUserToFlow
listOrganizations
getOrganization
getOrganizationById
updateOrganization
listSources
getSource
createSource
deleteSource
listInvitations
getInvitation
createInvitation
deleteInvitation
downloadFile
listFiles
uploadFile
-}

-- $client
--
-- The Flowdock API has different authentication mechanisms
-- for different parts of the API. Functions that depend on
-- specific authentication data are tagged with the authentication
-- type necessary to use them.
--
-- > {-# LANGUAGE OverloadedStrings #-}
-- >
-- > import Chat.Flowdock
-- >
-- > -- The push API uses the 'Push' authentication type
-- > main = withFlowdockClient (Push "YOUR_FLOW_TOKEN_HERE") $ \client -> do
-- >   let msg = newChatPushMessage "Hello World Bot" "Hello, world!"
-- >   pushToChat client msg
-- >

decodeAesonStream br = do
  mr <- (eitherDecode . fromChunks) <$> brConsume br
  case mr of
    Left e -> throw $ JSONError e
    Right r -> return r

withFlowdockClient :: ClientManagerSettings auth => auth -> (FlowdockClient auth -> IO a) -> IO a
withFlowdockClient a f = withManager (managerSettings a) $ \m -> f (FlowdockClient a m)

sendMessage :: MessageType a => FlowdockClient User -> Message a -> IO (MessageResponse a)
sendMessage (FlowdockClient (User token) man) m = do
  let req = applyBasicAuth (encodeUtf8 token) "" $ (addPath ("flows/" <> (m ^. flow . organization . to encodeUtf8) <> "/" <> (m ^. flow . flow . to encodeUtf8) <> "/messages") flowdockRestBaseRequest)
              { method = "POST"
              , requestBody = RequestBodyLBS $ encode $ messageJSON m
              }
  withResponse req man (decodeAesonStream . responseBody)

sendComment :: FlowdockClient User -> Message Comment -> IO (MessageResponse Comment)
sendComment (FlowdockClient (User token) man) m = do
  let req = applyBasicAuth (encodeUtf8 token) "" $ (addPath ("flows/" <> (m ^. flow . organization . to encodeUtf8) <> "/" <> (m ^. flow . flow . to encodeUtf8) <> "/messages/" <> (m ^. event . messageId . to (\(MessageId i) -> BC.pack (show i))) <> "/comments") flowdockRestBaseRequest)
              { method = "POST"
              , requestBody = RequestBodyLBS $ encode m
              }
  withResponse req man (decodeAesonStream . responseBody)


newInboxPushMessage = InboxPushMessage "" "" "" "" Nothing Nothing Nothing (Just Html) Nothing Nothing

makeFields ''InboxPushMessage
jsonizeToSnake ''InboxPushMessage

pushToInbox :: FlowdockClient Push -> InboxPushMessage -> IO ()
pushToInbox (FlowdockClient (Push token) man) msg = do
  -- post $ encode $ Authenticated t m
  let req = (addPath ("messages/team_inbox/" <> encodeUtf8 token) flowdockPushBaseRequest)
              { method = "POST"
              , requestBody = RequestBodyLBS $ encode msg
              }
  withResponse req man $ \_ -> return ()

makeFields ''ChatPushMessage
jsonizeToSnake ''ChatPushMessage

newChatPushMessage :: ExternalUserName -> Content -> ChatPushMessage
newChatPushMessage eun c = ChatPushMessage c eun Nothing Nothing

pushToChat :: FlowdockClient Push -> ChatPushMessage -> IO ()
pushToChat (FlowdockClient (Push token) man) msg = do
  -- post $ encode $ Authenticated t m
  let req = (addPath ("messages/chat/" <> encodeUtf8 token) flowdockPushBaseRequest)
              { method = "POST"
              , requestBody = RequestBodyLBS $ encode msg
              }
  withResponse req man $ \_ -> return ()

-- Streaming API
streamQueryString :: StreamParams a => StreamQuery a -> [(ByteString, Maybe ByteString)]
streamQueryString q = streamParams (q ^. source) ++ catMaybes [aq, uq]
  where
    aq = do
      a <- q ^. active
      return $ if a
        then ("active", Just "true")
        else ("active", Just "idle")
    uq = case q ^. user of
      Nothing -> Nothing
      Just u -> if u then Just ("user", Just "1") else Nothing

allFlows :: FlowFilter
allFlows = FlowFilter Nothing

justFlows :: [Flow] -> FlowFilter
justFlows = FlowFilter . Just

streamOptions :: a -> StreamQuery a
streamOptions x = StreamQuery x Nothing Nothing

streamFlow :: FlowdockClient User -> StreamQuery Flow -> (Producer Event IO () -> IO a) -> IO a
streamFlow (FlowdockClient (User token) man) q cb = do
  let req = applyBasicAuth (encodeUtf8 token) "" $ addPath ("flows/" <> org <> "/" <> flow) flowdockStreamBaseRequest
  withHTTP req man $ \r -> do
    let responseStream = responseBody r
    cb $ streamJSON responseStream
  where
    org  = encodeUtf8 $ flowOrganization $ streamQuerySource q
    flow = encodeUtf8 $ flowOrganization $ streamQuerySource q

streamFlows :: FlowdockClient User -> StreamQuery FlowFilter -> (Producer Event IO () -> IO a) -> IO a
streamFlows (FlowdockClient (User token) man) q cb = do
  let req = setQueryString (streamQueryString q) $ applyBasicAuth (encodeUtf8 token) "" $ addPath "flows" flowdockStreamBaseRequest
  withHTTP req man $ \r -> do
    let responseStream = responseBody r
    cb $ streamJSON responseStream

parseJSONStream :: Monad m => Parser ByteString m (Maybe (Either A.DecodingError Object))
parseJSONStream = do
  mStr <- draw
  case mStr of
    Nothing -> return ()
    Just str -> unDraw $ BC.dropWhile (== '\n') str
  A.decode

streamJSON :: MonadIO m => Producer ByteString m () -> Producer Object m ()
streamJSON = go
  where
    go p = do
      (r, p') <- lift $ runStateT parseJSONStream p
      case r of
        Nothing -> return ()
        Just v -> do
          case v of
            Left err -> liftIO $ putStrLn ("Decoding error: " ++ show err)
            Right ok -> yield ok
          go p'


-- getParentMessageId :: MessageResponse Comment -> MessageId
-- getParentMessageId = undefined