{-# LANGUAGE GADTs         #-}
{-# LANGUAGE TupleSections #-}
module Puppet.Runner.Daemon (
    Daemon(..)
  , initDaemon
) where

import           XPrelude

import qualified Data.Either.Strict          as S
import           Data.FileCache              as FileCache
import qualified Data.HashMap.Strict         as HM
import qualified Data.Text                   as Text
import           Debug.Trace                 (traceEventIO)
import           Foreign.Ruby.Safe
import qualified System.Directory            as Directory
import qualified System.Log.Formatter        as Log (simpleLogFormatter)
import qualified System.Log.Handler          as Log (setFormatter)
import qualified System.Log.Handler.Simple   as Log (streamHandler)
import qualified System.Log.Logger           as Log

import           Facter
import           Hiera.Server
import           Puppet.Runner.Daemon.FileParser
import           Puppet.Runner.Daemon.OptionalTests
import           Puppet.Runner.Erb
import           Puppet.Interpreter
import           Puppet.Parser
import           Puppet.Runner.Preferences
import           Puppet.Runner.Stats


{-| API for the Daemon.
The main method is `getCatalog`: given a node and a list of facts, it returns the result of the compilation.
This will be either an error, or a tuple containing:

- all the resources in this catalog
- the dependency map
- the exported resources
- a list of known resources, that might not be up to date, but are here for code coverage tests.

Notes :

* It might be buggy when top level statements that are not class\/define\/nodes are altered.
-}
data Daemon = Daemon
  { getCatalog :: NodeName -> Facts -> IO (Either PrettyError (FinalCatalog, EdgeMap, FinalCatalog, [Resource]))
  , parserStats :: MStats
  , catalogStats :: MStats
  , templateStats :: MStats
  }

{-| Entry point to get a Daemon.
It will initialize the parsing and interpretation infrastructure from the 'Preferences'.

Cache the AST of every .pp file. It could use a bit of memory. As a comparison, it
fits in 60 MB with the author's manifests, but really breathes when given 300 MB
of heap space. In this configuration, even if it spawns a ruby process for every
template evaluation, it is way faster than the puppet stack.

It can optionally talk with PuppetDB, by setting an URL via the 'prefPDB'.
The recommended way to set it to http://localhost:8080 and set a SSH tunnel :

> ssh -L 8080:localhost:8080 puppet.host
-}
initDaemon :: Preferences IO
           -> IO Daemon
initDaemon pref = do
  setupLogger (pref ^. prefLogLevel)
  logDebug "Initialize daemon"
  traceEventIO "initDaemon"
  hquery      <- hieraQuery pref
  fcache      <- newFileCache
  intr        <- startRubyInterpreter
  templStats  <- newStats
  getTemplate <- initTemplateDaemon intr pref templStats
  catStats    <- newStats
  parseStats  <- newStats
  pure (Daemon
         (getCatalog' pref (parseFunc (pref ^. prefPuppetPaths) fcache parseStats) getTemplate catStats hquery)
         parseStats
         catStats
         templStats
       )

getCatalog' :: Preferences IO
         -> ( TopLevelType -> Text -> IO (S.Either PrettyError Statement) )
         -> (TemplateSource -> InterpreterState -> InterpreterReader IO -> IO (S.Either PrettyError Text))
         -> MStats
         -> HieraQueryLayers IO
         -> NodeName
         -> Facts
         -> IO (Either PrettyError (FinalCatalog, EdgeMap, FinalCatalog, [Resource]))
getCatalog' pref parsingfunc getTemplate stats hquery node facts = do
  logDebug ("Received query for node " <> node)
  traceEventIO ("START getCatalog' " <> Text.unpack node)
  let catalogComputation = interpretCatalog (InterpreterReader
                                                (pref ^. prefNatTypes)
                                                parsingfunc
                                                getTemplate
                                                (pref ^. prefPDB)
                                                (pref ^. prefExtFuncs)
                                                node
                                                hquery
                                                defaultImpureMethods
                                                (pref ^. prefIgnoredmodules)
                                                (pref ^. prefExternalmodules)
                                                (pref ^. prefStrictness == Strict)
                                                (pref ^. prefPuppetPaths)
                                                (pref ^. prefRebaseFile)
                                                ((pref ^. prefFactsOverride) `HM.union` (pref ^. prefFactsDefault))
                                            )
                                            node
                                            facts
                                            (pref ^. prefPuppetSettings)
  (stmts :!: warnings) <- measure stats node catalogComputation
  mapM_ (\(p :!: m) -> Log.logM loggerName p (displayS (renderCompact (ppline node <> ":" <+> m)) "")) warnings
  traceEventIO ("STOP getCatalog' " <> toS node)
  case stmts of
    Left _ -> pure stmts -- no catalog so we can't do the extra tests
    Right r@(c,_,_,_) -> do
      if pref ^. prefExtraTests
        then second (const r) <$> (testCatalog pref c)
        else pure stmts

-- Build the 'HieraQueryLayers' needed by the interpreter to lookup hiera values.
hieraQuery :: Preferences IO -> IO (HieraQueryLayers IO)
hieraQuery pref = do
  global_api <- case pref ^. prefHieraPath of
    Just p  -> startHiera "global" p
    Nothing -> pure dummyHiera
  env_api <- getEnvApi
  mod_api <- getModApis
  pure (HieraQueryLayers global_api env_api mod_api)
  where
    getEnvApi :: IO (HieraQueryFunc IO)
    getEnvApi = do
      let f =  (pref ^. prefPuppetPaths . baseDir) <> "/hiera.yaml"
      found <- Directory.doesFileExist f
      if found then startHiera "environment" f else pure dummyHiera
    getModApis :: IO (Container (HieraQueryFunc IO))
    getModApis = do
      let ignored_modules = pref^.prefIgnoredmodules
          modpath = pref^.prefPuppetPaths.modulesPath
      dirs <- Directory.listDirectory modpath
      (HM.fromList . catMaybes) <$>
        for dirs (\dir -> runMaybeT $ do
          let modname = toS dir
              fp = modpath <> "/" <> dir <> "/hiera.yaml"
          guard (modname `notElem` ignored_modules)
          guard =<< liftIO (Directory.doesFileExist fp)
          liftIO $ (modname, ) <$> startHiera "module" fp)


defaultImpureMethods :: MonadIO m => IoMethods m
defaultImpureMethods =
  IoMethods (liftIO currentCallStack) (liftIO . file) (liftIO . traceEventIO)
  where
    file [] = return $ Left ""
    file (x:xs) = (Right <$> readFile (Text.unpack x)) `catch` (\SomeException {} -> file xs)

setupLogger :: Log.Priority -> IO ()
setupLogger p = do
  Log.updateGlobalLogger loggerName (Log.setLevel p)
  hs <- consoleLogHandler
  Log.updateGlobalLogger Log.rootLoggerName $ Log.setHandlers [hs]
  where
    consoleLogHandler = Log.setFormatter
                       <$> Log.streamHandler stdout Log.DEBUG
                       <*> pure (Log.simpleLogFormatter "$prio: $msg")