{-# LANGUAGE OverloadedLists #-}
-- | Contains an Haskell implementation (or mock implementation) of some ruby functions found in puppetlabs modules.
module Puppet.Runner.Puppetlabs (extFunctions) where

import           XPrelude

import           Crypto.Hash         as Crypto
import           Data.ByteString     (ByteString)
import           Data.Char           (isDigit)
import           Data.Foldable       (foldlM)
import qualified Data.HashMap.Strict as Map
import           Data.Scientific     as Sci
import qualified Data.Text           as Text
import qualified Data.Text.Encoding  as Text
import           Data.Vector         (Vector)
import           Formatting          (scifmt, sformat, (%), (%.))
import qualified Formatting          as FMT
import qualified System.Directory    as Directory
import           System.FilePath     ((</>), (<.>))
import           System.Random       (mkStdGen, randomRs)

import           Puppet.Interpreter

md5 :: Text -> Text
md5 = Text.pack . show . (Crypto.hash :: ByteString -> Digest MD5) . Text.encodeUtf8

extFun :: [(Text, Text, [PValue] -> InterpreterMonad PValue)]
extFun =  [ ("apache", "bool2httpd", apacheBool2httpd)
          , ("docker", "docker_swarm_join_flags", mockDockerSwarmJoinFlags)
          , ("docker", "docker_swarm_init_flags", mockDockerSwarmInitFlags)
          , ("docker", "docker_run_flags", mockDockerRunFlags)
          , ("docker", "docker_stack_flags", mockDockerStackFlags)
          , ("docker", "sanitised_name", dockerSanitisedName)
          , ("jenkins", "jenkins_port", mockJenkinsPort)
          , ("jenkins", "jenkins_prefix", mockJenkinsPrefix)
          , ("postgresql", "postgresql_acls_to_resources_hash", pgAclsToHash)
          , ("postgresql", "postgresql_password", pgPassword)
          , ("puppetdb", "puppetdb_create_subsetting_resource_hash", puppetdbCreateSubsettingResourceHash)
          , ("extlib", "random_password", randomPassword)
          , ("extlib", "cache_data", mockCacheData)
          , ("kubernetes", "kubeadm_init_flags", mockKubernetesInitFlags)
          , ("kubernetes", "kubeadm_join_flags", mockKubernetesJoinFlags)
          ]

-- | Build the map of available external functions.
--
-- If the ruby/puppet file is not found on the local filesystem the record is ignored. This is to avoid potential namespace conflict.
extFunctions :: FilePath -> IO (Container ( [PValue] -> InterpreterMonad PValue))
extFunctions modpath = foldlM f Map.empty extFun
  where
    f acc (nsp, name, fn) = do
      test <- testFile (toS nsp) name
      if test
         then pure $ Map.insert name fn acc
         else pure acc
    testFile nspath funcname =
      let funcpath0 = modpath </> nspath
          funcpath1 = funcpath0 </> "lib/puppet"
          funcpath2 = funcpath1 </> "parser/functions"
          funcpath3 = funcpath1 </> "functions"
      in
      isJust <$> Directory.findFile [ funcpath0 </> "functions"] (toS funcname <.> "pp")
      ||^
      isJust <$> Directory.findFile [ funcpath2
                                    , funcpath3
                                    , funcpath2 </> nspath
                                    , funcpath3 </> nspath
                                    ] (toS funcname <.> "rb")

apacheBool2httpd :: MonadThrowPos m => [PValue] -> m PValue
apacheBool2httpd [PBoolean True]  = pure $ PString "On"
apacheBool2httpd [PString "true"] = pure $ PString "On"
apacheBool2httpd [_]              = pure $ PString "Off"
apacheBool2httpd arg@_            = throwPosError $ "expect one single argument" <+> pretty arg

pgPassword :: MonadThrowPos m => [PValue] -> m PValue
pgPassword [PString username, PString pwd] =
    return $ PString $ "md5" <> md5 (pwd <> username)
pgPassword _ = throwPosError "expects 2 string arguments"

-- | The function is pure and always return the same "random" password.
randomPassword :: MonadThrowPos m => [PValue] -> m PValue
randomPassword [PNumber s] =
  PString . Text.pack . randomChars <$> scientificToInt s
  where
    randomChars n = take n $ randomRs ('a', 'z') (mkStdGen 1)

randomPassword _ = throwPosError "expect one single string arguments"


-- To be implemented if needed.
mockJenkinsPrefix :: MonadThrowPos m => [PValue] -> m PValue
mockJenkinsPrefix []    = return $ PString ""
mockJenkinsPrefix arg@_ = throwPosError $ "expect no argument" <+> pretty arg

-- To be implemented if needed.
mockJenkinsPort :: MonadThrowPos m => [PValue] -> m PValue
mockJenkinsPort []    = return $ PString "8080"
mockJenkinsPort arg@_ = throwPosError $ "expect no argument" <+> pretty arg

mockCacheData :: MonadThrowPos m => [PValue] -> m PValue
mockCacheData [_, _, b] = return b
mockCacheData arg@_     = throwPosError $ "expect 3 string arguments" <+> pretty arg

-- | Simple implemenation that does not handle all cases.
-- For instance 'auth_option' is currently not implemented.
-- Please add cases as needed.
pgAclsToHash :: MonadThrowPos m => [PValue] -> m PValue
pgAclsToHash [PArray as, PString ident, PNumber offset] = PHash <$> aclsToHash as ident offset
pgAclsToHash _ = throwPosError "expects 3 arguments; one array one string and one number"

aclsToHash :: MonadThrowPos m  => Vector PValue -> Text -> Scientific -> m (Container PValue)
aclsToHash vec ident offset = ifoldlM f Map.empty vec
  where
    f :: MonadThrowPos m => Int -> Container PValue -> PValue -> m (Container PValue)
    f idx acc (PString acl) = do
      let order = offset + scientific (toInteger idx) 0
          keymsg = sformat ("postgresql class generated rule " % FMT.stext % " " % FMT.int) ident idx
      x <- aclToHash (Text.words acl) order
      return $ Map.insert keymsg x acc
    f _ _ pval = throwPosError $ "expect a string as acl but get" <+> pretty pval

aclToHash :: (MonadThrowPos m) => [Text] -> Scientific -> m PValue
aclToHash acl@(typ : db : usr : remaining) order = analyze
  where
    fin remn hs = return $ PHash $
        if null remn
          then hs
          else Map.insert "auth_option" (PString (Text.unwords remn)) hs
    analyze = case remaining of
                method : remn | typ == "local" ->
                  fin remn $ baseHash & at "auth_method" ?~ PString method
                addr : msk : method : remn | Text.all isDigit msk ->
                  fin remn $ baseHash & at "address" ?~ PString (Text.unwords [addr,msk])
                                      & at "auth_method" ?~ PString method
                addr : method : remn ->
                  fin remn $ baseHash & at "address" ?~ PString addr
                                      & at "auth_method" ?~ PString method
                _ -> throwPosError $ "Unable to parse acl line" <+> squotes (ppline (Text.unwords acl))
    baseHash = [ ("type", PString "local")
               , ("database", PString db )
               , ("user", PString usr)
               , ("order", PString (sformat (FMT.left 3 '0' %. scifmt Sci.Fixed (Just 0))  order))
               ]
aclToHash acl _ = throwPosError $ "Unable to parse acl line" <+> squotes (ppline (Text.unwords acl))

-- faked implementation, replace by the correct one if you need so.
mockDockerRunFlags :: MonadThrowPos m => [PValue] -> m PValue
mockDockerRunFlags arg@[PHash _]= (pure . PString . show . head) arg
mockDockerRunFlags  arg@_ = throwPosError $ "Expect an hash as argument but was" <+> pretty arg

-- faked implementation, replace by the correct one if you need so.
mockDockerStackFlags :: MonadThrowPos m => [PValue] -> m PValue
mockDockerStackFlags arg@[PHash _]= (pure . PString . show . head) arg
mockDockerStackFlags  arg@_ = throwPosError $ "Expect an hash as argument but was" <+> pretty arg

-- faked implementation, replace by the correct one if you need so.
mockDockerSwarmJoinFlags :: MonadThrowPos m => [PValue] -> m PValue
mockDockerSwarmJoinFlags arg@[PHash _]= (pure . PString . show . head) arg
mockDockerSwarmJoinFlags  arg@_ = throwPosError $ "Expect an hash as argument but was" <+> pretty arg

-- faked implementation, replace by the correct one if you need so.
mockDockerSwarmInitFlags :: MonadThrowPos m => [PValue] -> m PValue
mockDockerSwarmInitFlags arg@[PHash _]= (pure . PString . show . head) arg
mockDockerSwarmInitFlags  arg@_ = throwPosError $ "Expect an hash as argument but was" <+> pretty arg

-- faked implementation, replace by the correct one if you need so.
mockKubernetesInitFlags :: MonadThrowPos m => [PValue] -> m PValue
mockKubernetesInitFlags arg@[PHash _]= (pure . PString . show . head) arg
mockKubernetesInitFlags  arg@_ = throwPosError $ "Expect an hash as argument but was" <+> pretty arg

-- faked implementation, replace by the correct one if you need so.
mockKubernetesJoinFlags :: MonadThrowPos m => [PValue] -> m PValue
mockKubernetesJoinFlags arg@[PHash _]= (pure . PString . show . head) arg
mockKubernetesJoinFlags  arg@_ = throwPosError $ "Expect an hash as argument but was" <+> pretty arg

-- utils
scientificToInt :: MonadThrowPos m => Scientific -> m Int
scientificToInt s = maybe (throwPosError $ "Unable to convert" <+> pretty s <+> "into an int.")
                          pure
                          (Sci.toBoundedInteger s)

-- https://github.com/puppetlabs/puppetlabs-puppetdb/blob/master/lib/puppet/parser/functions/puppetdb_create_subsetting_resource_hash.rb
puppetdbCreateSubsettingResourceHash :: MonadThrowPos m => [PValue] -> m PValue
puppetdbCreateSubsettingResourceHash [PHash s, PHash args] = do
  let res_hash = [ (k, PHash h)
                 | (k,v) <- itoList s
                 , let h = [ ( "subsetting", PString k) , ("value", v)] `Map.union` args
                 ]
  pure $ PHash (Map.fromList res_hash)
puppetdbCreateSubsettingResourceHash arg@_ = throwPosError $ "Expect 2 hashes as arguments but was" <+> pretty arg

-- To be implemented if needed.
dockerSanitisedName :: MonadThrowPos m => [PValue] -> m PValue
dockerSanitisedName [PString s] =
  -- ruby implementation: regsubst($name, '[^0-9A-Za-z.\-_]', '-', 'G')
  pure $ PString s
dockerSanitisedName arg@_ = throwPosError $ "Expect an hash as argument but was" <+> pretty arg