{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Rollbar.Item.Request
    ( Request(..)
    , Get(..)
    , IP(..)
    , Method(..)
    , MissingHeaders(..)
    , QueryString(..)
    , RawBody(..)
    , URL(..)
    , RemoveHeaders
    ) where
import Data.Aeson
    ( FromJSON
    , KeyValue
    , ToJSON
    , Value(Object, String)
    , object
    , pairs
    , parseJSON
    , toEncoding
    , toJSON
    , (.:)
    , (.=)
    )
import Data.Aeson.Types (typeMismatch)
import Data.Bifunctor   (bimap)
import Data.Maybe       (catMaybes, fromMaybe)
import Data.String      (IsString)
import GHC.Generics (Generic)
import Network.HTTP.Types (Query)
import Network.Socket     (SockAddr(SockAddrInet), tupleToHostAddress)
import Rollbar.Item.MissingHeaders
import Text.Read (readMaybe)
import qualified Data.ByteString    as BS
import qualified Data.Text          as T
import qualified Data.Text.Encoding as TE
data Request headers
    = Request
        { rawBody     :: RawBody
        , get         :: Get
        
        , headers     :: MissingHeaders headers
        , method      :: Method
        , queryString :: QueryString
        
        , url         :: URL
        , userIP      :: IP
        
        }
    deriving (Eq, Generic, Show)
newtype RawBody
    = RawBody BS.ByteString
    deriving (Eq, Generic, IsString, Show)
instance FromJSON RawBody where
    parseJSON v = RawBody . BS.pack <$> parseJSON v
instance ToJSON RawBody where
    toJSON (RawBody body) = toJSON (myDecodeUtf8 body)
    toEncoding (RawBody body) = toEncoding (myDecodeUtf8 body)
newtype Get
    = Get Query
    deriving (Eq, Generic, Show)
instance FromJSON Get where
    parseJSON v = Get . fmap (bimap BS.pack (fmap BS.pack)) <$> parseJSON v
instance ToJSON Get where
    toJSON (Get q) = object . catMaybes . queryKVs $ q
    toEncoding (Get q) = pairs . mconcat . catMaybes . queryKVs $ q
queryKVs :: forall kv. (KeyValue kv) => Query -> [Maybe kv]
queryKVs = fmap go
    where
    go :: (BS.ByteString, Maybe BS.ByteString) -> Maybe kv
    go (key', val') = do
        key <- myDecodeUtf8 key'
        let val = val' >>= myDecodeUtf8
        pure (key .= val)
newtype Method
    = Method BS.ByteString
    deriving (Eq, Generic, Show)
instance FromJSON Method where
    parseJSON v = Method . BS.pack <$> parseJSON v
instance ToJSON Method where
    toJSON (Method q) = toJSON (myDecodeUtf8 q)
    toEncoding (Method q) = toEncoding (myDecodeUtf8 q)
newtype QueryString
    = QueryString BS.ByteString
    deriving (Eq, Generic, Show)
instance FromJSON QueryString where
    parseJSON v = QueryString . BS.pack <$> parseJSON v
instance ToJSON QueryString where
    toJSON (QueryString q) = toJSON (myDecodeUtf8' q)
    toEncoding (QueryString q) = toEncoding (myDecodeUtf8' q)
newtype IP
    = IP SockAddr
    deriving (Eq, Generic, Show)
instance FromJSON IP where
    parseJSON v@(String s) = case T.splitOn "." s of
        [a', b', c', d] -> case T.splitOn ":" d of
            [e', f'] -> maybe (typeMismatch "IP" v) pure $ do
                [a, b, c, e] <- traverse (readMaybe . T.unpack) [a', b', c', e']
                f <- (readMaybe . T.unpack) f'
                pure . IP . SockAddrInet f $ tupleToHostAddress (a, b, c, e)
            _ -> typeMismatch "IP" v
        _ -> typeMismatch "IP" v
    parseJSON v = typeMismatch "IP" v
instance ToJSON IP where
    toJSON (IP ip) = toJSON (show ip)
    toEncoding (IP ip) = toEncoding (show ip)
requestKVs :: (KeyValue kv, RemoveHeaders headers) => Request headers -> [kv]
requestKVs Request{get, headers, method, queryString, rawBody, url, userIP} =
    [ "body" .= rawBody
    , "GET" .= get
    , "headers" .= headers
    , "method" .= method
    , "query_string" .= queryString
    , "url" .= url
    , "user_ip" .= userIP
    ]
instance FromJSON (Request headers) where
    parseJSON (Object o) =
        Request
            <$> o .: "body"
            <*> o .: "GET"
            <*> o .: "headers"
            <*> o .: "method"
            <*> o .: "query_string"
            <*> o .: "url"
            <*> o .: "user_ip"
    parseJSON v = typeMismatch "Request headers" v
instance (RemoveHeaders headers) => ToJSON (Request headers) where
    toJSON = object . requestKVs
    toEncoding = pairs . mconcat . requestKVs
newtype URL
    = URL (Maybe BS.ByteString, [T.Text])
    deriving (Eq, Generic, Show)
prettyURL :: URL -> T.Text
prettyURL (URL (host, parts)) =
    T.intercalate "/" (fromMaybe "" (host >>= myDecodeUtf8) : parts)
instance FromJSON URL where
    parseJSON (String s) = case T.splitOn "/" s of
        host:parts | "http" `T.isPrefixOf` host -> pure $ URL (Just $ TE.encodeUtf8 host, parts)
        parts -> pure $ URL (Nothing, parts)
    parseJSON v       = typeMismatch "URL" v
instance ToJSON URL where
    toJSON = toJSON . prettyURL
    toEncoding = toEncoding . prettyURL
myDecodeUtf8 :: BS.ByteString -> Maybe T.Text
myDecodeUtf8 = either (const Nothing) Just . TE.decodeUtf8'
myDecodeUtf8' :: BS.ByteString -> T.Text
myDecodeUtf8' = fromMaybe "" . myDecodeUtf8