{-# LANGUAGE PatternSignatures #-}
module HSH.Helpers.UnixUsers where

-- I don't know how portable this is.
-- It works for me, on ubuntu Hardy Heron
import HSH
import System.Directory (doesDirectoryExist)
import Text.Regex.PCRE

import qualified Data.String.Utils as SU
import Text.StringTemplate.Helpers

type SysUser = String
type SysGroup = String
type SysShell = String


data UseraddCmd = UseraddCmd {
  useraddUsername :: SysUser
  , useraddGrp :: Maybe SysGroup
  , useraddShell :: Maybe SysShell
  , useraddHomedir :: Maybe FilePath
  -- whether to create the user if there's already a homedir in their name,
  -- perhaps left over from another person with the same username
  --  who has been deleted but whose homedir remains.
  , useraddAllowExistingHomedir :: Bool 
  , useraddExe :: Maybe FilePath
  }

-- does nothing if the user already exists
useraddIfMissing uaCmd = do
  sysU <- isSystemUser (useraddUsername uaCmd)
  if not sysU
    then useradd uaCmd
    else return ()

groupaddIfMissing g = do
  sysG <- isSystemGroup g
  if not sysG
    then groupadd g
    else return ()

-- wrapper over groupadd
groupadd g = runIO $ "groupadd " ++ g
groupadd' :: Maybe FilePath -> SysGroup -> IO ()
groupadd' mbExe g = 
  let gaCmd = maybe "groupadd" id mbExe
  in runIO $ render1 [("gaCmd",gaCmd),("g",g)] "$gaCmd$ $g$"

-- a wrapper over the useradd command 
useradd uaCmd = do
  let uname = useraddUsername uaCmd 
      grp = maybe "" ("-g " ++ ) (useraddGrp uaCmd)
      shell = maybe "" ("--shell " ++) (useraddShell uaCmd)
      hdir = maybe "" ("--home-dir " ++) (useraddHomedir uaCmd) 
      useraddexe = maybe "useradd" id (useraddExe uaCmd)
  homedirExists <- doesDirectoryExist hdir
  if (not . null $ hdir) && (not . useraddAllowExistingHomedir $ uaCmd) && homedirExists
    then fail $ "createUnixUser failed, homedir exists: " ++ hdir
    else runIO $ {- traceIt $ -} render1 [ ("useraddexe",useraddexe) 
                                           , ("uname",uname) 
                                           , ("grp",grp) 
                                           , ("shell",shell) 
                                           , ("hdir",hdir) ]  
                                 "$useraddexe$ $uname$ $grp$ $shell$ $hdir$"

-- similar formats, so we can share most of the logic
isSystemUser = isSystemUG' "/etc/passwd"
isSystemGroup = isSystemUG' "/etc/group"
isSystemUG' f u = return . elem u . map parseug . lines
                   =<< readFile f
  -- basically just get the first column. regex gets everything up to the first :.
  where parseug ug = let ((_,match,_,_) :: (String,String,String,[String])) =  ug =~ "^[^:]*" 
           in match

sysGroupMember u g = do
  a <- getSysGroupsAll u
  return $ elem g a

getSysGroupPrimary u = runSL $ render1 [("u",u)] "id -ng $u$"
getSysGroupsSecondary u = do
  p <- getSysGroupPrimary u
  all <- getSysGroupsAll u
  return $ filter (p/=) all
getSysGroupsAll u = return . words =<< (runSL $ render1 [("u",u)] "id -nG $u$")



-- addUserToGroup fails if user/group doesn't exist
-- if user is already in group, there's no error, which is the same behavior of the unix command.

addUserToGroup = addUserToGroup' (Just "usermod")
addUserToGroup' :: (Maybe FilePath) -> SysUser -> SysGroup -> IO ()
addUserToGroup' mbUsermodCmd u g 
  | null . SU.strip $ u = fail "addUserToGroup: blank user"
  | null . SU.strip $ g = fail "addUserToGroup: blank group" 
  | otherwise =
      let usermodCmd = maybe "usermod" id mbUsermodCmd
      in runIO $ render1 [("u",u),("g",g)]
                                      "usermod -G $g$ -a $u$"

rmUserFromSecondaryGroup :: SysUser -> SysGroup -> IO ()
rmUserFromSecondaryGroup u g = do
  sd <- getSysGroupsAll u
  -- users are joined by a comma, no intervening whitespace (man usermod)
  let sdMod = SU.join "," $ filter (g/=) sd
  runIO $ render1 [("u",u),("sdMod",sdMod)] "usermod -G $sdMod$ $u$"



data ChownOptions = ChownOptions { 
  chownUser :: Maybe String
  , chownGrp :: Maybe String
  , chownR :: Bool -- recurse into subdirectories
  -- chownCreateUserIfMissing -- might be useful
  }

chown = chown' "chown"
chown' chownexe chownOptions path = do
  let uname = maybe "" id (chownUser chownOptions)
      grp = maybe "" id (chownGrp chownOptions)
      getOwner u@(_:_) g@(_:_) = Right $ u++":"++g 
      getOwner u@(_:_) [] = Right u
      getOwner [] g@(_:_) = Right $ ":"++g
      getOwner [] [] = Left $ "chown error, no user and no group"
      eOwner = getOwner uname grp
      rFlag = if (chownR chownOptions) then "-R" else ""
  case eOwner of
    Left msg -> fail msg
    Right owner -> runIO $ {- traceIt $ -} render1 [ ("chownexe",chownexe) 
                                           , ("owner",owner) 
                                           , ("rFlag",rFlag)
                                           , ("path",path) ]  
                                 "$chownexe$ $owner$ $rFlag$ $path$"


-- chown u g d = runIO $ render1 [("u",u),("g",g),("d",d)] "chown $u$:$g$ $d$"
-- chownR u g d = runIO $ render1 [("u",u),("g",g),("d",d)] "chown -R $u$:$g$ $d$"