-- | Maintainer: 2016 Evan Cofsky <evan@theunixman.com>
--
-- FreeBSD Poudriere properties

{-# Language GeneralizedNewtypeDeriving, DeriveDataTypeable #-}

module Propellor.Property.FreeBSD.Poudriere where

import Propellor.Base
import Propellor.Types.Info
import qualified Propellor.Property.FreeBSD.Pkg as Pkg
import qualified Propellor.Property.ZFS as ZFS
import qualified Propellor.Property.File as File

import Data.List
import qualified Data.Semigroup as Sem

poudriereConfigPath :: FilePath
poudriereConfigPath = "/usr/local/etc/poudriere.conf"

newtype PoudriereConfigured = PoudriereConfigured String
        deriving (Typeable, Sem.Semigroup, Monoid, Show)

instance IsInfo PoudriereConfigured where
        propagateInfo _ = PropagateInfo False

poudriereConfigured :: PoudriereConfigured -> Bool
poudriereConfigured (PoudriereConfigured _) = True

setConfigured :: Property (HasInfo + FreeBSD)
setConfigured = tightenTargets $
        pureInfoProperty "Poudriere Configured" (PoudriereConfigured "")

poudriere :: Poudriere -> Property (HasInfo + FreeBSD)
poudriere conf@(Poudriere _ _ _ _ _ _ zfs) = prop
        `requires` Pkg.installed "poudriere"
        `before` setConfigured
  where
        confProp :: Property FreeBSD
        confProp = tightenTargets $
                File.containsLines poudriereConfigPath (toLines conf)
        setZfs (PoudriereZFS z p) = ZFS.zfsSetProperties z p `describe` "Configuring Poudriere with ZFS"
        prop :: Property FreeBSD
        prop
                | isJust zfs = ((setZfs $ fromJust zfs) `before` confProp)
                | otherwise = confProp `describe` "Configuring Poudriere without ZFS"

poudriereCommand :: String -> [String] -> (String, [String])
poudriereCommand cmd args = ("poudriere", cmd:args)

runPoudriere :: String -> [String] -> IO [String]
runPoudriere cmd args =
        let
                (p, a) = poudriereCommand cmd args
        in
                lines <$> readProcess p a

listJails :: IO [String]
listJails = mapMaybe (headMaybe . take 1 . words)
        <$> runPoudriere "jail" ["-l", "-q"]

jailExists :: Jail -> IO Bool
jailExists (Jail name _ _) = isInfixOf [name] <$> listJails

jail :: Jail -> Property FreeBSD
jail j@(Jail name version arch) = tightenTargets $
        let
                chk = do
                        c <- poudriereConfigured <$> askInfo
                        nx <- liftIO $ not <$> jailExists j
                        return $ c && nx

                (cmd, args) = poudriereCommand "jail"  ["-c", "-j", name, "-a", val arch, "-v", val version]
                createJail = cmdProperty cmd args
        in
                check chk createJail
                        `describe` unwords ["Create poudriere jail", name]

data JailInfo = JailInfo String

data Poudriere = Poudriere
        { _resolvConf :: String
        , _freebsdHost :: String
        , _baseFs :: String
        , _usePortLint :: Bool
        , _distFilesCache :: FilePath
        , _svnHost :: String
        , _zfs :: Maybe PoudriereZFS
        }

defaultConfig :: Poudriere
defaultConfig = Poudriere
        "/etc/resolv.conf"
        "ftp://ftp5.us.FreeBSD.org"
        "/usr/local/poudriere"
        True
        "/usr/ports/distfiles"
        "svn.freebsd.org"
        Nothing

data PoudriereZFS = PoudriereZFS ZFS.ZFS ZFS.ZFSProperties

data Jail = Jail String FBSDVersion PoudriereArch

data PoudriereArch = I386 | AMD64 deriving (Eq)

instance ConfigurableValue PoudriereArch where
        val I386 = "i386"
        val AMD64 = "amd64"

fromArchitecture :: Architecture -> PoudriereArch
fromArchitecture X86_64 = AMD64
fromArchitecture X86_32 = I386
fromArchitecture _ = error "Not a valid Poudriere architecture."

yesNoProp :: Bool -> String
yesNoProp b = if b then "yes" else "no"

instance ToShellConfigLines Poudriere where
        toAssoc c = map (\(k, f) -> (k, f c))
                [ ("RESOLV_CONF", _resolvConf)
                , ("FREEBSD_HOST", _freebsdHost)
                , ("BASEFS", _baseFs)
                , ("USE_PORTLINT", yesNoProp . _usePortLint)
                , ("DISTFILES_CACHE", _distFilesCache)
                , ("SVN_HOST", _svnHost)
                ] ++ maybe [ ("NO_ZFS", "yes") ] toAssoc (_zfs c)

instance ToShellConfigLines PoudriereZFS where
        toAssoc (PoudriereZFS (ZFS.ZFS (ZFS.ZPool pool) dataset) _) =
                [ ("NO_ZFS", "no")
                , ("ZPOOL", pool)
                , ("ZROOTFS", val dataset)
                ]

type ConfigLine = String
type ConfigFile = [ConfigLine]

class ToShellConfigLines a where
        toAssoc :: a -> [(String, String)]

        toLines :: a -> [ConfigLine]
        toLines c = map (\(k, v) -> intercalate "=" [k, v]) (toAssoc c)

confFile :: FilePath
confFile = "/usr/local/etc/poudriere.conf"