{-# LANGUAGE GADTs             #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE RecordWildCards   #-}
{-# LANGUAGE StrictData        #-}
module Main where

import           XPrelude               hiding (option)

import qualified Data.Aeson             as Aeson
import qualified Data.HashMap.Strict    as HM
import qualified Data.Text              as Text
import qualified Data.Vector            as Vector
import qualified Data.Version           as Meta
import qualified Network.HTTP.Client    as Http
import           Options.Applicative    as O
import qualified Paths_language_puppet  as Meta

import           Facter
import           Puppet.Interpreter
import           PuppetDB


data Options = Options
  { _pdbloc     :: Maybe FilePath
  , _pdbtype    :: PDBType
  , _pdbcmd     :: Maybe Command
  , _pdbversion :: Bool
  }

data Command
  = DumpFacts
  | DumpFact Text
  | DumpNodes
  | EditFact Text Text
  | DeactivateNode Text
  | DumpResources Text
  | CreateTestDB FilePath
  | AddFacts Text

options :: Parser Options
options = Options
  <$> optional (strOption
      (  long "location"
      <> short 'l'
      <> metavar "FILE|URL"
      <> help "Location of the PuppetDB, a file for type 'test' or an URL for type 'remote'"))
  <*> option auto
      (  long "pdbtype"
      <> short 't'
      <> value PDBTest
      <> help "PuppetDB types : test, remote, dummy")
  <*> optional cmd
  <*> switch
      (  long "version"
      <> help "Output version information and exit")
  where
      cmd = subparser (  command "resources"   (info resourcesparser (progDesc "Output resources for one node" <> failureCode 5))
                      <> command "facts"  (info factparser (progDesc "Output facts for one node" <> failureCode 12 ))
                      <> command "nodes"     (info (pure DumpNodes)(progDesc "Output all nodes" <> failureCode 8))
                      <> command "dumpfacts" (info (pure DumpFacts)(progDesc "Dump all facts, store in /tmp/allfacts.yaml" <> failureCode 4))
                      <> command "snapshot"  (info createtestdb    (progDesc "Create a test DB from the current DB" <> failureCode 10))
                      <> command "delnode"   (info delnodeparser   (progDesc "Deactivate node" <> failureCode 6))
                      <> command "editfact"  (info factedit (progDesc "Edit a fact corresponding to a node" <> failureCode 7 ))
                      <> command "addfacts"  (info addfacts        (progDesc "Adds facts to the test DB for the given node name, if they are not already defined" <> failureCode 11))
                      )

factedit :: Parser Command
factedit = EditFact <$> O.argument auto mempty <*> O.argument auto mempty

factparser :: Parser Command
factparser = DumpFact <$> fmap Text.pack (O.strArgument (metavar "NODE"))

resourcesparser :: Parser Command
resourcesparser = DumpResources <$> fmap Text.pack (O.strArgument (metavar "NODE"))

delnodeparser :: Parser Command
delnodeparser = DeactivateNode <$> O.argument auto mempty

createtestdb :: Parser Command
createtestdb = CreateTestDB <$> O.argument str (metavar "FILE")

addfacts :: Parser Command
addfacts = AddFacts <$> O.argument auto mempty


display :: (Show r, Aeson.ToJSON a) => Text -> Either r a -> IO ()
display s (Left rr) = panic (s <> " " <> show rr)
display _ (Right a) = putLByteString (Aeson.encode a)

runCheck :: Doc -> ExceptT PrettyError IO a -> IO a
runCheck s = runExceptT >=> unwrapError s

showHelpText :: ParserPrefs -> ParserInfo a -> IO ()
showHelpText pprefs pinfo = handleParseResult . Failure $
  parserFailure pprefs pinfo ShowHelpText mempty

run :: Options -> IO ()
run Options {_pdbversion = False, _pdbcmd = Nothing} =
  putText "Please provide one of the available command (see --help for more information) " *> exitFailure
run Options {_pdbversion = True, ..} = putStrLn ("language-puppet " ++ Meta.showVersion Meta.version)

run Options{_pdbcmd = Just pdbcmd, ..} = do
  mgr <- Http.newManager Http.defaultManagerSettings
  epdbapi <- case (_pdbloc, _pdbtype) of
    (Just url, PDBRemote) -> pdbConnect mgr url
    (Just l, PDBTest)   -> loadTestDB l
    (_, x)              -> getDefaultDB x
  pdbapi <- case epdbapi of
    Left r  -> panic (show r)
    Right x -> return x
  case pdbcmd of
      DumpFacts -> if _pdbtype == PDBDummy
                     then puppetDBFacts "dummy"  pdbapi >>= mapM_ print . HM.toList
                     else do
                       allfacts <- runCheck "get facts" (getPDBFacts pdbapi QEmpty)
                       tmpdb <- loadTestDB "/tmp/allfacts.yaml" >>= unwrapError "load test db"
                       let groupfacts = foldl' groupfact HM.empty allfacts
                           groupfact curmap (FactInfo ndname fctname fctval) =
                               curmap & at ndname . non HM.empty %~ (at fctname ?~ fctval)
                       runCheck "replace facts in dummy db" (replaceFacts tmpdb (HM.toList groupfacts))
                       runCheck "commit db" (commitDB tmpdb)
      DumpFact n -> runExceptT (getPDBFacts pdbapi (QEqual FCertname n ) ) >>= display "dump fact"
      DumpNodes -> runExceptT (getNodes pdbapi QEmpty) >>= display "dump nodes"
      DumpResources n -> runExceptT (getResourcesOfNode pdbapi n QEmpty) >>= display "get resources"
      AddFacts n -> do
        unless (_pdbtype == PDBTest) (panic "This option only works with the test puppetdb")
        fcts <- puppetDBFacts n pdbapi
        runCheck "replace facts" (replaceFacts pdbapi [(n, fcts)])
        runCheck "commit db" (commitDB pdbapi)
      CreateTestDB destfile -> do
        ndb <- loadTestDB destfile >>= unwrapError "puppetdb load"
        allnodes <- runCheck "get nodes" (getNodes pdbapi QEmpty)
        allfacts <- runCheck "get facts" (getPDBFacts pdbapi QEmpty)
        let factsGrouped = HM.toList $ HM.fromListWith (<>) $ map (\x -> (x ^. factInfoNodename, HM.singleton (x ^. factInfoName) (x ^. factInfoVal))) allfacts
        runCheck "replace facts" (replaceFacts ndb factsGrouped)
        forM_ allnodes $ \pnodename -> do
            let ndename = pnodename ^. nodeInfoName
            res <- runCheck ("get resources for " <> ppline ndename) (getResourcesOfNode pdbapi ndename QEmpty)
            let wirecatalog = WireCatalog ndename "version" Vector.empty (Vector.fromList res) ndename
            runCheck "replace catalog" (replaceCatalog ndb wirecatalog)
        runCheck "commit db" (commitDB ndb)
      _ -> panic "Not yet implemented"

main :: IO ()
main =
  execParser opts >>= run
  where
    opts :: ParserInfo Options
    opts = info (helper <*> options)
            (fullDesc
             <> progDesc "A program to work with PuppetDB implementations"
             <> header "pdbQuery - work with PuppetDB implementations"
             <> failureCode 3)
