module Snap.Snaplet.Auth.Types where
import           Control.Arrow
import           Control.Monad.Trans
import           Crypto.PasswordStore
import           Data.Aeson
import           Data.ByteString       (ByteString)
import qualified Data.Configurator as C
import           Data.HashMap.Strict   (HashMap)
import qualified Data.HashMap.Strict   as HM
import           Data.Hashable         (Hashable)
import           Data.Time
import           Data.Text             (Text)
import           Data.Text.Encoding    (decodeUtf8, encodeUtf8)
import           Data.Typeable
import           Snap.Snaplet
#if !MIN_VERSION_base(4,8,0)
import           Control.Applicative
#endif
data Password = ClearText ByteString
              | Encrypted ByteString
  deriving (Read, Show, Ord, Eq)
defaultStrength :: Int
defaultStrength = 12
encrypt :: ByteString -> IO ByteString
encrypt = flip makePassword defaultStrength
verify 
    :: ByteString               
    -> ByteString               
    -> Bool
verify = verifyPassword 
encryptPassword :: Password -> IO Password
encryptPassword p@(Encrypted {}) = return p
encryptPassword (ClearText p)    = Encrypted `fmap` encrypt p 
checkPassword :: Password -> Password -> Bool
checkPassword (ClearText pw) (Encrypted pw') = verify pw pw'
checkPassword (ClearText pw) (ClearText pw') = pw == pw'
checkPassword (Encrypted pw) (Encrypted pw') = pw == pw'
checkPassword _ _ =
  error "checkPassword failed. Make sure you pass ClearText passwords"
data AuthFailure = AuthError String
                 | BackendError
                 | DuplicateLogin
                 | EncryptedPassword
                 | IncorrectPassword
                 | LockedOut UTCTime    
                 | PasswordMissing
                 | UsernameMissing
                 | UserNotFound
  deriving (Read, Ord, Eq, Typeable)
instance Show AuthFailure where
        show (AuthError s) = s
        show (BackendError) = "Failed to store data in the backend."
        show (DuplicateLogin) = "This login already exists in the backend."
        show (EncryptedPassword) = "Cannot login with encrypted password."
        show (IncorrectPassword) = "The password provided was not valid."
        show (LockedOut time) = "The login is locked out until " ++ show time
        show (PasswordMissing) = "No password provided."
        show (UsernameMissing) = "No username provided."
        show (UserNotFound) = "User not found in the backend."
newtype UserId = UserId { unUid :: Text }
  deriving ( Read, Show, Ord, Eq, FromJSON, ToJSON, Hashable )
#if MIN_VERSION_aeson(1,0,0)
deriving instance FromJSONKey UserId
deriving instance ToJSONKey UserId
#endif
data Role = Role ByteString
  deriving (Read, Show, Ord, Eq)
data AuthUser = AuthUser
    { userId               :: Maybe UserId
    , userLogin            :: Text
    
    
    , userEmail            :: Maybe Text
    , userPassword         :: Maybe Password
    , userActivatedAt      :: Maybe UTCTime
    , userSuspendedAt      :: Maybe UTCTime
    , userRememberToken    :: Maybe Text
    , userLoginCount       :: Int
    , userFailedLoginCount :: Int
    , userLockedOutUntil   :: Maybe UTCTime
    , userCurrentLoginAt   :: Maybe UTCTime
    , userLastLoginAt      :: Maybe UTCTime
    , userCurrentLoginIp   :: Maybe ByteString
    , userLastLoginIp      :: Maybe ByteString
    , userCreatedAt        :: Maybe UTCTime
    , userUpdatedAt        :: Maybe UTCTime
    , userResetToken       :: Maybe Text
    , userResetRequestedAt :: Maybe UTCTime
    , userRoles            :: [Role]
    , userMeta             :: HashMap Text Value
    }
  deriving (Show,Eq)
defAuthUser :: AuthUser
defAuthUser = AuthUser
    { userId               = Nothing
    , userLogin            = ""
    , userEmail            = Nothing
    , userPassword         = Nothing
    , userActivatedAt      = Nothing
    , userSuspendedAt      = Nothing
    , userRememberToken    = Nothing
    , userLoginCount       = 0
    , userFailedLoginCount = 0
    , userLockedOutUntil   = Nothing
    , userCurrentLoginAt   = Nothing
    , userLastLoginAt      = Nothing
    , userCurrentLoginIp   = Nothing
    , userLastLoginIp      = Nothing
    , userCreatedAt        = Nothing
    , userUpdatedAt        = Nothing
    , userResetToken       = Nothing
    , userResetRequestedAt = Nothing
    , userRoles            = []
    , userMeta             = HM.empty
    }
setPassword :: AuthUser -> ByteString -> IO AuthUser
setPassword au pass = do
    pw <- Encrypted <$> makePassword pass defaultStrength
    return $! au { userPassword = Just pw }
data AuthSettings = AuthSettings {
    asMinPasswdLen       :: Int
      
  , asRememberCookieName :: ByteString
      
  , asRememberPeriod     :: Maybe Int
      
    
  , asLockout            :: Maybe (Int, NominalDiffTime)
      
  , asSiteKey            :: FilePath
      
}
defAuthSettings :: AuthSettings
defAuthSettings = AuthSettings {
    asMinPasswdLen       = 8
  , asRememberCookieName = "_remember"
  , asRememberPeriod     = Just (2*7*24*60*60)
  , asLockout            = Nothing
  , asSiteKey            = "site_key.txt"
}
authSettingsFromConfig :: Initializer b v AuthSettings
authSettingsFromConfig = do
    config <- getSnapletUserConfig
    minPasswordLen <- liftIO $ C.lookup config "minPasswordLen"
    let pw = maybe id (\x s -> s { asMinPasswdLen = x }) minPasswordLen
    rememberCookie <- liftIO $ C.lookup config "rememberCookie"
    let rc = maybe id (\x s -> s { asRememberCookieName = x }) rememberCookie
    rememberPeriod <- liftIO $ C.lookup config "rememberPeriod"
    let rp = maybe id (\x s -> s { asRememberPeriod = Just x }) rememberPeriod
    lockout <- liftIO $ C.lookup config "lockout"
    let lo = maybe id (\x s -> s { asLockout = Just (second fromInteger x) })
                   lockout
    siteKey <- liftIO $ C.lookup config "siteKey"
    let sk = maybe id (\x s -> s { asSiteKey = x }) siteKey
    return $ (pw . rc . rp . lo . sk) defAuthSettings
                             
                             
                             
instance ToJSON AuthUser where
  toJSON u = object
    [ "uid"                .= userId                u
    , "login"              .= userLogin             u
    , "email"              .= userEmail             u
    , "pw"                 .= userPassword          u
    , "activated_at"       .= userActivatedAt       u
    , "suspended_at"       .= userSuspendedAt       u
    , "remember_token"     .= userRememberToken     u
    , "login_count"        .= userLoginCount        u
    , "failed_login_count" .= userFailedLoginCount  u
    , "locked_until"       .= userLockedOutUntil    u
    , "current_login_at"   .= userCurrentLoginAt    u
    , "last_login_at"      .= userLastLoginAt       u
    , "current_ip"         .= fmap decodeUtf8 (userCurrentLoginIp u)
    , "last_ip"            .= fmap decodeUtf8 (userLastLoginIp u)
    , "created_at"         .= userCreatedAt         u
    , "updated_at"         .= userUpdatedAt         u
    , "reset_token"        .= userResetToken        u
    , "reset_requested_at" .= userResetRequestedAt  u
    , "roles"              .= userRoles             u
    , "meta"               .= userMeta              u
    ]
instance FromJSON AuthUser where
  parseJSON (Object v) = AuthUser
    <$> v .: "uid"
    <*> v .: "login"
    <*> v .: "email"
    <*> v .: "pw"
    <*> v .: "activated_at"
    <*> v .: "suspended_at"
    <*> v .: "remember_token"
    <*> v .: "login_count"
    <*> v .: "failed_login_count"
    <*> v .: "locked_until"
    <*> v .: "current_login_at"
    <*> v .: "last_login_at"
    <*> fmap (fmap encodeUtf8) (v .: "current_ip")
    <*> fmap (fmap encodeUtf8) (v .: "last_ip")
    <*> v .: "created_at"
    <*> v .: "updated_at"
    <*> v .: "reset_token"
    <*> v .: "reset_requested_at"
    <*> v .:? "roles" .!= []
    <*> v .: "meta"
  parseJSON _ = error "Unexpected JSON input"
instance ToJSON Password where
  toJSON (Encrypted x) = toJSON $ decodeUtf8 x
  toJSON (ClearText _) =
      error "ClearText passwords can't be serialized into JSON"
instance FromJSON Password where
  parseJSON = fmap (Encrypted . encodeUtf8) . parseJSON
instance ToJSON Role where
  toJSON (Role x) = toJSON $ decodeUtf8 x
instance FromJSON Role where
  parseJSON = fmap (Role . encodeUtf8) . parseJSON