{-# LANGUAGE AutoDeriveTypeable     #-}
{-# LANGUAGE DeriveGeneric          #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GADTs                  #-}
{-# LANGUAGE MultiParamTypeClasses  #-}
{-# LANGUAGE ScopedTypeVariables    #-}
{-# LANGUAGE TemplateHaskell        #-}

module Puppet.Interpreter.Types (
  -- * Operational state
   InterpreterState(InterpreterState)
 , scopes
 , definedResources
 , nestedDeclarations
 , resModifiers
 , extraRelations
 , curScope
 , curPos
 , loadedClasses
 -- * Operational reader
 , InterpreterReader(InterpreterReader)
 , readerNativeTypes
 , readerGetStatement
 , readerGetTemplate
 , readerPdbApi
 , readerExternalFunc
 , readerNodename
 , readerHieraQuery
 , readerIoMethods
 , readerIgnoredModules
 , readerExternalModules
 , readerIsStrict
 , readerPuppetPaths
 , readerFacts
 , readerRebaseFile
 -- * Interpreter monad
 , InterpreterMonad
 , InterpreterWriter
 , InterpreterInstr(..)
 , Strictness(..)
 -- * Io methods
 , IoMethods(IoMethods)
 , ioGetCurrentCallStack
 , ioReadFile
 , ioTraceEvent
 , MonadThrowPos(..)
 -- * Resource modifier
 , ResourceModifier(ResourceModifier)
 , rmResType
 , rmDeclaration
 , rmSearch
 , rmType
 , rmMutation
 , rmModifierType
 , ModifierType(..)
 , OverrideType(..)
 , ResourceCollectorType(..)
 , ClassIncludeType(..)
 , RSearchExpression(..)
 -- * Scope information
 , ScopeInformation(ScopeInformation)
 , scopeResDefaults
 , scopeVariables
 , scopeParent
 , scopeOverrides
 , scopeContainer
 , scopeExtraTags
 , CurContainer(CurContainer)
 , cctype
 , cctags
 -- * Resource default
 , ResDefaults(ResDefaults)
 , resDefValues
 , resDefSrcScope
 , resDefPos
 , resDefType
 , ResRefOverride(..)
 , ScopeEnteringContext(..)
 , TopLevelType(..)
 -- * Hiera
 , HieraQueryLayers(..)
 , globalLayer
 , environmentLayer
 , moduleLayer
 -- * Template
 , TemplateSource(..)
 -- * Re-export
 , module Puppet.Language
) where

import           XPrelude.Extra
import           XPrelude.PP

import qualified Control.Monad.Fail as Fail
import           Control.Monad.Operational
import           Control.Monad.State.Strict
import           Control.Monad.Writer.Class
import           Data.Aeson                 as A
import qualified Data.Either.Strict         as S
import qualified Data.HashMap.Strict        as HM
import qualified Data.List                  as List
import qualified Data.Maybe.Strict          as S
import qualified GHC.Show
import qualified GHC.Stack
import qualified System.Log.Logger          as Log

import           Facter
import           Hiera.Server
import           Puppet.Language
import           Puppet.Parser.Types
import           PuppetDB


-- | The intepreter can run in two modes : a strict mode (recommended), and
-- a permissive mode.
data Strictness
  = Strict
  | Permissive
  deriving (Show, Eq)

instance FromJSON Strictness where
  parseJSON (Bool True)  = pure Strict
  parseJSON (Bool False) = pure Permissive
  parseJSON _            = mzero

data RSearchExpression
  = REqualitySearch !Text !PValue
  | RNonEqualitySearch !Text !PValue
  | RAndSearch !RSearchExpression !RSearchExpression
  | ROrSearch !RSearchExpression !RSearchExpression
  | RAlwaysTrue
  deriving (Show, Eq)

-- | Puppet has two main ways to declare classes: include-like and resource-like.
--
-- See <https://docs.puppetlabs.com/puppet/latest/reference/lang_classes.html#include-like-vs-resource-like puppet reference>.
data ClassIncludeType
  = ClassIncludeLike -- ^ Using the include or contain function
  | ClassResourceLike -- ^ Resource like declaration
  deriving (Eq)

-- | Differentiate the distinct top level types such as node, define or class.
data TopLevelType
  = TopNode -- ^ For node entries
  | TopDefine -- ^ For defines
  | TopClass -- ^ For classes
  deriving (Generic, Eq)

instance Hashable TopLevelType

-- | From the evaluation of Resource Default Declaration.
data ResDefaults = ResDefaults
  { _resDefType     :: !Text
  , _resDefSrcScope :: !Text
  , _resDefValues   :: !(Container PValue)
  , _resDefPos      :: !PPosition
  }

-- | From the evaluation of Resource Override Declaration.
data ResRefOverride = ResRefOverride
  { _rrid     :: !RIdentifier
  , _rrparams :: !(Container PValue)
  , _rrpos    :: !PPosition
  } deriving (Eq)

data ScopeEnteringContext
  = SENormal
  | SEChild !Text -- ^ We enter the scope as the child of another class
  | SEParent !Text -- ^ We enter the scope as the parent of another class

-- | The type of the container together with its tags.
data CurContainer = CurContainer
  { _cctype :: !CurContainerDesc
  , _cctags :: !(HashSet Text)
  } deriving (Eq)

data ScopeInformation = ScopeInformation
  { _scopeVariables   :: !(Container (Pair (Pair PValue PPosition) CurContainerDesc))
  , _scopeResDefaults :: !(Container ResDefaults)
  , _scopeExtraTags   :: !(HashSet Text)
  , _scopeContainer   :: !CurContainer
  , _scopeOverrides   :: !(HashMap RIdentifier ResRefOverride)
  , _scopeParent      :: !(S.Maybe Text)
  }

data InterpreterState = InterpreterState
  { _scopes             :: !(Container ScopeInformation)
  , _loadedClasses      :: !(Container (Pair ClassIncludeType PPosition))
  , _definedResources   :: !(HM.HashMap RIdentifier Resource)
  , _curScope           :: ![CurContainerDesc]
  , _curPos             :: !PPosition
  , _nestedDeclarations :: !(HashMap (TopLevelType, Text) Statement)
  , _extraRelations     :: ![LinkInformation]
  , _resModifiers       :: ![ResourceModifier]
  }

data IoMethods m = IoMethods
  { _ioGetCurrentCallStack :: m [String]
  , _ioReadFile            :: [Text] -> m (Either String Text)
  , _ioTraceEvent          :: String -> m ()
  }

-- | All available queries including the global and module layer
-- The environment layer is not implemented.
--
-- The datatype belongs to the "Puppet.Interpreter" module because it serves to implement how Hiera is used within Puppet.
data HieraQueryLayers m = HieraQueryLayers
  { _globalLayer :: HieraQueryFunc m
  , _environmentLayer :: HieraQueryFunc m
  , _moduleLayer :: Container (HieraQueryFunc m)
  }

-- | Whether the template source is specified 'inline' or loaded from a file.
data  TemplateSource= Inline Text | Filename FilePath

data InterpreterReader m = InterpreterReader
  { _readerNativeTypes     :: !(Container NativeTypeMethods)
  , _readerGetStatement    :: TopLevelType -> Text -> m (S.Either PrettyError Statement) -- ^ Access to parsed statements
  , _readerGetTemplate     :: TemplateSource -> InterpreterState -> InterpreterReader m -> m (S.Either PrettyError Text)
  , _readerPdbApi          :: PuppetDBAPI m
  , _readerExternalFunc    :: Container ([PValue] -> InterpreterMonad PValue) -- ^ External func such as stdlib or puppetlabs
  , _readerNodename        :: Text
  , _readerHieraQuery      :: HieraQueryLayers m
  , _readerIoMethods       :: IoMethods m
  , _readerIgnoredModules  :: HashSet Text
  , _readerExternalModules :: HashSet Text
  , _readerIsStrict        :: Bool
  , _readerPuppetPaths     :: PuppetDirPaths
  , _readerRebaseFile      :: Maybe FilePath
  , _readerFacts           :: Container PValue -- ^ Access to the list of facts that were given to the 'Preferences' module
  }

data InterpreterInstr a where
  -- Utility for using what's in 'InterpreterReader'
  GetNativeTypes        :: InterpreterInstr (Container NativeTypeMethods)
  GetStatement          :: TopLevelType -> Text -> InterpreterInstr Statement
  ComputeTemplate       :: TemplateSource-> InterpreterState -> InterpreterInstr Text
  ExternalFunction      :: Text -> [PValue] -> InterpreterInstr PValue
  Facts                 :: InterpreterInstr (Container PValue)
  GetNodeName           :: InterpreterInstr Text
  HieraQuery            :: Container PValue -> Text -> HieraQueryType -> InterpreterInstr (Maybe PValue)
  GetCurrentCallStack   :: InterpreterInstr [String]
  IsIgnoredModule       :: Text -> InterpreterInstr Bool
  IsExternalModule      :: Text -> InterpreterInstr Bool
  IsStrict              :: InterpreterInstr Bool
  PuppetPaths           :: InterpreterInstr PuppetDirPaths
  RebaseFile            :: InterpreterInstr (Maybe FilePath)
  -- error
  ErrorThrow            :: PrettyError -> InterpreterInstr a
  ErrorCatch            :: InterpreterMonad a -> (PrettyError -> InterpreterMonad a) -> InterpreterInstr a
  -- writer
  WriterTell            :: InterpreterWriter -> InterpreterInstr ()
  WriterPass            :: InterpreterMonad (a, InterpreterWriter -> InterpreterWriter) -> InterpreterInstr a
  WriterListen          :: InterpreterMonad a -> InterpreterInstr (a, InterpreterWriter)
  -- puppetdb wrappers  , see 'PuppetDBAPI' for details
  PDBInformation        :: InterpreterInstr Doc
  PDBReplaceCatalog     :: WireCatalog -> InterpreterInstr ()
  PDBReplaceFacts       :: [(NodeName, Facts)] -> InterpreterInstr ()
  PDBDeactivateNode     :: NodeName -> InterpreterInstr ()
  PDBGetFacts           :: Query FactField -> InterpreterInstr [FactInfo]
  PDBGetResources       :: Query ResourceField -> InterpreterInstr [Resource]
  PDBGetNodes           :: Query NodeField -> InterpreterInstr [NodeInfo]
  PDBCommitDB           :: InterpreterInstr ()
  PDBGetResourcesOfNode :: NodeName -> Query ResourceField -> InterpreterInstr [Resource]
  -- Reading the first file that can be read in a list
  ReadFile              :: [Text] -> InterpreterInstr Text
  -- Tracing events
  TraceEvent            :: String -> InterpreterInstr ()

-- | The main monad
type InterpreterMonad = ProgramT InterpreterInstr (State InterpreterState)

instance Fail.MonadFail InterpreterMonad where
    fail = throwError . PrettyError . ppstring

instance MonadError PrettyError InterpreterMonad where
  throwError = singleton . ErrorThrow
  catchError a c = singleton (ErrorCatch a c)

-- | Log
type InterpreterWriter = [Pair Log.Priority Doc]
instance MonadWriter InterpreterWriter InterpreterMonad where
  tell = singleton . WriterTell
  pass = singleton . WriterPass
  listen = singleton . WriterListen

data ResourceModifier = ResourceModifier
  { _rmResType      :: !Text
  , _rmModifierType :: !ModifierType
  , _rmType         :: !ResourceCollectorType
  , _rmSearch       :: !RSearchExpression
  , _rmMutation     :: !(Resource -> InterpreterMonad Resource)
  , _rmDeclaration  :: !PPosition
  }

instance Show ResourceModifier where
  show (ResourceModifier rt mt ct se _ p) = List.unwords ["ResourceModifier", show rt, show mt, show ct, "(" ++ show se ++ ")", "???", show p]

data ModifierType
  = ModifierCollector -- ^ For collectors, optional resources
  | ModifierMustMatch -- ^ For stuff like realize
  deriving (Show, Eq)

data OverrideType
  = CantOverride -- ^ Overriding forbidden, will throw an error
  | Replace -- ^ Can silently replace
  | CantReplace -- ^ Silently ignore errors
  | AppendAttribute -- ^ Can append values
  deriving (Show, Eq)

data ResourceCollectorType
  = RealizeVirtual
  | RealizeCollected
  | DontRealize
  deriving (Show, Eq)

makeLenses ''ResDefaults
makeLenses ''HieraQueryLayers
makeLenses ''ResourceModifier
makeLenses ''InterpreterReader
makeLenses ''IoMethods
makeLenses ''CurContainer
makeLenses ''ScopeInformation
makeLenses ''InterpreterState


class Monad m => MonadThrowPos m where
  throwPosError :: Doc -> m a

-- Useful for mocking for instance in a REPL
instance MonadThrowPos (Either Doc) where
  throwPosError = Left

class MonadStack m where
  getCurrentCallStack :: m [String]

instance MonadStack InterpreterMonad where
  getCurrentCallStack = singleton GetCurrentCallStack

instance MonadThrowPos InterpreterMonad where
  throwPosError s = do
    p <- use (curPos . _1)
    stack <- getCurrentCallStack
    let dstack = if null stack
                   then line
                   else mempty </> ppstring (GHC.Stack.renderStack stack)
    throwError (PrettyError (s <+> "at" <+> showPos p <> dstack))