module LaunchDarkly.Server.Integrations.TestData.FlagBuilder
    ( FlagBuilder(..)
    , UserKey
    , VariationIndex
    , newFlagBuilder
    , booleanFlag
    , on
    , fallthroughVariation
    , offVariation
    , variationForAllUsers
    , valueForAllUsers
    , variationForUser
    , variations
    , buildFlag
    , UserAttribute
    , ifMatch
    , ifNotMatch
    , FlagRuleBuilder
    , andMatch
    , andNotMatch
    , thenReturn
    , Variation
    )
    where

import qualified Data.Aeson as Aeson
import           Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import           Data.Text (Text)
import qualified Data.Text as T
import           GHC.Natural (Natural)
import qualified LaunchDarkly.Server.Features as F
import qualified LaunchDarkly.Server.Operators as Op
import           Data.Function ((&))

type UserKey = Text
type VariationIndex = Integer

trueVariationForBoolean, falseVariationForBoolean :: VariationIndex
trueVariationForBoolean = 0
falseVariationForBoolean = 1

variationForBoolean :: Bool -> VariationIndex
variationForBoolean True = trueVariationForBoolean
variationForBoolean False = falseVariationForBoolean

-- |
-- A builder for feature flag configurations to be used with "LaunchDarkly.Server.Integrations.TestData".
-- 
-- see 'LaunchDarkly.Server.Integrations.TestData.flag' and
-- 'LaunchDarkly.Server.Integrations.TestData.update'
data FlagBuilder = FlagBuilder
    { fbKey :: Text
    , fbOffVariation :: Maybe VariationIndex
    , fbOn :: Bool
    , fbFallthroughVariation :: Maybe VariationIndex
    , fbVariations :: [Aeson.Value]
    , fbTargetMap :: Map UserKey VariationIndex
    , fbRules :: [FlagRule]
    } deriving (Show)

fbTargets :: FlagBuilder -> [F.Target]
fbTargets flagBuilder =
    Map.elems $
    Map.mapWithKey (flip F.Target) $
    Map.foldrWithKey go mempty (fbTargetMap flagBuilder)
  where
    go userKey variation =
        Map.insertWith (<>) variation [userKey]

buildFlag :: Natural -> FlagBuilder -> F.Flag
buildFlag version flagBuilder =
    F.Flag
        { F.key = fbKey flagBuilder
        , F.version = version
        , F.on = fbOn flagBuilder
        , F.trackEvents = False
        , F.trackEventsFallthrough = False
        , F.deleted = False
        , F.prerequisites = []
        , F.salt = "salt"
        , F.targets = fbTargets flagBuilder
        , F.rules = mapWithIndex convertFlagRule (fbRules flagBuilder)
        , F.fallthrough = F.VariationOrRollout (fbFallthroughVariation flagBuilder) Nothing
        , F.offVariation = fbOffVariation flagBuilder
        , F.variations = fbVariations flagBuilder
        , F.debugEventsUntilDate = Nothing
        , F.clientSideAvailability = F.ClientSideAvailability False False False
        }

mapWithIndex :: Integral num => (num -> a -> b) -> [a] -> [b]
mapWithIndex f l =
    fmap (uncurry f) (zip [0..] l)

newFlagBuilder :: Text -> FlagBuilder
newFlagBuilder key =
    FlagBuilder
        { fbKey = key
        , fbOffVariation = Nothing
        , fbOn = True
        , fbFallthroughVariation = Nothing
        , fbVariations = mempty
        , fbTargetMap = mempty
        , fbRules = mempty
        }

booleanFlagVariations :: [Aeson.Value]
booleanFlagVariations = [Aeson.Bool True, Aeson.Bool False]

isBooleanFlag :: FlagBuilder -> Bool
isBooleanFlag flagBuilder
  | booleanFlagVariations == fbVariations flagBuilder = True
  | otherwise = False

-- | 
-- A shortcut for setting the flag to use the standard boolean configuration.
--
-- This is the default for all new flags created with 'LaunchDarkly.Server.Integrations.TestData.flag'. The flag
-- will have two variations, @True@ and @False@ (in that order); it will return
-- @False@ whenever targeting is off, and @True@ when targeting is on if no other
-- settings specify otherwise.
booleanFlag :: FlagBuilder -> FlagBuilder
booleanFlag flagBuilder
    | isBooleanFlag flagBuilder =
        flagBuilder
    | otherwise =
        flagBuilder
            & variations booleanFlagVariations
            & fallthroughVariation trueVariationForBoolean
            & offVariation falseVariationForBoolean
-- |
-- Sets targeting to be on or off for this flag.
--
-- The effect of this depends on the rest of the flag configuration, just as it does on the
-- real LaunchDarkly dashboard. In the default configuration that you get from calling
-- 'LaunchDarkly.Server.Integrations.TestData.flag' with a new flag key, the flag will return @False@ 
-- whenever targeting is off, and @True@ when targeting is on.
on :: Bool -- ^ isOn @True@ if targeting should be on
   -> FlagBuilder
   -> FlagBuilder
on isOn fb =
    fb{ fbOn = isOn }

-- |
-- Removes any existing rules from the flag. 
-- This undoes the effect of methods like 'ifMatch' or 'ifNotMatch'
clearRules :: FlagBuilder -> FlagBuilder
clearRules fb =
    fb{ fbRules = mempty }

-- |
-- Removes any existing user targets from the flag. 
-- This undoes the effect of methods like 'variationForUser'
clearUserTargets :: FlagBuilder -> FlagBuilder
clearUserTargets fb =
    fb{ fbTargetMap = mempty }

-- |
-- Sets the flag to always return the specified variation value for all users.
--
-- The value may be of any type that implements 'Aeson.ToJSON'. This method changes the
-- flag to have only a single variation, which is this value, and to return the same
-- variation regardless of whether targeting is on or off. Any existing targets or rules
-- are removed.
valueForAllUsers :: Aeson.ToJSON value
                 => value -- the desired value to be returned for all users
                 -> FlagBuilder
                 -> FlagBuilder
valueForAllUsers val fb =
    fb & variations [Aeson.toJSON val]
       & variationForAllUsers (0 :: VariationIndex)

-- |
-- Changes the allowable variation values for the flag.
--
-- The value may be of any JSON type, as defined by 'Aeson.Value'. For instance, a boolean flag
-- normally has [toJSON True, toJSON False]; a string-valued flag might have
-- [toJSON "red", toJSON "green"]; etc.
variations :: [Aeson.Value] -- ^ the desired variations
           -> FlagBuilder
           -> FlagBuilder
variations values fb =
    fb{ fbVariations = values }

-- Should this actually use overloaded function names?
class Variation val where
    -- |
    -- Specifies the fallthrough variation. The fallthrough is the value
    -- that is returned if targeting is on and the user was not matched by a more specific
    -- target or rule.
    --
    -- If the flag was previously configured with other variations and the variation specified is a boolean,
    -- this also changes it to a boolean flag.
    fallthroughVariation :: val -- ^ @True@ or @False@ or the desired fallthrough variation index: 0 for the first, 1 for the second, etc.
                         -> FlagBuilder
                         -> FlagBuilder

    -- | 
    -- Specifies the off variation for a flag. This is the variation that is returned
    -- whenever targeting is off.
    --
    -- If the flag was previously configured with other variations and the variation specified is a boolean,
    -- this also changes it to a boolean flag.
    offVariation :: val -- ^ @True@ or @False@ or the desired fallthrough variation index: 0 for the first, 1 for the second, etc.
                 -> FlagBuilder
                 -> FlagBuilder

    -- |
    -- Sets the flag to always return the specified variation for all users.
    --
    -- The variation is specified, Targeting is switched on, and any existing targets or rules are removed.
    -- The fallthrough variation is set to the specified value. The off variation is left unchanged.
    --
    -- If the flag was previously configured with other variations and the variation specified is a boolean,
    -- this also changes it to a boolean flag.
    variationForAllUsers :: val -- ^ @True@ or @False@ or the desired fallthrough variation index: 0 for the first, 1 for the second, etc.
                         -> FlagBuilder
                         -> FlagBuilder

    -- |
    -- Sets the flag to return the specified variation for a specific user key when targeting
    -- is on.
    --
    -- This has no effect when targeting is turned off for the flag.
    --
    -- If the flag was previously configured with other variations and the variation specified is a boolean,
    -- this also changes it to a boolean flag.
    variationForUser :: UserKey -- ^ a user key to target
                     -> val -- ^ @True@ or @False@ or the desired fallthrough variation index: 0 for the first, 1 for the second, etc.
                     -> FlagBuilder
                     -> FlagBuilder

    -- |
    -- Finishes defining the rule, specifying the result as either a boolean
    -- or a variation index.
    -- 
    -- If the flag was previously configured with other variations and the variation specified is a boolean,
    -- this also changes it to a boolean flag.
    thenReturn :: val -- ^ @True@ or @False@ or the desired fallthrough variation index: 0 for the first, 1 for the second, etc.
               -> FlagRuleBuilder
               -> FlagBuilder

instance Variation Integer where
    fallthroughVariation variationIndex fb =
        fb{ fbFallthroughVariation = Just variationIndex }

    offVariation variationIndex fb =
        fb{ fbOffVariation = Just variationIndex }

    variationForAllUsers variationIndex fb =
        fb & on True
           & clearRules
           & clearUserTargets
           & fallthroughVariation variationIndex

    variationForUser userKey variationIndex fb =
        fb{ fbTargetMap = Map.insert userKey variationIndex (fbTargetMap fb) }

    thenReturn variationIndex ruleBuilder =
        let fb = frbBaseBuilder ruleBuilder
        in fb{ fbRules = FlagRule (frbClauses ruleBuilder) variationIndex : fbRules fb }

instance Variation Bool where
    fallthroughVariation value fb =
        fb & booleanFlag
           & fallthroughVariation (variationForBoolean value)
    offVariation value fb =
        fb & booleanFlag
           & offVariation (variationForBoolean value)
    variationForAllUsers value fb =
        fb & booleanFlag
           & variationForAllUsers (variationForBoolean value)
    variationForUser userKey value fb =
        fb & booleanFlag
           & variationForUser userKey (variationForBoolean value)
    thenReturn value ruleBuilder =
        ruleBuilder { frbBaseBuilder = booleanFlag $ frbBaseBuilder ruleBuilder }
            & thenReturn (variationForBoolean value)

type UserAttribute = Text

-- |
-- Starts defining a flag rule, using the "is one of" operator.
--
-- For example, this creates a rule that returns @True@ if the name is \"Patsy\" or \"Edina\":
-- 
-- @
-- testData
--     & flag "flag"
--     & ifMatch "name" [toJSON \"Patsy\", toJSON \"Edina\"]
--     & thenReturn True
-- @
ifMatch :: UserAttribute -- ^ attribute the user attribute to match against
        -> [Aeson.Value] -- ^ values to compare to
        -> FlagBuilder
        -> FlagRuleBuilder -- ^ call 'thenReturn' to finish the rule, or add more tests with 'andMatch' or 'andNotMatch' 
ifMatch userAttribute values fb =
    newFlagRuleBuilder fb
     & andMatch userAttribute values

-- |
-- Starts defining a flag rule, using the "is not one of" operator.
--
-- For example, this creates a rule that returns @True@ if the name is neither \"Saffron\" nor \"Bubble\"
-- 
-- @
-- testData
--     & flag "flag"
--     & ifNotMatch "name" [toJSON \"Saffron\", toJSON \"Bubble\"]
--     & thenReturn True
-- @
ifNotMatch :: UserAttribute -- ^ attribute the user attribute to match against
           -> [Aeson.Value] -- ^ values to compare to
           -> FlagBuilder
           -> FlagRuleBuilder -- ^ call 'thenReturn' to finish the rule, or add more tests with 'andMatch' or 'andNotMatch' 
ifNotMatch userAttribute values fb =
    newFlagRuleBuilder fb
     & andNotMatch userAttribute values

data Clause = Clause
    { clauseAttribute :: UserAttribute
    , clauseValues    :: [Aeson.Value]
    , clauseNegate    :: Bool
    } deriving (Show)

data FlagRule = FlagRule
    { frClauses :: [Clause]
    , frVariation :: VariationIndex
    } deriving (Show)

convertFlagRule :: Integer -> FlagRule -> F.Rule
convertFlagRule idx flagRule =
    F.Rule
        { F.id = T.pack $ "rule" <> show idx
        , F.variationOrRollout = F.VariationOrRollout (Just $ frVariation flagRule) Nothing
        , F.clauses = fmap convertClause (frClauses flagRule)
        , F.trackEvents = False
        }

convertClause :: Clause -> F.Clause
convertClause clause =
    F.Clause
        { F.attribute = clauseAttribute clause
        , F.negate = clauseNegate clause
        , F.values = clauseValues clause
        , F.op = Op.OpIn
        }
-- |
-- A builder for feature flag rules to be used with 'FlagBuilder'.
--
-- In the LaunchDarkly model, a flag can have any number of rules, and a rule can have any number of
-- clauses. A clause is an individual test such as \"name is \'X\'\". A rule matches a user if all of the
-- rule's clauses match the user.
--
-- To start defining a rule, use one of the matching functions such as 'ifMatch' or 'ifNotMatch'. 
-- This defines the first clause for the rule.
-- Optionally, you may add more clauses with the rule builder functions such as 'andMatch' and 'andNotMatch'. 
-- Finally, call 'thenReturn' to finish defining the rule.
data FlagRuleBuilder = FlagRuleBuilder
    { frbClauses :: [Clause]
    , frbBaseBuilder :: FlagBuilder
    } deriving (Show)

newFlagRuleBuilder :: FlagBuilder -> FlagRuleBuilder
newFlagRuleBuilder baseBuilder =
    FlagRuleBuilder
        { frbClauses = mempty
        , frbBaseBuilder = baseBuilder
        }
-- |
-- Adds another clause, using the "is one of" operator.
--
-- For example, this creates a rule that returns @True@ if the name is \"Patsy\" and the
-- country is \"gb\":
-- 
-- @
-- testData 
--     & flag "flag"
--     & ifMatch "name" [toJSON \"Patsy\"]
--     & andMatch "country" [toJSON \"gb\"]
--     & thenReturn True
-- @ 
andMatch :: UserAttribute -- ^ the user attribute to match against
         -> [Aeson.Value] -- ^ values to compare to
         -> FlagRuleBuilder
         -> FlagRuleBuilder
andMatch userAttribute values ruleBuilder =
    ruleBuilder{ frbClauses = Clause userAttribute values False : frbClauses ruleBuilder }

-- |
-- Adds another clause, using the "is not one of" operator.
--
-- For example, this creates a rule that returns @True@ if the name is \"Patsy\" and the
-- country is not \"gb\":
-- 
-- @
-- testData 
--     & flag "flag"
--     & ifMatch "name" [toJSON \"Patsy\"]
--     & andNotMatch "country" [toJSON \"gb\"]
--     & thenReturn True
-- @ 
andNotMatch :: UserAttribute -- ^ the user attribute to match against
            -> [Aeson.Value] -- ^ values to compare to
            -> FlagRuleBuilder
            -> FlagRuleBuilder
andNotMatch userAttribute values ruleBuilder =
    ruleBuilder{ frbClauses = Clause userAttribute values True : frbClauses ruleBuilder }