{-# LANGUAGE TemplateHaskell #-}
--  Private modules.
--  Function and data types that are used to define the native types.
module Puppet.Language.NativeTypes.Helpers
  ( module Exports
  , ipaddr
  , nativetypemethods
  , NativeTypeName
  , NativeTypeValidate
  , NativeTypeMethods
  , HasNativeTypeMethods(..)
  , paramname
  , rarray
  , string
  , strings
  , string_s
  , noTrailingSlash
  , fullyQualified
  , fullyQualifieds
  , values
  , defaultvalue
  , nameval
  , defaultValidate
  , integer
  , integers
  , mandatory
  , mandatoryIfNotAbsent
  , inrange
  , faketype
  , defaulttype
  , runarray
  , perror
  , validateSourceOrContent
  ) where

import           XPrelude                 as Exports

import           Data.Aeson.Lens          (_Integer, _Number)
import           Data.Char                (isDigit)
import qualified Data.HashMap.Strict      as Map
import qualified Data.HashSet             as HS
import qualified Data.Text                as Text
import qualified Data.Vector              as V

import           Puppet.Language.Core     as Exports
import           Puppet.Language.Resource as Exports
import           Puppet.Language.Value    as Exports
import qualified Text.Read


metaparameters :: HS.HashSet Text
metaparameters = HS.fromList ["tag","stage","name","title","alias","audit","check","loglevel","noop","schedule", "EXPORTEDSOURCE", "require", "before", "register", "notify"]

type NativeTypeName = Text
type NativeTypeValidate = Resource -> Either PrettyError Resource

-- | Attributes (and providers) of a puppet resource type bundled with validation rules
data NativeTypeMethods = NativeTypeMethods
  { _puppetValidate :: NativeTypeValidate
  , _puppetFields   :: HS.HashSet Text
  }

makeClassy ''NativeTypeMethods

paramname :: Text -> Doc
paramname = red . ppline

-- | Useful helper for buiding error messages
perror :: Doc -> Either PrettyError Resource
perror = Left . PrettyError

-- | Smart constructor for 'NativeTypeMethods'.
nativetypemethods :: [(Text, [Text -> NativeTypeValidate])] -> NativeTypeValidate -> NativeTypeMethods
nativetypemethods def extraV =
  let params = fromKeys def
  in NativeTypeMethods (defaultValidate params >=> parameterFunctions def >=> extraV)  params
  where
    fromKeys = HS.fromList . map fst

faketype :: NativeTypeName -> (NativeTypeName, NativeTypeMethods)
faketype tname = (tname, NativeTypeMethods Right HS.empty)

defaulttype :: NativeTypeName -> (NativeTypeName, NativeTypeMethods)
defaulttype tname = (tname, NativeTypeMethods (defaultValidate HS.empty) HS.empty)

{-| Validate resources given a list of valid parameters:

      * checks that no unknown parameters have been set (except metaparameters)
-}
defaultValidate :: HS.HashSet Text -> NativeTypeValidate
defaultValidate validparameters = checkParameterList validparameters >=> addDefaults

checkParameterList :: HS.HashSet Text -> NativeTypeValidate
checkParameterList validparameters res | HS.null validparameters = Right res
                                       | otherwise = if HS.null setdiff
                                            then Right res
                                            else perror $ "Unknown parameters: " <+> list (map paramname $ HS.toList setdiff)
    where
        keyset = HS.fromList $ Map.keys (res ^. rattributes)
        setdiff = HS.difference keyset (metaparameters `HS.union` validparameters)

-- | This validator always accept the resources, but add the default parameters (to be defined :)
addDefaults :: NativeTypeValidate
addDefaults res = Right (res & rattributes %~ newparams)
    where
        def PUndef = False
        def _      = True
        newparams p = Map.filter def $ Map.union p defaults
        defaults    = Map.empty

-- | Helper function that runs a validor on a 'PArray'
runarray :: Text -> (Text -> PValue -> NativeTypeValidate) -> NativeTypeValidate
runarray param func res = case res ^. rattributes . at param of
  Just (PArray x) -> V.foldM (flip (func param)) res x
  Just x          -> perror $ "Parameter" <+> paramname param <+> "should be an array, not" <+> pretty x
  Nothing         -> Right res

{-| This checks that a given parameter is a string. If it is a 'PBoolean' or
'PNumber' it will convert them to a string.
-}
string :: Text -> NativeTypeValidate
string param res = case res ^. rattributes . at param of
  Just x  -> string' param x res
  Nothing -> Right res

strings :: Text -> NativeTypeValidate
strings param = runarray param string'

-- | Validates a string or an array of strings
string_s :: Text -> NativeTypeValidate
string_s param res = case res ^. rattributes . at param of
  Nothing         -> Right res
  Just (PArray _) -> strings param res
  Just _          -> string param res

string' :: Text -> PValue -> NativeTypeValidate
string' param rev res = case rev of
  PString _      -> Right res
  PBoolean True  -> Right (res & rattributes . at param ?~ PString "true")
  PBoolean False -> Right (res & rattributes . at param ?~ PString "false")
  PNumber n      -> Right (res & rattributes . at param ?~ PString (scientific2text n))
  x              -> perror $ "Parameter" <+> paramname param <+> "should be a string, and not" <+> pretty x

-- | Makes sure that the parameter, if defined, has a value among this list.
values :: [Text] -> Text -> NativeTypeValidate
values valuelist param res = case res ^. rattributes . at param of
    Just (PString x) -> if x `elem` valuelist
        then Right res
        else perror $ "Parameter" <+> paramname param <+> "value should be one of" <+> list (map ppline valuelist) <+> "and not" <+> ppline x
    Just x  -> perror $ "Parameter" <+> paramname param <+> "value should be one of" <+> list (map ppline valuelist) <+> "and not" <+> pretty x
    Nothing -> Right res

-- | This fills the default values of unset parameters.
defaultvalue :: Text -> Text -> NativeTypeValidate
defaultvalue value param = Right . over (rattributes . at param) (Just . fromMaybe (PString value))

-- | Checks that a given parameter, if set, is a 'PNumber'.
-- If it is a 'PString' it will attempt to parse it.
integer :: Text -> NativeTypeValidate
integer prm res = string prm res >>= integer' prm
  where
    integer' pr rs = case rs ^. rattributes . at pr of
      Just x -> integer'' prm x res
      Nothing -> Right rs

integers :: Text -> NativeTypeValidate
integers param = runarray param integer''

integer'' :: Text -> PValue -> NativeTypeValidate
integer'' param val res = case val ^? _Integer of
  Just v -> Right (res & rattributes . at param ?~ PNumber (fromIntegral v))
  _ -> perror $ "Parameter" <+> paramname param <+> "must be an integer"

-- | Copies the "name" value into the parameter if this is not set.
-- It implies the `string` validator.
nameval :: Text -> NativeTypeValidate
nameval prm res =
  string prm res >>= \r ->
    case r ^. rattributes . at prm of
      Just (PString al) -> Right (res & rid . iname .~ al)
      Just x -> perror ("The alias must be a string, not" <+> pretty x)
      Nothing -> Right (r & rattributes . at prm ?~ PString (r ^. rid . iname))

-- | Checks that a given parameter is set unless the resources "ensure" is set to absent
mandatoryIfNotAbsent :: Text -> NativeTypeValidate
mandatoryIfNotAbsent param res =
  case res ^. rattributes . at param of
    Just _  -> Right res
    Nothing -> case res ^. rattributes . at "ensure" of
      Just "absent" -> Right res
      _ -> perror $ "Parameter" <+> paramname param <+> "should be set."

-- | Checks that a given parameter is set.
mandatory :: Text -> NativeTypeValidate
mandatory param res = case res ^. rattributes . at param of
    Just _  -> Right res
    Nothing -> perror $ "Parameter" <+> paramname param <+> "should be set."

-- Helper that takes a list of stuff and will generate a validator.
parameterFunctions :: [(Text, [Text -> NativeTypeValidate])] -> NativeTypeValidate
parameterFunctions argrules rs = foldM parameterFunctions' rs argrules
    where
    parameterFunctions' :: Resource -> (Text, [Text -> NativeTypeValidate]) -> Either PrettyError Resource
    parameterFunctions' r (param, validationfunctions) = foldM (parameterFunctions'' param) r validationfunctions
    parameterFunctions'' :: Text -> Resource -> (Text -> NativeTypeValidate) -> Either PrettyError Resource
    parameterFunctions'' param r validationfunction = validationfunction param r

-- checks that a parameter is fully qualified
fullyQualified :: Text -> NativeTypeValidate
fullyQualified param res = case res ^. rattributes . at param of
    Just path -> fullyQualified' param path res
    Nothing   -> Right res

noTrailingSlash :: Text -> NativeTypeValidate
noTrailingSlash param res =
  case res ^. rattributes . at param of
    Just (PString x) -> if Text.last x == '/'
                          then perror ("Parameter" <+> paramname param <+> "should not have a trailing slash")
                          else Right res
    _ -> Right res

fullyQualifieds :: Text -> NativeTypeValidate
fullyQualifieds param = runarray param fullyQualified'

fullyQualified' :: Text -> PValue -> NativeTypeValidate
fullyQualified' param path res =
  case path of
    PString ("") -> perror $ "Empty path for parameter" <+> paramname param
    PString p    -> if Text.head p == '/'
                      then Right res
                      else perror $ "Path must be absolute, not" <+> ppline p <+> "for parameter" <+> paramname param
    x            -> perror $ "SHOULD NOT HAPPEN: path is not a resolved string, but" <+> pretty x <+> "for parameter" <+> paramname param

rarray :: Text -> NativeTypeValidate
rarray param res = case res ^. rattributes . at param of
    Just (PArray _) -> Right res
    Just x          -> Right $ res & rattributes . at param ?~ PArray (V.singleton x)
    Nothing         -> Right res

ipaddr :: Text -> NativeTypeValidate
ipaddr param res =
  case res ^. rattributes . at param of
    Nothing -> Right res
    Just (PString ip) ->
        if checkipv4 ip 0
          then Right res
          else perror $ "Invalid IP address for parameter" <+> paramname param
    Just x -> perror $ "Parameter" <+> paramname param <+> "should be an IP address string, not" <+> pretty x

checkipv4 :: Text -> Int -> Bool
checkipv4 _  4 = False -- means that there are more than 4 groups
checkipv4 "" _ = False -- should never get an empty string
checkipv4 ip v =
    let (cur, nxt) = Text.break (=='.') ip
        nextfunc = if Text.null nxt
            then v == 3
            else checkipv4 (Text.tail nxt) (v+1)
        goodcur = not (Text.null cur) && Text.all isDigit cur && (let rcur = Text.Read.read (Text.unpack cur) :: Int in (rcur >= 0) && (rcur <= 255))
    in goodcur && nextfunc

inrange :: Integer -> Integer -> Text -> NativeTypeValidate
inrange mi ma param res =
    let va = res ^. rattributes . at param
        na = va ^? traverse . _Number
    in case (va,na) of
        (Nothing, _) -> Right res
        (_,Just v)   -> if (v >= fromIntegral mi) && (v <= fromIntegral ma)
                          then Right res
                          else perror $ "Parameter" <+> paramname param <> "'s value should be between" <+> pretty mi <+> "and" <+> pretty ma
        (Just x,_)   -> perror $ "Parameter" <+> paramname param <+> "should be an integer, and not" <+> pretty x

validateSourceOrContent :: NativeTypeValidate
validateSourceOrContent res = let
    parammap =  res ^. rattributes
    source    = Map.member "source"  parammap
    content   = Map.member "content" parammap
    in if source && content
         then perror "Source and content can't be specified at the same time"
         else Right res