{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE NoDisambiguateRecordFields, NoRecordWildCards #-}
module Servant.Server.Auth.Token.Acid.Schema where

import Control.Monad.Reader
import Control.Monad.State
import Data.Acid
import Data.Aeson.WithField
import Data.Int
import Data.List (sortBy)
import Data.Map.Strict (Map)
import Data.Ord
import Data.SafeCopy
import Data.Text (Text)
import Data.Time
import Language.Haskell.TH
import Safe

import Servant.API.Auth.Token
import Servant.API.Auth.Token.Pagination
import Servant.Server.Auth.Token.Common
import Servant.Server.Auth.Token.Model(
    UserImplId
  , UserImpl(..)
  , UserPermId
  , UserPerm(..)
  , AuthTokenId
  , AuthToken(..)
  , UserRestoreId
  , UserRestore(..)
  , UserSingleUseCodeId
  , UserSingleUseCode(..)
  , AuthUserGroupId
  , AuthUserGroup(..)
  , AuthUserGroupUsersId
  , AuthUserGroupUsers(..)
  , AuthUserGroupPermsId
  , AuthUserGroupPerms(..)
  )

import qualified Data.Map.Strict as M
import qualified Data.Foldable as F

-- | Holds all data for auth server in acid-state container
data Model = Model {
  -- | Holds users by id
  modelUsers                    :: !(Map UserImplId UserImpl)
  -- | Holds users by login (same content as 'modelUsers')
, modelUsersByLogin             :: !(Map Login (WithId UserImplId UserImpl))
  -- | Holds 'UserPerm'
, modelUserPerms                :: !(Map UserPermId UserPerm)
  -- | Holds 'AuthToken'
, modelAuthTokens               :: !(Map AuthTokenId AuthToken)
  -- | Holds 'UserRestore'
, modelUserRestores             :: !(Map UserRestoreId UserRestore)
  -- | Holds 'UserSingleUseCode'
, modelUserSingleUseCodes       :: !(Map UserSingleUseCodeId UserSingleUseCode)
  -- | Holds 'AuthUserGroup'
, modelAuthUserGroups           :: !(Map AuthUserGroupId AuthUserGroup)
  -- | Holds 'AuthUserGroupUsers'
, modelAuthUserGroupUsers       :: !(Map AuthUserGroupUsersId AuthUserGroupUsers)
  -- | Holds 'AuthUserGroupPerms'
, modelAuthUserGroupPerms       :: !(Map AuthUserGroupPermsId AuthUserGroupPerms)
  -- | Holds next id for entities
, modelNextUserImplId           :: !Int64
-- | Holds next id for entities
, modelNextUserPermId           :: !Int64
-- | Holds next id for entities
, modelNextAuthTokenId          :: !Int64
-- | Holds next id for entities
, modelNextUserRestoreId        :: !Int64
-- | Holds next id for entities
, modelNextUserSingleUseCodeId  :: !Int64
-- | Holds next id for entities
, modelNextAuthUserGroupId      :: !Int64
-- | Holds next id for entities
, modelNextAuthUserGroupUserId  :: !Int64
-- | Holds next id for entities
, modelNextAuthUserGroupPermId  :: !Int64
}

-- | Defines empty model for new database
newModel :: Model
newModel = Model {
    modelUsers = mempty
  , modelUsersByLogin = mempty
  , modelUserPerms = mempty
  , modelAuthTokens = mempty
  , modelUserRestores = mempty
  , modelUserSingleUseCodes = mempty
  , modelAuthUserGroups = mempty
  , modelAuthUserGroupUsers = mempty
  , modelAuthUserGroupPerms = mempty
  , modelNextUserImplId = 0
  , modelNextUserPermId = 0
  , modelNextAuthTokenId = 0
  , modelNextUserRestoreId = 0
  , modelNextUserSingleUseCodeId = 0
  , modelNextAuthUserGroupId = 0
  , modelNextAuthUserGroupUserId = 0
  , modelNextAuthUserGroupPermId = 0
  }

-- | The end user should implement this for his global type
class HasModelRead a where
  askModel :: a -> Model

-- | The end user should implement this fot his global type
class HasModelRead a => HasModelWrite a where
  putModel :: a -> Model -> a

-- | List of queries of the backend. Can be used if you want additional queries alongside
-- with the auth ones.
--
-- Usage:
-- @
-- makeAcidic ''Model (acidQueries ++ [{- your queries herer-}])
-- @
acidQueries :: [Name]
acidQueries = [
    mkName "getUserImpl"
  , mkName "getUserImplByLogin"
  , mkName "listUsersPaged"
  , mkName "getUserImplPermissions"
  , mkName "deleteUserPermissions"
  , mkName "insertUserPerm"
  , mkName "insertUserImpl"
  , mkName "replaceUserImpl"
  , mkName "deleteUserImpl"
  , mkName "hasPerm"
  , mkName "getFirstUserByPerm"
  , mkName "selectUserImplGroups"
  , mkName "clearUserImplGroups"
  , mkName "insertAuthUserGroup"
  , mkName "insertAuthUserGroupUsers"
  , mkName "insertAuthUserGroupPerms"
  , mkName "getAuthUserGroup"
  , mkName "listAuthUserGroupPermissions"
  , mkName "listAuthUserGroupUsers"
  , mkName "replaceAuthUserGroup"
  , mkName "clearAuthUserGroupUsers"
  , mkName "clearAuthUserGroupPerms"
  , mkName "deleteAuthUserGroup"
  , mkName "listGroupsPaged"
  , mkName "setAuthUserGroupName"
  , mkName "setAuthUserGroupParent"
  , mkName "insertSingleUseCode"
  , mkName "setSingleUseCodeUsed"
  , mkName "getUnusedCode"
  , mkName "invalidatePermanentCodes"
  , mkName "selectLastRestoreCode"
  , mkName "insertUserRestore"
  , mkName "findRestoreCode"
  , mkName "replaceRestoreCode"
  , mkName "findAuthToken"
  , mkName "findAuthTokenByValue"
  , mkName "insertAuthToken"
  , mkName "replaceAuthToken"
  ]

-- | The end user should inline this TH in his code
makeModelAcidic :: Name -> DecsQ
makeModelAcidic globalStateName = makeAcidic globalStateName acidQueries

instance HasModelRead Model where
  askModel = id

instance HasModelWrite Model where
  putModel = const id

asksM :: HasModelRead a => (Model -> b) -> Query a b
asksM f = fmap (f . askModel) ask

modifyM :: HasModelWrite a => (Model -> Model) -> Update a ()
modifyM f = modify' (\a -> putModel a . f . askModel $ a)

getM :: HasModelWrite a => Update a Model
getM = fmap askModel get

putM :: HasModelWrite a => Model -> Update a ()
putM m = modifyM (const m)

-- | Mixin queries to work with auth state
deriveQueries :: Name -> DecsQ
deriveQueries globalStateName = [d|
    -- Getting user from storage
    getUserImpl :: HasModelRead $a => UserImplId -> Query $a (Maybe UserImpl)
    getUserImpl i = M.lookup i <$> asksM modelUsers

    -- Getting user from storage by login
    getUserImplByLogin :: HasModelRead $a => Login -> Query $a (Maybe (WithId UserImplId UserImpl))
    getUserImplByLogin l = M.lookup l <$> asksM modelUsersByLogin

    -- Helper to get page from map
    getPagedList :: Ord i => Page -> PageSize -> Map i a -> ([WithId i a], Word)
    getPagedList p s m = (uncurry WithField <$> es, fromIntegral $ F.length m)
      where
        es = take (fromIntegral s) . drop (fromIntegral $ p * s) . sortBy (comparing fst) . M.toList $ m

    -- Get paged list of users and total count of users
    listUsersPaged :: HasModelRead $a => Page -> PageSize -> Query $a ([WithId UserImplId UserImpl], Word)
    listUsersPaged p s = getPagedList p s <$> asksM modelUsers

    -- Get user permissions, ascending by tag
    getUserImplPermissions :: HasModelRead $a => UserImplId -> Query $a [WithId UserPermId UserPerm]
    getUserImplPermissions i = fmap (uncurry WithField) . M.toList . M.filter ((i ==) . userPermUser) <$> asksM modelUserPerms

    -- Delete user permissions
    deleteUserPermissions :: HasModelWrite $a => UserImplId -> Update $a ()
    deleteUserPermissions i = modifyM $ \m -> m { modelUserPerms = f $ modelUserPerms m }
      where
        f m = m `M.difference` M.filter ((i ==) . userPermUser) m

    -- Insertion of new user permission
    insertUserPerm :: HasModelWrite $a => UserPerm -> Update $a UserPermId
    insertUserPerm p = do
      m <- getM
      let
        i = toKey $ modelNextUserPermId m
        perms = M.insert i p . modelUserPerms $ m
        m' = m { modelUserPerms = perms, modelNextUserPermId = modelNextUserPermId m + 1 }
      m' `seq` putM m'
      return i

    -- Insertion of new user
    insertUserImpl :: HasModelWrite $a => UserImpl -> Update $a UserImplId
    insertUserImpl v = do
      m <- getM
      let
        i = toKey $ modelNextUserImplId m
        vals = M.insert i v . modelUsers $ m
        vals' = M.insert (userImplLogin v) (WithField i v) . modelUsersByLogin $ m
        m' = m { modelUsers = vals, modelUsersByLogin = vals', modelNextUserImplId = modelNextUserImplId m + 1 }
      m' `seq` putM m'
      return i

    -- Replace user with new value
    replaceUserImpl :: HasModelWrite $a => UserImplId -> UserImpl -> Update $a ()
    replaceUserImpl i v = modifyM $ \m -> m {
        modelUsers = M.insert i v . modelUsers $ m
      , modelUsersByLogin = M.insert (userImplLogin v) (WithField i v) . modelUsersByLogin $ m
      }

    -- Delete user by id
    deleteUserImpl :: HasModelWrite $a => UserImplId -> Update $a ()
    deleteUserImpl i = do
      deleteUserPermissions i
      modifyM $ \m -> case M.lookup i . modelUsers $ m of
        Nothing -> m
        Just ui -> m {
            modelUsers = M.delete i . modelUsers $ m
          , modelUsersByLogin = M.delete (userImplLogin ui) . modelUsersByLogin $ m
          }

    -- Check whether the user has particular permission
    hasPerm :: HasModelRead $a => UserImplId -> Permission -> Query $a Bool
    hasPerm i p = (> 0) . F.length . M.filter (\up -> userPermUser up == i && userPermPermission up == p) <$> asksM modelUserPerms

    -- Get any user with given permission
    getFirstUserByPerm :: HasModelRead $a => Permission -> Query $a (Maybe (WithId UserImplId UserImpl))
    getFirstUserByPerm perm = do
      m <- asksM modelUserPerms
      case M.toList . M.filter (\p -> userPermPermission p == perm) $ m of
        [] -> return Nothing
        ((_, p) : _) -> fmap (WithField $ userPermUser p) <$> getUserImpl (userPermUser p)

    -- Select user groups and sort them by ascending name
    selectUserImplGroups :: HasModelRead $a => UserImplId -> Query $a [WithId AuthUserGroupUsersId AuthUserGroupUsers]
    selectUserImplGroups i = fmap (uncurry WithField) . M.toList . M.filter ((i ==) . authUserGroupUsersUser) <$> asksM modelAuthUserGroupUsers

    -- Remove user from all groups
    clearUserImplGroups :: HasModelWrite $a => UserImplId -> Update $a ()
    clearUserImplGroups i = modifyM $ \m -> m { modelAuthUserGroupUsers = f $ modelAuthUserGroupUsers m }
      where
        f m = m `M.difference` M.filter ((i ==) . authUserGroupUsersUser) m

    -- Add new user group
    insertAuthUserGroup :: HasModelWrite $a => AuthUserGroup -> Update $a AuthUserGroupId
    insertAuthUserGroup v = do
      m <- getM
      let
        i = toKey $ modelNextAuthUserGroupId m
        vals = M.insert i v . modelAuthUserGroups $ m
        m' = m { modelAuthUserGroups = vals, modelNextAuthUserGroupId = modelNextAuthUserGroupId m + 1 }
      m' `seq` putM m'
      return i

    -- Add user to given group
    insertAuthUserGroupUsers :: HasModelWrite $a => AuthUserGroupUsers -> Update $a AuthUserGroupUsersId
    insertAuthUserGroupUsers v = do
      m <- getM
      let
        i = toKey $ modelNextAuthUserGroupUserId m
        vals = M.insert i v . modelAuthUserGroupUsers $ m
        m' = m { modelAuthUserGroupUsers = vals, modelNextAuthUserGroupUserId = modelNextAuthUserGroupUserId m + 1 }
      m' `seq` putM m'
      return i

    -- Add permission to given group
    insertAuthUserGroupPerms :: HasModelWrite $a => AuthUserGroupPerms -> Update $a AuthUserGroupPermsId
    insertAuthUserGroupPerms v = do
      m <- getM
      let
        i = toKey $ modelNextAuthUserGroupPermId m
        vals = M.insert i v . modelAuthUserGroupPerms $ m
        m' = m { modelAuthUserGroupPerms = vals, modelNextAuthUserGroupPermId = modelNextAuthUserGroupPermId m + 1 }
      m' `seq` putM m'
      return i

    -- Find user group by id
    getAuthUserGroup :: HasModelRead $a => AuthUserGroupId -> Query $a (Maybe AuthUserGroup)
    getAuthUserGroup i = M.lookup i <$> asksM modelAuthUserGroups

    -- Get list of permissions of given group
    listAuthUserGroupPermissions :: HasModelRead $a => AuthUserGroupId -> Query $a [WithId AuthUserGroupPermsId AuthUserGroupPerms]
    listAuthUserGroupPermissions i = fmap (uncurry WithField) . M.toList . M.filter ((i ==) . authUserGroupPermsGroup) <$> asksM modelAuthUserGroupPerms

    -- Get list of all users of the group
    listAuthUserGroupUsers :: HasModelRead $a => AuthUserGroupId -> Query $a [WithId AuthUserGroupUsersId AuthUserGroupUsers]
    listAuthUserGroupUsers i = fmap (uncurry WithField) . M.toList . M.filter ((i ==) . authUserGroupUsersGroup) <$> asksM modelAuthUserGroupUsers

    -- Replace record of user group
    replaceAuthUserGroup :: HasModelWrite $a => AuthUserGroupId -> AuthUserGroup -> Update $a ()
    replaceAuthUserGroup i v = modifyM $ \m -> m { modelAuthUserGroups = M.insert i v $ modelAuthUserGroups m }

    -- Remove all users from group
    clearAuthUserGroupUsers :: HasModelWrite $a => AuthUserGroupId -> Update $a ()
    clearAuthUserGroupUsers i = modifyM $ \m -> m { modelAuthUserGroupUsers = f $ modelAuthUserGroupUsers m }
      where
        f m = m `M.difference` M.filter ((i ==) . authUserGroupUsersGroup) m

    -- Remove all permissions from group
    clearAuthUserGroupPerms :: HasModelWrite $a => AuthUserGroupId -> Update $a ()
    clearAuthUserGroupPerms i = modifyM $ \m -> m { modelAuthUserGroupPerms = f $ modelAuthUserGroupPerms m }
      where
        f m = m `M.difference` M.filter ((i ==) . authUserGroupPermsGroup) m

    -- Delete user group from storage
    deleteAuthUserGroup :: HasModelWrite $a => AuthUserGroupId -> Update $a ()
    deleteAuthUserGroup i = do
      clearAuthUserGroupUsers i
      clearAuthUserGroupPerms i
      modifyM $ \m -> m { modelAuthUserGroups = M.delete i $ modelAuthUserGroups m }

    -- Get paged list of user groups with total count
    listGroupsPaged :: HasModelRead $a => Page -> PageSize -> Query $a ([WithId AuthUserGroupId AuthUserGroup], Word)
    listGroupsPaged p s = getPagedList p s <$> asksM modelAuthUserGroups

    -- Set group name
    setAuthUserGroupName :: HasModelWrite $a => AuthUserGroupId -> Text -> Update $a ()
    setAuthUserGroupName i n = modifyM $ \m -> m { modelAuthUserGroups = M.adjust (\v -> v { authUserGroupName = n }) i $ modelAuthUserGroups m }

    -- Set group parent
    setAuthUserGroupParent :: HasModelWrite $a => AuthUserGroupId -> Maybe AuthUserGroupId -> Update $a ()
    setAuthUserGroupParent i p = modifyM $ \m -> m { modelAuthUserGroups = M.adjust (\v -> v { authUserGroupParent = p }) i $ modelAuthUserGroups m }

    -- Add new single use code
    insertSingleUseCode :: HasModelWrite $a => UserSingleUseCode -> Update $a UserSingleUseCodeId
    insertSingleUseCode v = do
      m <- getM
      let
        i = toKey $ modelNextUserSingleUseCodeId m
        vals = M.insert i v . modelUserSingleUseCodes $ m
        m' = m { modelUserSingleUseCodes = vals, modelNextUserSingleUseCodeId = modelNextUserSingleUseCodeId m + 1 }
      m' `seq` putM m'
      return i

    -- Set usage time of the single use code
    setSingleUseCodeUsed :: HasModelWrite $a => UserSingleUseCodeId -> Maybe UTCTime -> Update $a ()
    setSingleUseCodeUsed i mt = modifyM $ \m -> m { modelUserSingleUseCodes = M.adjust (\v -> v { userSingleUseCodeUsed = mt }) i $ modelUserSingleUseCodes m }

    -- Find unused code for the user and expiration time greater than the given time
    getUnusedCode :: HasModelRead $a => SingleUseCode -> UserImplId -> UTCTime -> Query $a (Maybe (WithId UserSingleUseCodeId UserSingleUseCode))
    getUnusedCode c i t = fmap (uncurry WithField) . headMay . sorting . M.toList . M.filter f <$> asksM modelUserSingleUseCodes
      where
        sorting = sortBy (comparing $ Down . userSingleUseCodeExpire . snd)
        f usc =
             userSingleUseCodeValue usc == c
          && userSingleUseCodeUser usc == i
          && userSingleUseCodeUsed usc == Nothing
          && (userSingleUseCodeExpire usc == Nothing || userSingleUseCodeExpire usc >= Just t)

    -- Invalidate all permanent codes for user and set use time for them
    invalidatePermanentCodes :: HasModelWrite $a => UserImplId -> UTCTime -> Update $a ()
    invalidatePermanentCodes i t = modifyM $ \m -> m { modelUserSingleUseCodes = f $ modelUserSingleUseCodes m }
      where
        f m = (fmap invalidate . M.filter isPermanent $ m) `M.union` m
        invalidate su = su { userSingleUseCodeUsed = Just t }
        isPermanent usc =
             userSingleUseCodeUser usc == i
          && userSingleUseCodeUsed usc == Nothing
          && userSingleUseCodeExpire usc == Nothing

    -- Select last valid restoration code by the given current time
    selectLastRestoreCode :: HasModelRead $a => UserImplId -> UTCTime -> Query $a (Maybe (WithId UserRestoreId UserRestore))
    selectLastRestoreCode i t = fmap (uncurry WithField) . headMay . sorting . M.toList . M.filter f <$> asksM modelUserRestores
      where
        sorting = sortBy (comparing $ Down . userRestoreExpire . snd)
        f ur = userRestoreUser ur == i && userRestoreExpire ur > t

    -- Insert new restore code
    insertUserRestore :: HasModelWrite $a => UserRestore -> Update $a UserRestoreId
    insertUserRestore v = do
      m <- getM
      let
        i = toKey $ modelNextUserRestoreId m
        vals = M.insert i v . modelUserRestores $ m
        m' = m { modelUserRestores = vals, modelNextUserRestoreId = modelNextUserRestoreId m + 1 }
      m' `seq` putM m'
      return i

    -- Find unexpired by the time restore code
    findRestoreCode :: HasModelRead $a => UserImplId -> RestoreCode -> UTCTime -> Query $a (Maybe (WithId UserRestoreId UserRestore))
    findRestoreCode i rc t = fmap (uncurry WithField) . headMay . sorting . M.toList . M.filter f <$> asksM modelUserRestores
      where
        sorting = sortBy (comparing $ Down . userRestoreExpire . snd)
        f ur = userRestoreUser ur == i && userRestoreValue ur == rc && userRestoreExpire ur > t

    -- Replace restore code with new value
    replaceRestoreCode :: HasModelWrite $a => UserRestoreId -> UserRestore -> Update $a ()
    replaceRestoreCode i v = modifyM $ \m -> m { modelUserRestores = M.insert i v $ modelUserRestores m }

    -- Find first non-expired by the time token for user
    findAuthToken :: HasModelRead $a => UserImplId -> UTCTime -> Query $a (Maybe (WithId AuthTokenId AuthToken))
    findAuthToken i t = fmap (uncurry WithField) . headMay . M.toList . M.filter f <$> asksM modelAuthTokens
      where
        f atok = authTokenUser atok == i && authTokenExpire atok > t

    -- Find token by value
    findAuthTokenByValue :: HasModelRead $a => SimpleToken -> Query $a (Maybe (WithId AuthTokenId AuthToken))
    findAuthTokenByValue v = fmap (uncurry WithField) . headMay . M.toList . M.filter f <$> asksM modelAuthTokens
      where
        f atok = authTokenValue atok == v

    -- Insert new token
    insertAuthToken :: HasModelWrite $a => AuthToken -> Update $a AuthTokenId
    insertAuthToken v = do
      m <- getM
      let
        i = toKey $ modelNextAuthTokenId m
        vals = M.insert i v . modelAuthTokens $ m
        m' = m { modelAuthTokens = vals, modelNextAuthTokenId = modelNextAuthTokenId m + 1 }
      m' `seq` putM m'
      return i

    -- Replace auth token with new value
    replaceAuthToken :: HasModelWrite $a => AuthTokenId -> AuthToken -> Update $a ()
    replaceAuthToken i v = modifyM $ \m -> m { modelAuthTokens = M.insert i v $ modelAuthTokens m }
    |]
  where
    a = conT globalStateName

deriveSafeCopy 0 'base ''UserImplId
deriveSafeCopy 0 'base ''UserImpl
deriveSafeCopy 0 'base ''UserPermId
deriveSafeCopy 0 'base ''UserPerm
deriveSafeCopy 0 'base ''AuthTokenId
deriveSafeCopy 0 'base ''AuthToken
deriveSafeCopy 0 'base ''UserRestoreId
deriveSafeCopy 0 'base ''UserRestore
deriveSafeCopy 0 'base ''UserSingleUseCodeId
deriveSafeCopy 0 'base ''UserSingleUseCode
deriveSafeCopy 0 'base ''AuthUserGroupId
deriveSafeCopy 0 'base ''AuthUserGroup
deriveSafeCopy 0 'base ''AuthUserGroupUsersId
deriveSafeCopy 0 'base ''AuthUserGroupUsers
deriveSafeCopy 0 'base ''AuthUserGroupPermsId
deriveSafeCopy 0 'base ''AuthUserGroupPerms
deriveSafeCopy 0 'base ''Model

instance (SafeCopy k, SafeCopy v) => SafeCopy (WithField i k v) where
  putCopy (WithField k v) = contain $ do
    safePut k
    safePut v
  getCopy = contain $ WithField
    <$> safeGet
    <*> safeGet