{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

{-|
    Module      : Network.Wai.Middleware.Rollbar
    Description : WAI middleware for interfacing with Rollbar
    Copyright   : (c) Hardy Jones, 2017
    License     : BSD3
    Maintainer  : jones3.hardy@gmail.com
    Stability   : experimental

    Provides middleware for communicating with Rollbar.

    Currently has middleware for sending all server errors to Rollbar.
    More to come shortly.
-}

module Network.Wai.Middleware.Rollbar (requests) where

import Control.Concurrent (forkIO)
import Control.Exception  (Handler(Handler), catches, displayException)
import Control.Monad      (when)

import Data.Aeson   (ToJSON, defaultOptions, genericToEncoding, toEncoding)
import Data.Functor (void)
import Data.Maybe   (fromMaybe)
import Data.Time    (getCurrentTime)
import Data.UUID.V4 (nextRandom)

import GHC.Generics (Generic)

import Network.HostName          (getHostName)
import Network.HTTP.Client       (HttpException)
import Network.HTTP.Simple
    ( JSONException
    , Request
    , defaultRequest
    , httpNoBody
    , setRequestBodyJSON
    , setRequestHost
    , setRequestIgnoreStatus
    , setRequestMethod
    , setRequestPath
    , setRequestPort
    , setRequestSecure
    )
import Network.HTTP.Types.Status
    (Status(Status), statusCode, statusIsServerError, statusMessage)
import Network.Wai               (Middleware, ResponseReceived)

import System.Environment (getExecutablePath)
import System.IO          (hPutStrLn, stderr)

import qualified Data.Text          as T
import qualified Data.Text.Encoding as TE
import qualified Network.Wai        as NW
import qualified Rollbar.Item       as RI

-- | Middleware that watches responses
--  and sends an item to Rollbar if it is a server error (5xx).
--
--  Sends additional metadata including the request information.
requests :: RI.AccessToken -> RI.Environment -> Middleware
requests accessToken environment app req handler' = app req handler
    where
    handler :: NW.Response -> IO ResponseReceived
    handler res = do
        _ <- forkIO $ handle500 accessToken environment req res
        handler' res

handle500 :: RI.AccessToken -> RI.Environment -> NW.Request -> NW.Response -> IO ()
handle500 accessToken environment req res =
    when (statusIsServerError $ NW.responseStatus res) (send accessToken environment req res)
        `catches`
            [ Handler handleHttpException
            , Handler handleJSONException
            ]

send :: RI.AccessToken -> RI.Environment -> NW.Request -> NW.Response -> IO ()
send accessToken environment req res = do
    uuid <- Just . RI.UUID4 <$> nextRandom
    timestamp <- Just <$> getCurrentTime
    host <- Just <$> getHostName
    root <- Just . RI.Root . T.pack <$> getExecutablePath
    let request = Just RI.Request {..}
    let server = Just RI.Server { RI.branch = Nothing, RI.serverCodeVersion = Nothing, .. }
    let itemData = (RI.error environment messageBody payload)
            { RI.request, RI.server, RI.timestamp, RI.uuid }
    let rReq = rollbarRequest RI.Item{..}
    void $ httpNoBody rReq
    where
    Status{..} = NW.responseStatus res
    headers = RI.Headers $ NW.requestHeaders req
    messageBody = RI.MessageBody <$> myDecodeUtf8 statusMessage
    rawBody = ""
    get = RI.Get $ NW.queryString req
    method = RI.Method $ NW.requestMethod req
    queryString = RI.QueryString $ NW.rawQueryString req
    url = RI.URL (NW.requestHeaderHost req, NW.pathInfo req)
    userIP = RI.IP $ NW.remoteHost req
    referer = myDecodeUtf8 =<< NW.requestHeaderReferer req
    range = myDecodeUtf8 =<< NW.requestHeaderRange req
    userAgent = myDecodeUtf8 =<< NW.requestHeaderUserAgent req
    payload = Payload
        { statusMessage = myDecodeUtf8' statusMessage
        , ..
        }

    myDecodeUtf8 = either (const Nothing) Just . TE.decodeUtf8'
    myDecodeUtf8' = fromMaybe "" . myDecodeUtf8

handleHttpException :: HttpException -> IO ()
handleHttpException e = do
    hPutStrLn stderr "Ran into an exception while sending a request to Rollbar:"
    hPutStrLn stderr $ displayException e

handleJSONException :: JSONException -> IO ()
handleJSONException e = do
    hPutStrLn stderr "Ran into an exception while parsing JSON response from Rollbar:"
    hPutStrLn stderr $ displayException e

rollbarRequest :: ToJSON a => RI.Item a -> Network.HTTP.Simple.Request
rollbarRequest payload =
    setRequestMethod "POST"
    . setRequestSecure True
    . setRequestHost "api.rollbar.com"
    . setRequestPort 443
    . setRequestPath "api/1/item/"
    . setRequestBodyJSON payload
    . setRequestIgnoreStatus
    $ defaultRequest

data Payload
    = Payload
        { statusCode    :: Int
        , statusMessage :: T.Text
        , userAgent     :: Maybe T.Text
        , range         :: Maybe T.Text
        , referer       :: Maybe T.Text
        }
    deriving (Generic, Show)

instance ToJSON Payload where
    toEncoding = genericToEncoding defaultOptions