{-# LANGUAGE LambdaCase, TemplateHaskell, MultiParamTypeClasses, FunctionalDependencies, FlexibleInstances #-}
{- | This module runs a Hiera server that caches Hiera data. There is
a huge caveat : only the data files are watched for changes, not the main configuration file.

A minor bug is that interpolation will not work for inputs containing the % character when it isn't used for interpolation.
-}
module Hiera.Server (startHiera,dummyHiera,HieraQueryFunc) where

import qualified Data.FileCache as F
import qualified Data.Yaml as Y
import qualified Data.Aeson as A
import Data.Aeson (FromJSON,Value(..),(.:?),(.!=))
import qualified Data.Either.Strict as S
import qualified Data.Maybe.Strict as S
import qualified Data.Text as T
import qualified Data.Attoparsec.Text as AT
import qualified Data.ByteString.Lazy as BS
import qualified Data.Vector as V
import qualified Data.HashMap.Strict as HM
import Data.Tuple.Strict
import Control.Monad.Writer.Strict

import Control.Applicative
import Control.Lens
import Control.Lens.Aeson
import System.FilePath.Lens (directory)
import Control.Exception

import Puppet.PP hiding ((<$>))
import Puppet.Interpreter.Types
import Puppet.Interpreter.Resolve
import Puppet.Utils (strictifyEither)

data HieraConfig = HieraConfig { _hieraconfigBackends  :: [HieraBackend]
                               , _hieraconfigHierarchy :: [InterpolableHieraString]
                               , _hieraconfigBasedir   :: FilePath
                               } deriving Show

data HieraBackend = YamlBackend FilePath
                  | JsonBackend FilePath
                  deriving Show

newtype InterpolableHieraString = InterpolableHieraString [HieraStringPart]
                                  deriving Show

data HieraStringPart = HString T.Text
                     | HVariable T.Text
                     deriving Show

instance Pretty HieraStringPart where
    pretty (HString t) = ttext t
    pretty (HVariable v) = dullred (string "%{" <> ttext v <> string "}")
    prettyList = mconcat . map pretty

type HieraCache = F.FileCacheR Doc Y.Value

makeFields ''HieraConfig

instance FromJSON InterpolableHieraString where
    parseJSON (String s) = case parseInterpolableString s of
                               Right x -> return (InterpolableHieraString x)
                               Left rr -> fail rr
    parseJSON _ = fail "Invalid value type"

instance FromJSON HieraConfig where
    parseJSON (Object v) = do
        let genBackend :: T.Text -> Y.Parser HieraBackend
            genBackend backendname = do
                (backendConstructor, skey) <- case backendname of
                                                  "yaml" -> return (YamlBackend, ":yaml")
                                                  "json" -> return (JsonBackend, ":json")
                                                  _ -> fail ("Unknown backend " ++ T.unpack backendname)
                datadir <- case (Object v) ^? key skey . key ":datadir" of
                                  Just (String dtdir) -> return dtdir
                                  Just _              -> fail ":datadir should be a string"
                                  Nothing             -> return "/etc/puppet/hieradata"
                return (backendConstructor (T.unpack datadir))
        HieraConfig
            <$> (v .:? ":backends" .!= ["yaml"] >>= mapM genBackend)
            <*> (v .:? ":hierarchy" .!= [InterpolableHieraString [HString "common"]])
            <*> pure "/etc/puppet/hieradata"
    parseJSON _ = fail "Not a valid Hiera configuration"

-- | An attoparsec parser that turns text into parts that are ready for
-- interpolation
interpolableString :: AT.Parser [HieraStringPart]
interpolableString = AT.many1 (fmap HString rawPart <|> fmap HVariable interpPart)
    where
        rawPart = AT.takeWhile1 (/= '%')
        interpPart = AT.string "%{" *> AT.takeWhile1 (/= '}') <* AT.char '}'

parseInterpolableString :: T.Text -> Either String [HieraStringPart]
parseInterpolableString t = AT.parseOnly interpolableString t

-- | The only method you'll ever need. It runs a Hiera server and gives you
-- a querying function. The 'Nil' output is explicitely given as a Maybe
-- type.
startHiera :: FilePath -> IO (Either String (HieraQueryFunc))
startHiera hieraconfig = Y.decodeFileEither hieraconfig >>= \case
    Left ex -> return (Left (show ex))
    Right cfg -> do
        let ncfg = cfg & basedir .~ (hieraconfig ^. directory) <> "/"
        cache <- F.newFileCache
        return (Right (query ncfg cache))

-- | A dummy hiera function that will be used when hiera is not detected
dummyHiera :: HieraQueryFunc
dummyHiera _ _ _ = return (S.Right ([] :!: S.Nothing))

-- | The combinator for "normal" queries
queryCombinator :: [LogWriter (S.Maybe PValue)] -> LogWriter (S.Maybe PValue)
queryCombinator [] = return S.Nothing
queryCombinator (x:xs) = x >>= \case
    v@(S.Just _) -> return v
    S.Nothing -> queryCombinator xs

-- | The combinator for hiera_array
queryCombinatorArray :: [LogWriter (S.Maybe PValue)] -> LogWriter (S.Maybe PValue)
queryCombinatorArray = fmap rejoin . sequence
    where
        rejoin = S.Just . PArray . V.concat . map toA
        toA S.Nothing = V.empty
        toA (S.Just (PArray r)) = r
        toA (S.Just a) = V.singleton a

-- | The combinator for hiera_array
queryCombinatorHash :: [LogWriter (S.Maybe PValue)] -> LogWriter (S.Maybe PValue)
queryCombinatorHash = fmap (S.Just . PHash . mconcat . map toH) . sequence
    where
        toH S.Nothing = mempty
        toH (S.Just (PHash h)) = h
        toH _ = throw (ErrorCall "The hiera value was not a hash")

interpolateText :: Container ScopeInformation -> T.Text -> T.Text
interpolateText vars t = case (parseInterpolableString t ^? _Right) >>= resolveInterpolable vars of
                             Just x -> x
                             Nothing -> t

resolveInterpolable :: Container ScopeInformation -> [HieraStringPart] -> Maybe T.Text
resolveInterpolable vars = fmap T.concat . mapM (resolveInterpolablePart vars)

resolveInterpolablePart :: Container ScopeInformation -> HieraStringPart -> Maybe T.Text
resolveInterpolablePart _ (HString x) = Just x
resolveInterpolablePart vars (HVariable v) = getVariable vars "::" v ^? _Right . _PString

interpolatePValue :: Container ScopeInformation -> PValue -> PValue
interpolatePValue v (PHash h) = PHash . HM.fromList . map ( (_1 %~ interpolateText v) . (_2 %~ interpolatePValue v) ) . HM.toList $ h
interpolatePValue v (PArray r) = PArray (fmap (interpolatePValue v) r)
interpolatePValue v (PString t) = PString (interpolateText v t)
interpolatePValue _ x = x

type LogWriter = WriterT InterpreterWriter IO

query :: HieraConfig -> HieraCache -> HieraQueryFunc
query (HieraConfig b h bd) cache vars hquery qtype = fmap (S.Right . prepout) (runWriterT (sequencerFunction (map query' h))) `catch` (\e -> return . S.Left . string . show $ (e :: SomeException))
    where
        prepout (a,s) = s :!: a
        sequencerFunction = case qtype of
                                Priority   -> queryCombinator
                                ArrayMerge -> queryCombinatorArray
                                HashMerge  -> queryCombinatorHash
        query' :: InterpolableHieraString -> LogWriter (S.Maybe PValue)
        query' (InterpolableHieraString strs) =
            case resolveInterpolable vars strs of
                Just s -> sequencerFunction (map (query'' s) b)
                Nothing -> warn ("Hiera: could not interpolate " <> pretty strs) >> return S.Nothing
        query'' :: T.Text -> HieraBackend -> LogWriter (S.Maybe PValue)
        query'' hieraname backend = do
            let (decodefunction, datadir, extension) = case backend of
                                                (JsonBackend d) -> (fmap (strictifyEither . (_Left %~ string). A.eitherDecode') . BS.readFile       , d, ".json")
                                                (YamlBackend d) -> (fmap (strictifyEither . (_Left %~ string . show))           . Y.decodeFileEither, d, ".yaml")
                filename = mbd <> datadir <> "/" <> T.unpack hieraname <> extension
                    where
                        mbd = case datadir of
                                  '/' : _ -> mempty
                                  _ -> bd
                mfromJSON :: Maybe Value -> LogWriter (S.Maybe PValue)
                mfromJSON Nothing = return S.Nothing
                mfromJSON (Just v) = case A.fromJSON v of
                                         A.Success a -> return (S.Just (interpolatePValue vars a))
                                         _ -> warn ("Hiera: could not convert this Value to a Puppet type: " <> string (show v)) >> return S.Nothing
            v <- liftIO (F.query cache filename (decodefunction filename))
            case v of
                S.Left r -> debug ("Hiera: error when reading file " <> string filename <+> r) >> return S.Nothing
                S.Right x -> mfromJSON (x ^? key hquery)