{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, TemplateHaskell, NamedFieldPuns #-}
module Distribution.Server.Users.Users (
    -- * Users type
    Users,

    -- * Construction
    emptyUsers,
    addUserEnabled,
    addUserDisabled,
    addUser,
    insertUserAccount,

    -- * Modification
    deleteUser,
    setUserEnabledStatus,
    setUserAuth,
    setUserName,

    -- * Lookup
    lookupUserId,
    lookupUserName,

    -- ** Lookup utils
    userIdToName,

    -- * Enumeration
    enumerateAllUsers,
    enumerateActiveUsers,

    -- * Error codes
    ErrUserNameClash(..),
    ErrUserIdClash(..),
    ErrNoSuchUserId(..),
    ErrDeletedUser(..),
  ) where

import Distribution.Server.Users.Types

import Distribution.Server.Framework.Instances ()
import Distribution.Server.Framework.MemSize

import Control.Monad (guard)
import Data.Maybe (fromMaybe)
import Data.List  (sort, group)
import qualified Data.Map as Map
import qualified Data.IntMap as IntMap
import Data.SafeCopy (base, deriveSafeCopy)
import Data.Typeable (Typeable)
import Control.Exception (assert)


-- | The entire collection of users. Manages the mapping between 'UserName'
-- and 'UserId'.
--
data Users = Users {
    -- | A map from UserId to UserInfo
    userIdMap   :: !(IntMap.IntMap UserInfo),
    -- | A map from active UserNames to the UserId for that name
    userNameMap :: !(Map.Map UserName UserId),
    -- | The next available UserId
    nextId      :: !UserId
  }
  deriving (Eq, Typeable, Show)

instance MemSize Users where
  memSize (Users a b c) = memSize3 a b c

$(deriveSafeCopy 0 'base ''Users)

checkinvariant :: Users -> Users
checkinvariant users = assert (invariant users) users

invariant :: Users -> Bool
invariant Users{userIdMap, userNameMap, nextId} =
      nextIdIsRight
   && noUserNameOverlap
   && userNameMapComplete
   && userNameMapConsistent
  where
    nextIdIsRight =
      --  1) the next id should be 0 if the userIdMap is empty
      --     or one bigger than the maximum allocated id
      let UserId nextid = nextId
       in nextid == case IntMap.maxViewWithKey userIdMap of
                      Nothing                     -> 0
                      Just ((maxAllocatedId,_),_) -> maxAllocatedId + 1

    noUserNameOverlap =
      --  2) there must be no overlap in the user names of active accounts
      --     (active are enabled or disabled but not deleted)
          all (\g -> length g == 1)
        . group . sort
        . map userName . filter (isActiveAccount . userStatus)
        . IntMap.elems
        $ userIdMap

    userNameMapComplete =
      -- 3) the userNameMap must map every active user name to the id of the
      --    corresponding user info
          Map.keys userNameMap
       == sort [ userName uinfo
               | uinfo <- IntMap.elems userIdMap
               , isActiveAccount (userStatus uinfo)]

    userNameMapConsistent =
      and [ case IntMap.lookup uid userIdMap of
              Nothing    -> False
              Just uinfo -> userName uinfo == uname
          | (uname, UserId uid) <- Map.toList userNameMap ]

  -- the point is, user names can be recycled but user ids never are
  -- this simplifies things because other user groups in the system do not
  -- need to be adjusted when an account is enabled/disabled/deleted
  -- it also allows us to track historical info, like name of uploader
  -- even if that user name has been recycled, the user ids will be distinct.


emptyUsers :: Users
emptyUsers = Users {
    userIdMap   = IntMap.empty,
    userNameMap = Map.empty,
    nextId      = UserId 0
  }

-- error codes
data ErrUserNameClash = ErrUserNameClash deriving Typeable
data ErrUserIdClash   = ErrUserIdClash   deriving Typeable
data ErrNoSuchUserId  = ErrNoSuchUserId  deriving Typeable
data ErrDeletedUser   = ErrDeletedUser   deriving Typeable

$(deriveSafeCopy 0 'base ''ErrUserNameClash)
$(deriveSafeCopy 0 'base ''ErrUserIdClash)
$(deriveSafeCopy 0 'base ''ErrNoSuchUserId)
$(deriveSafeCopy 0 'base ''ErrDeletedUser)

(?!) :: Maybe a -> e -> Either e a
ma ?! e = maybe (Left e) Right ma



lookupUserId :: UserId -> Users -> Maybe UserInfo
lookupUserId (UserId userId) users = IntMap.lookup userId (userIdMap users)

lookupUserName :: UserName -> Users -> Maybe (UserId, UserInfo)
lookupUserName uname users = do
    case Map.lookup uname (userNameMap users) of
      Nothing  -> Nothing
      Just uid -> Just (uid, fromMaybe impossible (lookupUserId uid users))
  where
    impossible = error "lookupUserName: invariant violation"

-- | Convert a 'UserId' to a 'UserName'. If the user id doesn't exist,
-- an ugly placeholder is used instead.
--
userIdToName :: Users -> UserId -> UserName
userIdToName users userId@(UserId idNum) =
    case lookupUserId userId users of
      Just user -> userName user
      Nothing   -> UserName $ "~id#" ++ show idNum


-- | Add a new user account, in the enabled state.
--
addUserEnabled :: UserName -> UserAuth -> Users
               -> Either ErrUserNameClash (Users, UserId)
addUserEnabled name auth = addUser name (AccountEnabled auth)

-- | Add a new user account, in the disabled state and with no password.
--
addUserDisabled :: UserName -> Users
                -> Either ErrUserNameClash (Users, UserId)
addUserDisabled name = addUser name (AccountDisabled Nothing)

-- | Add a new user account with the given user status.
--
addUser :: UserName -> UserStatus -> Users -> Either ErrUserNameClash (Users, UserId)
addUser name status users =
  case Map.lookup name (userNameMap users) of
    Just _  -> Left ErrUserNameClash
    Nothing -> users' `seq` Right (users', userid)
      where
        userid@(UserId uid) = nextId users
        uinfo = UserInfo {
          userName   = name,
          userStatus = status
        }
        users' = checkinvariant users {
          userIdMap   = IntMap.insert uid uinfo (userIdMap users),
          userNameMap = Map.insert name userid (userNameMap users),
          nextId      = UserId (uid + 1)
        }

-- | Insert pre-existing user info. This should only be used for constructing
-- a user db manually or from a backup.
--
insertUserAccount :: UserId -> UserInfo -> Users
                  -> Either (Either ErrUserIdClash ErrUserNameClash) Users
insertUserAccount userId@(UserId uid) uinfo users = do
    guard (not userIdInUse)                     ?! Left  ErrUserIdClash
    guard (not userNameInUse || isUserDeleted)  ?! Right ErrUserNameClash
    return $! checkinvariant users {
          userIdMap   = IntMap.insert uid uinfo (userIdMap users),
          userNameMap = if isUserDeleted
                          then userNameMap users
                          else Map.insert (userName uinfo) userId (userNameMap users),
          nextId      = let UserId nextid = nextId users
                        in UserId (max nextid (uid + 1))
        }
  where
    userIdInUse   = IntMap.member uid (userIdMap users)
    userNameInUse = Map.member (userName uinfo) (userNameMap users)
    isUserDeleted = case userStatus uinfo of
                      AccountDeleted -> True
                      _              -> False


-- | Delete a user account.
--
-- Prevents the given user from performing authenticated operations.
-- This operation is idempotent but not reversible. Deleting an account forgets
-- any authentication credentials and the user name becomes available for
-- re-use in a new account.
--
-- Unlike 'UserName's, 'UserId's are never actually deleted or re-used. This is
-- what distinguishes disabling and deleting an account; a disabled account can
-- be enabled again and a disabled account does not release the user name for
-- re-use.
--
deleteUser :: UserId -> Users -> Either ErrNoSuchUserId Users
deleteUser (UserId userId) users = do
  userInfo     <- lookupUserId (UserId userId) users ?! ErrNoSuchUserId
  let userInfo' = userInfo { userStatus = AccountDeleted }
  return $! checkinvariant users {
    userIdMap   = IntMap.insert userId userInfo' (userIdMap users),
    userNameMap = Map.delete (userName userInfo) (userNameMap users)
  }

-- | Change the status of a user account to enabled or disabled.
--
-- Prevents the given user from performing any authenticated operations.
-- This operation is idempotent and reversable. Use 'enable' to re-enable a
-- disabled account.
--
-- The disabled state is intended to be temporary. Use 'delete' to permanently
-- delete the account and release the user name to be re-used.
--
setUserEnabledStatus :: UserId -> Bool -> Users -> Either (Either ErrNoSuchUserId ErrDeletedUser) Users
setUserEnabledStatus (UserId uid) enable users = do
    userInfo  <- lookupUserId (UserId uid) users ?! Left  ErrNoSuchUserId
    userInfo' <- changeStatus userInfo           ?! Right ErrDeletedUser
    return $! checkinvariant users {
        userIdMap = IntMap.insert uid userInfo' (userIdMap users)
    }
  where
    changeStatus userInfo | enable = case userStatus userInfo of
        AccountEnabled  _           -> Just userInfo
        AccountDisabled (Just auth) -> Just userInfo { userStatus = AccountEnabled auth }
        AccountDisabled Nothing     -> Nothing
        AccountDeleted              -> Nothing

    changeStatus userInfo          = case userStatus userInfo of
        AccountEnabled  auth       -> Just userInfo { userStatus = AccountDisabled (Just auth) }
        AccountDisabled _          -> Just userInfo
        AccountDeleted             -> Nothing

-- | Replace the user authentication for the given user.
--
setUserAuth :: UserId -> UserAuth -> Users -> Either (Either ErrNoSuchUserId ErrDeletedUser) Users
setUserAuth (UserId uid) newauth users = do
    userInfo  <- lookupUserId (UserId uid) users ?! Left  ErrNoSuchUserId
    userInfo' <- changeAuth userInfo             ?! Right ErrDeletedUser
    return $! checkinvariant users {
      userIdMap = IntMap.insert uid userInfo' (userIdMap users)
    }
  where
    changeAuth userInfo = case userStatus userInfo of
        AccountEnabled  _oldauth -> Just $ userInfo { userStatus = AccountEnabled newauth }
        AccountDisabled _oldauth -> Just $ userInfo { userStatus = AccountDisabled (Just newauth) }
        AccountDeleted           -> Nothing

-- | Change the username for a user account. The new name must not be in use.
--
setUserName :: UserId -> UserName -> Users
            -> Either (Either ErrNoSuchUserId ErrUserNameClash) Users
setUserName (UserId uid) newname users = do
    userinfo  <- lookupUserId (UserId uid) users ?! Left  ErrNoSuchUserId
    guard (not (userNameInUse newname))          ?! Right ErrUserNameClash
    let oldname   = userName userinfo
        userinfo' = userinfo { userName = newname }
    return $! checkinvariant users {
      userIdMap   = IntMap.insert uid userinfo' (userIdMap users),
      userNameMap = Map.insert newname (UserId uid) . Map.delete oldname $ userNameMap users
    }
  where
    userNameInUse uname = Map.member uname (userNameMap users)

enumerateAllUsers :: Users -> [(UserId, UserInfo)]
enumerateAllUsers users =
    [ (UserId uid, uinfo) | (uid, uinfo) <- IntMap.assocs (userIdMap users) ]

enumerateActiveUsers :: Users -> [(UserId, UserInfo)]
enumerateActiveUsers users =
    [ (UserId uid, uinfo) | (uid, uinfo) <- IntMap.assocs (userIdMap users)
                          , isActiveAccount (userStatus uinfo) ]
