module Web.Wheb.Plugins.Auth 
  ( 
  
    login
  , logout
  , register
  , getCurrentUser
  , queryCurrentUser
  , loginRequired
  
  
  , authMiddleware 
  
  
  , AuthUser (..)
  , AuthContainer (..)
  , AuthApp (..)
  , AuthState (..)
  , AuthBackend (..)
  , AuthError (..)
  , UserKey
  , Password
  , PwHash
  
  
  , makePwHash
  , verifyPw
  , getUserSessionKey
  ) where
import Control.Monad.Except (liftM, MonadError(throwError), MonadIO(..))
import Crypto.PasswordStore (makePassword, verifyPassword)
import Data.Text.Encoding as ES (decodeUtf8, encodeUtf8)
import Data.Text.Lazy as T (fromStrict, pack, Text, toStrict)
import Web.Wheb (getHandlerState, getWithApp, modifyHandlerState', 
                 WhebError(Error403), WhebHandlerT, WhebMiddleware, WhebT)
import Web.Wheb.Plugins.Session (deleteSessionValue, getSessionValue', SessionApp, setSessionValue)
    
register :: (AuthApp a, MonadIO m) => AuthUser -> Password -> WhebT a b m (Either AuthError AuthUser)
register un pw = runWithContainer $ backendRegister un pw
login :: (AuthApp a, AuthState b, MonadIO m) => UserKey -> Password -> WhebT a b m (Either AuthError AuthUser)
login un pw = do
  loginResult <- (runWithContainer $ backendLogin un pw)
  case loginResult of
      Right au@(AuthUser userKey) -> do
          sessionKey <- getUserSessionKey
          deleteSessionValue sessionKey
          setSessionValue sessionKey userKey
          authSetUser (Just au)
      _ -> return ()
  return loginResult
logout :: (AuthApp a, AuthState b, MonadIO m) => WhebT a b m ()
logout = (runWithContainer backendLogout) >> (authSetUser Nothing)
getCurrentUser :: (AuthState b, MonadIO m) => WhebT a b m (Maybe AuthUser)
getCurrentUser = liftM getAuthUser getHandlerState
queryCurrentUser :: (AuthApp a, MonadIO m) => WhebT a b m (Maybe AuthUser)
queryCurrentUser = getUserSessionKey >>= 
                 getSessionValue' (T.pack "") >>=
                 (\uid -> runWithContainer $ backendGetUser uid)
loginRequired :: (AuthState b, MonadIO m) =>
                 WhebHandlerT a b m ->
                 WhebHandlerT a b m
loginRequired action = getCurrentUser >>=
                       (maybe (throwError Error403) (const action))
authMiddleware :: (AuthApp a, AuthState b, MonadIO m) => WhebMiddleware a b m
authMiddleware = do
    cur <- queryCurrentUser
    authSetUser cur
    return Nothing
type UserKey = Text
type Password = Text
type PwHash = Text
data AuthError = DuplicateUsername | UserDoesNotExist | InvalidPassword
  deriving (Show)
data AuthUser = AuthUser { uniqueUserKey :: UserKey } deriving (Show)
type PossibleUser = Maybe AuthUser
data AuthContainer = forall r. AuthBackend r => AuthContainer r
class SessionApp a => AuthApp a where
  getAuthContainer :: a -> AuthContainer
class AuthState a where
  getAuthUser    :: a -> PossibleUser 
  modifyAuthUser :: (PossibleUser -> PossibleUser) -> a -> a
class AuthBackend c where
  backendLogin    :: (AuthApp a, MonadIO m) => SessionApp a => UserKey -> Password -> c -> WhebT a b m (Either AuthError AuthUser)
  backendRegister :: (AuthApp a, MonadIO m) => AuthUser -> Password -> c -> WhebT a b m (Either AuthError AuthUser)
  backendGetUser  :: (AuthApp a, MonadIO m) => UserKey -> c -> WhebT a b m (Maybe AuthUser)
  backendLogout   :: (AuthApp a, MonadIO m) => c -> WhebT a b m ()
  backendLogout _ =  getUserSessionKey >>= deleteSessionValue
  
runWithContainer :: (AuthApp a, MonadIO m) =>
                    (forall r. AuthBackend r => r -> WhebT a s m b) -> 
                    WhebT a s m b
runWithContainer f = do
  AuthContainer authStore <- getWithApp getAuthContainer
  f authStore
authSetUser :: (AuthApp a, AuthState b, MonadIO m) => PossibleUser -> WhebT a b m ()
authSetUser cur = modifyHandlerState' (modifyAuthUser (const cur))
getUserSessionKey :: (AuthApp a, MonadIO m) => WhebT a b m Text
getUserSessionKey = return $ T.pack "user-id" 
makePwHash :: MonadIO m => Password -> WhebT a b m PwHash
makePwHash pw = liftM (T.fromStrict . ES.decodeUtf8) $ 
                        liftIO $ makePassword (ES.encodeUtf8 $ T.toStrict pw) 14
verifyPw :: Text -> Text -> Bool
verifyPw pw hash = verifyPassword (ES.encodeUtf8 $ T.toStrict pw) 
                          (ES.encodeUtf8 $ T.toStrict hash)