{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module: BDCS.Export.Utils
-- Copyright: (c) 2017 Red Hat, Inc.
-- License: LGPL
--
-- Maintainer: https://github.com/weldr
-- Stability: alpha
-- Portability: portable
--
-- Miscellaneous utilities useful in exporting objects.

module BDCS.Export.Utils(runHacks,
                         runTmpfiles)
 where

import           Control.Conditional(ifM, whenM)
import           Control.Exception(tryJust)
import qualified Control.Exception.Lifted as CEL
import           Control.Monad(guard)
import           Control.Monad.Except(MonadError, throwError)
import           Control.Monad.IO.Class(liftIO)
import           Control.Monad.Logger(MonadLoggerIO, logDebugN)
import           Control.Monad.Trans.Control(MonadBaseControl)
import           Data.List(intercalate)
import           Data.List.Split(splitOn)
import           System.Directory(createDirectoryIfMissing, doesFileExist, listDirectory, removePathForcibly, renameFile)
import           System.FilePath((</>))
import           System.IO.Error(isDoesNotExistError)

import BDCS.Export.TmpFiles(setupFilesystem)
import BDCS.Utils.Process(callProcessLogged)

import Paths_bdcs(getDataFileName)

-- | Run filesystem hacks needed to make a directory tree bootable.  Any exporter that produces a
-- finished image should call this function.  Otherwise, it is not generally useful and should be
-- avoided.  The exact hacks required is likely to change over time.
--
-- Any exceptions from 'Control.Exception' will be convered into 'Control.Monad.Except' style
-- exceptions, which can then be handled via catchError from that module.
runHacks :: (MonadBaseControl IO m, MonadError String m, MonadLoggerIO m) => FilePath -> m ()
runHacks exportPath = runHacks' exportPath `CEL.catch` \e -> throwError $ show (e :: CEL.IOException)

-- This is a helper for runHacks that does all the hard work.  It exists separately so the main
-- function can handle any exceptions without making a mess of the code.
runHacks' :: (MonadBaseControl IO m, MonadLoggerIO m) => FilePath -> m ()
runHacks' exportPath = do
    -- set a root password
    -- pre-crypted from "redhat"
    logDebugN "Setting root password"
    liftIO $ do
        shadowRecs <- map (splitOn ":") <$> lines <$> ifM (doesFileExist (exportPath </> "etc" </> "shadow"))
                                                          (readFile (exportPath </> "etc" </> "shadow"))
                                                          (return "")
        let newRecs = map (\rec -> case rec of
                                       "root":_:rest -> ["root", "$6$3VLMX3dyCGRa.JX3$RpveyimtrKjqcbZNTanUkjauuTRwqAVzRK8GZFkEinbjzklo7Yj9Z6FqXNlyajpgCdsLf4FEQQKH6tTza35xs/"] ++ rest
                                       _             -> rec)
                          shadowRecs
        writeFile (exportPath </> "etc" </> "shadow.new") (unlines $ map (intercalate ":") newRecs)
        renameFile (exportPath </> "etc" </> "shadow.new") (exportPath </> "etc" </> "shadow")

    -- create an empty machine-id
    logDebugN "Creating empty /etc/machine-id"
    liftIO $ writeFile (exportPath </> "etc" </> "machine-id") ""

    -- Install a sysusers.d config file, and run systemd-sysusers to implement it
    let sysusersDir = exportPath </> "usr" </> "lib" </> "sysusers.d"
    liftIO $ do
        createDirectoryIfMissing True sysusersDir
        getDataFileName "data/sysusers-default.conf" >>= readFile >>= writeFile (sysusersDir </> "weldr.conf")

    callProcessLogged "systemd-sysusers" ["--root", exportPath]

    -- Run depmod on any kernel modules that might be present
    let modDir = exportPath </> "usr" </> "lib" </> "modules"
    modVers <- liftIO $ tryJust (guard . isDoesNotExistError) (listDirectory modDir)
    mapM_ (\ver -> callProcessLogged "depmod" ["-b", exportPath, "-a", ver]) $ either (const []) id modVers

    -- Create a fstab stub
    logDebugN "Creating stub /etc/fstab"
    liftIO $ writeFile (exportPath </> "etc" </> "fstab") "LABEL=composer / ext2 defaults 0 0"

    -- Clean up /run
    -- Some packages create directories in /var/run, which a symlink to /run, which is a tmpfs.
    logDebugN "Removing directories in /run"
    liftIO $ (map ((exportPath </> "run") </>) <$> listDirectory (exportPath </> "run")) >>= mapM_ removePathForcibly

    -- EXTRA HACKY: turn off mod_ssl
    let sslConf = exportPath </> "etc" </> "httpd" </> "conf.d" </> "ssl.conf"
    whenM (liftIO $ doesFileExist sslConf) $ do
        logDebugN "Disabling mod_ssl"
        liftIO $ renameFile sslConf (sslConf ++ ".off")

-- | Run tmpfiles.d snippet on the new directory.  Most exporters should call this function.  Otherwise,
-- it is not generally useful and should be avoided.
--
-- Any exceptions from 'Control.Exception' will be convered into 'Control.Monad.Except' style
-- exceptions, which can then be handled via catchError from that module.
runTmpfiles :: (MonadBaseControl IO m, MonadError String m, MonadLoggerIO m) => FilePath -> m ()
runTmpfiles exportPath = runTmpfiles' exportPath `CEL.catch` \e -> throwError $ show (e :: CEL.IOException)

-- This is a helper for runTmpfiles that does all the hard work.  It exists separately so the main
-- function can handle any exceptions without making a mess of the code.
runTmpfiles' :: MonadLoggerIO m => FilePath -> m ()
runTmpfiles' exportPath =  do
    configPath <- liftIO $ getDataFileName "data/tmpfiles-default.conf"
    setupFilesystem exportPath configPath