{-# LANGUAGE OverloadedStrings #-}
{-|
Module      : Network.NSQ.Identify
Description : The metadata component for formatting and parsing the metadata sent to nsqd as part of the feature negotiation done upon connection establish.
-}
module Network.NSQ.Identify
    ( defaultIdentify
    , defaultUserAgent
    , encodeMetadata

    ) where

import Prelude hiding (take)
import Data.Maybe

import qualified Data.Map.Strict as Map

import qualified Data.ByteString.Lazy as BL

import qualified Data.Text as T

import Data.Aeson ((.=))
import qualified Data.Aeson as A
import qualified Data.Aeson.Types as A

import Network.NSQ.Types

-- | Build a default 'IdentifyMetadata' that makes sense which is
-- basically just setting the client 'Identification' and leaving
-- the rest of the settings up to the server to determine.
defaultIdentify :: T.Text -> T.Text -> IdentifyMetadata
defaultIdentify cid host = IdentifyMetadata
    { ident = Identification cid host Nothing Nothing Nothing
    , tls = Nothing
    , compression = Nothing
    , heartbeatInterval = Nothing
    , outputBufferSize = Nothing
    , outputBufferTimeout = Nothing
    , sampleRate = Nothing
    , custom = Nothing
    , customNegotiation = False
    }

-- | The default user agent to send, for identifying what client library is
-- connecting to the nsqd.
defaultUserAgent :: T.Text
defaultUserAgent = "hsnsq/0.1.2.0" -- TODO: find out how to identify this in the build step

-- | Generate a collection of Aeson pairs to insert into the json
-- that is being sent to the server as part of the metadata negotiation.
featureNegotiation :: IdentifyMetadata -> [A.Pair]
featureNegotiation im = catMaybes
    (
        tlsSettings (tls im)
        ++
        [ optionalSettings "heartbeat_interval" (-1) $ heartbeatInterval im
        , optionalSettings "output_buffer_size" (-1) $ outputBufferSize im
        , optionalSettings "output_buffer_timeout" (-1) $ outputBufferTimeout im
        , optionalSettings "sample_rate" 0 $ sampleRate im
        ]
        ++
        optionalCompression (compression im)
    )

-- | Take an optional setting and render an Aeson pair.
optionalSettings :: T.Text -> Int -> Maybe OptionalSetting -> Maybe A.Pair
optionalSettings _ _ Nothing                = Nothing
optionalSettings name def (Just Disabled)   = Just (name, A.toJSON def)
optionalSettings name _ (Just (Custom val)) = Just (name, A.toJSON val)

-- | Render the Aeson pairs for optional compression
optionalCompression :: Maybe Compression -> [Maybe A.Pair]
optionalCompression Nothing              = []
optionalCompression (Just NoCompression) = Just `fmap` [ "snappy" .= False, "deflate" .= False ]
optionalCompression (Just Snappy)        = Just `fmap` [ "snappy" .= True, "deflate" .= False ]
optionalCompression (Just (Deflate l))   = Just `fmap` [ "snappy" .= False, "deflate" .= True, "deflate_level" .= l ]

-- | Take the custom settings out of the custom map and render Aeson pairs
customMetadata :: Maybe (Map.Map T.Text T.Text) -> [A.Pair]
customMetadata Nothing    = []
customMetadata (Just val) = Map.foldrWithKey (\k v xs -> (k .= v):xs) [] val

-- | Tls settings
tlsSettings :: Maybe TLS -> [Maybe A.Pair]
tlsSettings Nothing      = []
tlsSettings (Just NoTLS) = [Just $ "tls_v1" .= False]
tlsSettings (Just TLSV1) = [Just $ "tls_v1" .= True]

-- TODO: This is an Orphan instance because the type is in types.hs, need to fix this
instance A.ToJSON IdentifyMetadata where
    toJSON im@(IdentifyMetadata{ident=i}) = A.object
        (
            -- Identification section
            [ "client_id"  .= clientId i
            , "hostname"   .= hostname i
            , "short_id"   .= fromMaybe (clientId i) (shortId i)
            , "long_id"    .= fromMaybe (hostname i) (longId i)
            , "user_agent" .= fromMaybe defaultUserAgent (userAgent i)

            -- Feature Negotiation section
            , "feature_negotiation" .= (not (null $ featureNegotiation im) || customNegotiation im)
            ]
            ++
            featureNegotiation im
            ++
            customMetadata (custom im)
        )

-- | Encode the metadata from 'IdentifyMetadata' into a 'ByteString' for
-- feeding the 'Identify' 'Command' for sending the metadata to the nsq
-- daemon as part of the feature negotiation.
encodeMetadata :: IdentifyMetadata -> BL.ByteString
encodeMetadata = A.encode