module LaunchDarkly.Server.Config.Internal
    ( Config(..)
    , mapConfig
    , ConfigI(..)
    , shouldSendEvents
    , ApplicationInfo
    , makeApplicationInfo
    , withApplicationValue
    , getApplicationInfoHeader
    ) where

import Control.Monad.Logger               (LoggingT)
import Data.Generics.Product              (getField)
import Data.Text                          (Text)
import qualified Data.Text as T
import Data.Set                           (Set)
import GHC.Natural                        (Natural)
import GHC.Generics                       (Generic)
import Network.HTTP.Client                (Manager)

import LaunchDarkly.Server.Store               (StoreInterface)
import LaunchDarkly.Server.DataSource.Internal (DataSourceFactory)
import LaunchDarkly.AesonCompat (KeyMap, insertKey, emptyObject, toList)
import qualified LaunchDarkly.AesonCompat as AesonCompat
import Data.List (sortBy)
import Control.Lens ((&))
import Data.Ord (comparing)

mapConfig :: (ConfigI -> ConfigI) -> Config -> Config
mapConfig f (Config c) = Config $ f c

shouldSendEvents :: ConfigI -> Bool
shouldSendEvents config = (not $ getField @"offline" config) && (getField @"sendEvents" config)

-- | Config allows advanced configuration of the LaunchDarkly client.
newtype Config = Config ConfigI

data ConfigI = ConfigI
    { key                   :: !Text
    , baseURI               :: !Text
    , streamURI             :: !Text
    , eventsURI             :: !Text
    , storeBackend          :: !(Maybe StoreInterface)
    , storeTTLSeconds       :: !Natural
    , streaming             :: !Bool
    , allAttributesPrivate  :: !Bool
    , privateAttributeNames :: !(Set Text)
    , flushIntervalSeconds  :: !Natural
    , pollIntervalSeconds   :: !Natural
    , userKeyLRUCapacity    :: !Natural
    , inlineUsersInEvents   :: !Bool
    , eventsCapacity        :: !Natural
    , logger                :: !(LoggingT IO () -> IO ())
    , sendEvents            :: !Bool
    , offline               :: !Bool
    , requestTimeoutSeconds :: !Natural
    , useLdd                :: !Bool
    , dataSourceFactory     :: !(Maybe DataSourceFactory)
    , manager               :: !(Maybe Manager)
    , applicationInfo       :: !(Maybe ApplicationInfo)
    } deriving (Generic)

-- | An object that allows configuration of application metadata.
--
-- Application metadata may be used in LaunchDarkly analytics or other product
-- features, but does not affect feature flag evaluations.
--
-- To use these properties, provide an instance of ApplicationInfo to the 'Config' with 'configSetApplicationInfo'.
newtype ApplicationInfo = ApplicationInfo (KeyMap Text) deriving (Show, Eq)

-- | Create a default instance
makeApplicationInfo :: ApplicationInfo
makeApplicationInfo = ApplicationInfo emptyObject

-- | Set a new name / value pair into the application info instance.
--
-- Values have the following restrictions:
-- - Cannot be empty
-- - Cannot exceed 64 characters in length
-- - Can only contain a-z, A-Z, 0-9, period (.), dash (-), and underscore (_).
--
-- Invalid values or unsupported keys will be ignored.
withApplicationValue :: Text -> Text -> ApplicationInfo -> ApplicationInfo
withApplicationValue _ "" info = info
withApplicationValue name value info@(ApplicationInfo map)
    | (name `elem` ["id", "version"]) == False = info
    | T.length(value) > 64 = info
    | (all (`elem` ['a'..'z'] ++ ['A' .. 'Z'] ++ ['0' .. '9'] ++ ['.', '-', '_']) (T.unpack value)) == False = info
    | otherwise = ApplicationInfo $ insertKey name value map

getApplicationInfoHeader :: ApplicationInfo -> Maybe Text
getApplicationInfoHeader (ApplicationInfo values)
    | AesonCompat.null values = Nothing
    | otherwise = toList values
        & sortBy (comparing fst)
        & map makeTag
        & T.unwords
        & Just
    where makeTag (key, value) = "application-" <> key <> "/" <> value