{-# LANGUAGE QuasiQuotes                #-}
{-# LANGUAGE TemplateHaskell            #-}
{-|
Module      : Servant.Server.Auth.Token.Model
Description : Internal operations with RDBMS
Copyright   : (c) Anton Gushcha, 2016
License     : MIT
Maintainer  : ncrashed@gmail.com
Stability   : experimental
Portability : Portable
-}
module Servant.Server.Auth.Token.Model(
  -- * DB entities
    UserImpl(..)
  , UserPerm(..)
  , AuthToken(..)
  , UserRestore(..)
  , AuthUserGroup(..)
  , AuthUserGroupUsers(..)
  , AuthUserGroupPerms(..)
  , EntityField(..)
  -- * IDs of entities
  , UserImplId
  , UserPermId
  , AuthTokenId
  , UserRestoreId
  , AuthUserGroupId
  , AuthUserGroupUsersId
  , AuthUserGroupPermsId
  -- * Operations
  , runDB
  , migrateAll
  , passToByteString
  , byteStringToPass
  -- ** User
  , userToUserInfo
  , readUserInfo
  , getUserPermissions
  , setUserPermissions
  , createUser
  , hasPerm
  , hasPerms
  , createAdmin
  , ensureAdmin
  , patchUser
  , setUserPassword'
  -- ** User groups
  , getUserGroups
  , setUserGroups
  , validateGroups
  , getGroupPermissions
  , getUserGroupPermissions
  , getUserAllPermissions
  , readUserGroup
  , toAuthUserGroup
  , insertUserGroup
  , updateUserGroup
  , deleteUserGroup
  , patchUserGroup
  ) where 

import Control.Monad 
import Control.Monad.IO.Class 
import Control.Monad.Reader 
import Crypto.PasswordStore
import Data.Maybe
import Data.Monoid
import Data.Text (Text)
import Data.Time
import Database.Persist.Postgresql
import Database.Persist.TH 
import GHC.Generics

import qualified Data.ByteString as BS 
import qualified Data.Foldable as F 
import qualified Data.List as L 
import qualified Data.Sequence as S 
import qualified Data.Text.Encoding as TE 

import Servant.API.Auth.Token
import Servant.Server.Auth.Token.Common 
import Servant.Server.Auth.Token.Config 
import Servant.Server.Auth.Token.Patch 

share [mkPersist sqlSettings
     , mkDeleteCascade sqlSettings
     , mkMigrate "migrateAll"] [persistLowerCase|
UserImpl
  login       Login
  password    Password     -- encrypted with salt
  email       Email
  UniqueLogin login
  deriving Generic Show

UserPerm 
  user        UserImplId 
  permission  Permission
  deriving Generic Show

AuthToken
  value       SimpleToken 
  user        UserImplId
  expire      UTCTime
  deriving Generic Show

UserRestore
  value       RestoreCode
  user        UserImplId 
  expire      UTCTime 
  deriving Generic Show 

AuthUserGroup
  name        Text 
  parent      AuthUserGroupId Maybe
  deriving Generic Show 

AuthUserGroupUsers
  group       AuthUserGroupId 
  user        UserImplId 
  deriving Generic Show 

AuthUserGroupPerms
  group       AuthUserGroupId 
  permission  Permission
  deriving Generic Show 
|]

-- | Execute database transaction
runDB :: (MonadReader AuthConfig m, MonadIO m) => SqlPersistT IO b -> m b
runDB query = do
  pool <- asks getPool
  liftIO $ runSqlPool query pool

-- | Convert password to bytestring
passToByteString :: Password -> BS.ByteString
passToByteString = TE.encodeUtf8

-- | Convert bytestring into password
byteStringToPass :: BS.ByteString -> Password
byteStringToPass = TE.decodeUtf8

-- | Helper to convert user to response
userToUserInfo :: Entity UserImpl -> [Permission] -> [UserGroupId] -> RespUserInfo 
userToUserInfo (Entity uid UserImpl{..}) perms groups = RespUserInfo {
      respUserId = fromIntegral $ fromSqlKey uid
    , respUserLogin = userImplLogin
    , respUserEmail = userImplEmail
    , respUserPermissions = perms 
    , respUserGroups = groups
  }

-- | Get user by id
readUserInfo :: UserId -> SqlPersistT IO (Maybe RespUserInfo)
readUserInfo uid' = do 
  let uid = toKey uid'
  muser <- get uid 
  case muser of 
    Nothing -> return Nothing 
    Just user -> do 
      perms <- getUserPermissions uid 
      groups <- getUserGroups uid 
      return . Just $ 
        userToUserInfo (Entity uid user) perms groups

-- | Return list of permissions for the given user (only permissions that are assigned to him directly)
getUserPermissions :: UserImplId -> SqlPersistT IO [Permission]
getUserPermissions uid = do 
  perms <- selectList [UserPermUser ==. uid] [Asc UserPermPermission]
  return $ userPermPermission . entityVal <$> perms

-- | Return list of permissions for the given user
setUserPermissions :: UserImplId -> [Permission] -> SqlPersistT IO ()
setUserPermissions uid perms = do 
  deleteWhere [UserPermUser ==. uid]
  forM_ perms $ void . insert . UserPerm uid

-- | Creation of new user
createUser :: Int -> Login -> Password -> Email -> [Permission] -> SqlPersistT IO UserImplId 
createUser strength login pass email perms = do
  pass' <- liftIO $ makePassword (passToByteString pass) strength
  i <- insert UserImpl {
      userImplLogin = login
    , userImplPassword = byteStringToPass pass'
    , userImplEmail = email 
    }
  forM_ perms $ void . insert . UserPerm i 
  return i

-- | Check whether the user has particular permission
hasPerm :: UserImplId -> Permission -> SqlPersistT IO Bool 
hasPerm i perm = do 
  c <- count [UserPermUser ==. i, UserPermPermission ==. perm]
  return $ c > 0

-- | Check whether the user has particular permissions
hasPerms :: UserImplId -> [Permission] -> SqlPersistT IO Bool 
hasPerms _ [] = return True
hasPerms i perms = do 
  perms' <- getUserAllPermissions i 
  return $ and $ (`elem` perms') <$> perms 

-- | Creates user with admin privileges
createAdmin :: Int -> Login -> Password -> Email -> SqlPersistT IO UserImplId
createAdmin strength login pass email = createUser strength login pass email [adminPerm]

-- | Ensures that DB has at leas one admin, if not, creates a new one
-- with specified info.
ensureAdmin :: Int -> Login -> Password -> Email -> SqlPersistT IO ()
ensureAdmin strength login pass email = do 
  madmin <- selectFirst [UserPermPermission ==. adminPerm] []
  whenNothing madmin $ void $ createAdmin strength login pass email 

-- | Apply patches for user
patchUser :: Int -- ^ Password strength
  -> PatchUser -> Entity UserImpl -> SqlPersistT IO (Entity UserImpl)
patchUser strength PatchUser{..} =  
        withPatch patchUserLogin (\l (Entity i u) -> pure $ Entity i u { userImplLogin = l })
    >=> withPatch patchUserPassword patchPassword
    >=> withPatch patchUserEmail (\e (Entity i u) -> pure $ Entity i u { userImplEmail = e })
    >=> withPatch patchUserPermissions patchPerms
    >=> withPatch patchUserGroups patchGroups
    where 
      patchPassword ps (Entity i u) = Entity <$> pure i <*> setUserPassword' strength ps u
      patchPerms ps (Entity i u) = do 
        setUserPermissions i ps
        return $ Entity i u
      patchGroups gs (Entity i u) = do 
        setUserGroups i gs
        return $ Entity i u

-- | Update password of user 
setUserPassword' :: MonadIO m => Int -- ^ Password strength
  -> Password -> UserImpl -> m UserImpl
setUserPassword' strength pass user = do
  pass' <- liftIO $ makePassword (passToByteString pass) strength
  return $ user { userImplPassword = byteStringToPass pass' }

-- | Get all groups the user belongs to
getUserGroups :: UserImplId -> SqlPersistT IO [UserGroupId]
getUserGroups i = fmap (fromKey . authUserGroupUsersGroup . entityVal)
  <$> selectList [AuthUserGroupUsersUser ==. i] [Asc AuthUserGroupUsersGroup]

-- | Rewrite all user groups
setUserGroups :: UserImplId -> [UserGroupId] -> SqlPersistT IO ()
setUserGroups i gs = do 
  deleteWhere [AuthUserGroupUsersUser ==. i]
  gs' <- validateGroups gs 
  forM_ gs' $ \g -> void $ insert (AuthUserGroupUsers g i)

-- | Leave only existing groups
validateGroups :: [UserGroupId] -> SqlPersistT IO [AuthUserGroupId]
validateGroups is = do
  pairs <- mapM ((\i -> (i,) <$> get i) . toKey) is 
  return $ fmap fst . filter (isJust . snd) $ pairs

-- | Getting permission of a group and all it parent groups
getGroupPermissions :: UserGroupId -> SqlPersistT IO [Permission]
getGroupPermissions = go S.empty S.empty . toKey
  where 
  go !visited !perms !i = do 
    mg <- get i 
    case mg of 
      Nothing -> return $ F.toList perms 
      Just AuthUserGroup{..} -> do 
        curPerms <- fmap (authUserGroupPermsPermission . entityVal) <$> 
          selectList [AuthUserGroupPermsGroup ==. i] [Asc AuthUserGroupPermsPermission]
        let perms' = perms <> S.fromList curPerms
        case authUserGroupParent of 
          Nothing -> return $ F.toList perms'
          Just pid -> if isJust $ pid `S.elemIndexL` visited
            then fail $ "Recursive user group graph: " <> show (visited S.|> pid)
            else go (visited S.|> pid) perms' pid 

-- | Get user permissions that are assigned to him/her via groups only
getUserGroupPermissions :: UserImplId -> SqlPersistT IO [Permission]
getUserGroupPermissions i = do 
  groups <- getUserGroups i 
  perms <- mapM getGroupPermissions groups 
  return $ L.sort . L.nub . concat $ perms

-- | Get user permissions that are assigned to him/her either by direct
-- way or by his/her groups.
getUserAllPermissions :: UserImplId -> SqlPersistT IO [Permission]
getUserAllPermissions i = do 
  permsDr <- getUserPermissions i 
  permsGr <- getUserGroupPermissions i 
  return $ L.sort . L.nub $ permsDr <> permsGr 

-- | Collect full info about user group from RDBMS
readUserGroup :: UserGroupId -> SqlPersistT IO (Maybe UserGroup)
readUserGroup i = do 
  let i' = toKey $ i 
  mu <- get i' 
  case mu of 
    Nothing -> return Nothing 
    Just AuthUserGroup{..} -> do 
      users <- fmap (authUserGroupUsersUser . entityVal) <$> 
        selectList [AuthUserGroupUsersGroup ==. i'] [Asc AuthUserGroupUsersUser]
      perms <- fmap (authUserGroupPermsPermission . entityVal) <$> 
        selectList [AuthUserGroupPermsGroup ==. i'] [Asc AuthUserGroupPermsPermission]
      return $ Just UserGroup {
          userGroupName = authUserGroupName 
        , userGroupUsers = fromKey <$> users 
        , userGroupPermissions = perms 
        , userGroupParent = fromKey <$> authUserGroupParent
        }

-- | Helper to convert user group into values of several tables
toAuthUserGroup :: UserGroup -> (AuthUserGroup, AuthUserGroupId -> [AuthUserGroupUsers], AuthUserGroupId -> [AuthUserGroupPerms])
toAuthUserGroup UserGroup{..} = (ag, users, perms)
  where 
  ag = AuthUserGroup {
      authUserGroupName = userGroupName 
    , authUserGroupParent = toKey <$> userGroupParent
    }
  users i = (\ui   -> AuthUserGroupUsers i (toKey $ ui)) <$> userGroupUsers 
  perms i = (\perm -> AuthUserGroupPerms i perm) <$> userGroupPermissions

-- | Insert user group into RDBMS
insertUserGroup :: UserGroup -> SqlPersistT IO UserGroupId
insertUserGroup u = do 
  let (ag, users, perms) = toAuthUserGroup u
  i <- insert ag 
  forM_ (users i) $ void . insert
  forM_ (perms i) $ void . insert
  return $ fromKey $ i 

-- | Replace user group with new value
updateUserGroup :: UserGroupId -> UserGroup -> SqlPersistT IO ()
updateUserGroup i u = do 
  let i' = toKey $ i 
  let (ag, users, perms) = toAuthUserGroup u
  replace i' ag 
  deleteWhere [AuthUserGroupUsersGroup ==. i'] 
  deleteWhere [AuthUserGroupPermsGroup ==. i']
  forM_ (users i') $ void . insert
  forM_ (perms i') $ void . insert

-- | Erase user group from RDBMS, cascade
deleteUserGroup :: UserGroupId -> SqlPersistT IO ()
deleteUserGroup i = do 
  let i' = toKey $ i 
  deleteWhere [AuthUserGroupUsersGroup ==. i'] 
  deleteWhere [AuthUserGroupPermsGroup ==. i']
  deleteCascade i'

-- | Partial update of user group
patchUserGroup :: UserGroupId -> PatchUserGroup -> SqlPersistT IO ()
patchUserGroup i PatchUserGroup{..} = do 
  let i' = toKey $ i
      patchName = (\n -> AuthUserGroupName =. n) <$> patchUserGroupName
      patchParent = case patchUserGroupNoParent of 
        Just True -> Just $ AuthUserGroupParent =. Nothing 
        _ -> (\p -> AuthUserGroupParent =. Just (toSqlKey .fromIntegral $ p)) <$> patchUserGroupParent
  update i' $ catMaybes [patchName, patchParent]
  whenJust patchUserGroupUsers $ \uids -> do
    deleteWhere [AuthUserGroupUsersGroup ==. i'] 
    forM_ uids $ insert . AuthUserGroupUsers i' . toKey  
  whenJust patchUserGroupPermissions $ \perms -> do
    deleteWhere [AuthUserGroupUsersGroup ==. i'] 
    forM_ perms $ insert . AuthUserGroupPerms i'