{-# LANGUAGE UndecidableInstances #-}

module Model.Tokens
  ( BearerToken(..)
  , _tokenIdentifier, _tokenAuthority, _tokenRoutes, _tokenAddAuth, _tokenRestrictions, _tokenRestrictionIx, _tokenRestrictionAt, _tokenIssuedAt, _tokenIssuedBy, _tokenExpiresAt, _tokenStartsAt
  , tokenRestrict
  , tokenToJSON, tokenParseJSON
  ) where

import ClassyPrelude.Yesod
import Yesod.Core.Instances ()
  
import Model
import Utils (assertM')
import Utils.Lens hiding ((.=))
import Data.Aeson.Lens (AsJSON(..))

import Yesod.Auth (AuthId)

import Jose.Jwt (IntDate(..))
import qualified Jose.Jwt as Jose

import Jose.Jwt.Instances ()
import Data.Aeson.Types.Instances ()

import Data.HashSet (HashSet)

import qualified Data.HashMap.Strict as HashMap
import Data.HashMap.Strict.Instances ()
import Data.HashSet.Instances ()
import Data.Time.Clock.Instances ()

import Data.Aeson.Types (Parser, (.:?), (.:), (.!=), (.=))
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Types as JSON

import CryptoID

import Data.Time.Clock.POSIX

import Data.Binary (Binary)



-- | Presenting a `BearerToken` transfers some authorisation from `tokenAuthority` to /whoever/ presents the token
data BearerToken site = BearerToken
  { tokenIdentifier   :: TokenId
    -- ^ Unique identifier for each token; maybe useful for tracing usage of tokens
  , tokenAuthority    :: AuthId site
    -- ^ Tokens only grant rights the `tokenAuthority` has (i.e. `AuthTag`s are evaluated with the user set to `tokenAuthority`)
  , tokenRoutes       :: Maybe (HashSet (Route site))
    -- ^ Tokens can optionally be restricted to only be usable on a subset of routes
  , tokenAddAuth      :: Maybe AuthDNF
    -- ^ Tokens can specify an additional predicate logic formula of `AuthTag`s that needs to evaluate to `Authorized` in order for the token to be valid.
  , tokenRestrictions :: HashMap (Route site) Value
    -- ^ Tokens can be restricted to certain actions within the context of a route (i.e. creating an user only with a certain username, resetting a password only if the old matches a hash, ...)
    --
    -- In general this is not encrypted; some care is required to not expose sensitive information to the bearer of the token
  , tokenIssuedAt     :: UTCTime
  , tokenIssuedBy     :: InstanceId
  , tokenExpiresAt
  , tokenStartsAt     :: Maybe UTCTime
  } deriving (Generic, Typeable)

deriving instance (Eq (AuthId site), Eq (Route site)) => Eq (BearerToken site)
deriving instance (Read (AuthId site), Eq (Route site), Hashable (Route site), Read (Route site)) => Read (BearerToken site)
deriving instance (Show (AuthId site), Show (Route site)) => Show (BearerToken site)

instance (Binary (AuthId site), Binary (Route site), Hashable (Route site), Eq (Route site)) => Binary (BearerToken site)

makeLenses_ ''BearerToken

_tokenRestrictionIx :: (FromJSON a, ToJSON a, Hashable (Route site), Eq (Route site)) => Route site -> Traversal' (BearerToken site) a
-- ^ Focus a singular restriction (by route) if it exists
--
-- This /cannot/ be used to add restrictions, use `_tokenRestrictionAt` or `tokenRestrict` instead
_tokenRestrictionIx route = _tokenRestrictions . ix route . _JSON

_tokenRestrictionAt :: (FromJSON a, ToJSON a, Hashable (Route site), Eq (Route site)) => Route site -> Traversal' (BearerToken site) (Maybe a)
-- ^ Focus a singular restriction (by route) whether it exists, or not
_tokenRestrictionAt route = _tokenRestrictions . at route . maybePrism _JSON

tokenRestrict :: (ToJSON a, Hashable (Route site), Eq (Route site)) => Route site -> a -> BearerToken site -> BearerToken site
-- ^ Add a restriction to a `BearerToken`
--
-- If a restriction already exists for the targeted route, it's silently overwritten
tokenRestrict route (toJSON -> resVal) = over _tokenRestrictions $ HashMap.insert route resVal



tokenToJSON :: forall m.
               ( MonadHandler m
               , HasCryptoUUID (AuthId (HandlerSite m)) m
               , RenderRoute (HandlerSite m)
               ) => BearerToken (HandlerSite m) -> m Value
-- ^ Encode a `BearerToken` analogously to `toJSON`
--
-- Monadic context is needed because `AuthId`s are encrypted during encoding
tokenToJSON BearerToken{..} = do
  cID <- encrypt tokenAuthority :: m (CryptoUUID (AuthId (HandlerSite m)))
  let stdPayload = Jose.JwtClaims
        { jwtIss = Just $ toPathPiece tokenIssuedBy
        , jwtSub = Nothing
        , jwtAud = Nothing
        , jwtExp = IntDate . utcTimeToPOSIXSeconds <$> tokenExpiresAt
        , jwtNbf = IntDate . utcTimeToPOSIXSeconds <$> tokenStartsAt
        , jwtIat = Just . IntDate $ utcTimeToPOSIXSeconds tokenIssuedAt
        , jwtJti = Just $ toPathPiece tokenIdentifier
        }
  return . JSON.object $
    catMaybes [ Just $ "authority" .= cID
              , ("routes" .=) <$> tokenRoutes
              , ("add-auth" .=) <$> tokenAddAuth
              , ("restrictions" .=) <$> assertM' (not . HashMap.null) tokenRestrictions
              ]
    ++ let JSON.Object hm = toJSON stdPayload in HashMap.toList hm

tokenParseJSON :: forall site.
                  ( HasCryptoUUID (AuthId site) (ReaderT CryptoIDKey Parser)
                  , ParseRoute site
                  , Hashable (Route site)
                  )
               => Value
               -> ReaderT CryptoIDKey Parser (BearerToken site)
-- ^ Decode a `Value` to a `BearerToken` analogously to `parseJSON`
--
-- Monadic context is needed because `AuthId`s are encrypted during encoding
--
-- It's usually easier to use `Utils.Tokens.tokenParseJSON'`
tokenParseJSON v@(Object o) = do
  tokenAuthority' <- lift (o .: "authority") :: ReaderT CryptoIDKey Parser (CryptoUUID (AuthId site))
  tokenAuthority <- decrypt tokenAuthority'

  tokenRoutes        <- lift $ o .:? "routes"
  tokenAddAuth       <- lift $ o .:? "add-auth"
  tokenRestrictions  <- lift $ o .:? "restrictions" .!= HashMap.empty
  Jose.JwtClaims{..} <- lift $ parseJSON v

  let unIntDate (IntDate posix) = posixSecondsToUTCTime posix

  Just tokenIssuedBy <- return $ jwtIss >>= fromPathPiece
  Just tokenIdentifier <- return $ jwtJti >>= fromPathPiece
  Just tokenIssuedAt <- return $ unIntDate <$> jwtIat
  let tokenExpiresAt = unIntDate <$> jwtExp
      tokenStartsAt = unIntDate <$> jwtNbf

  return BearerToken{..}
tokenParseJSON v = lift $ JSON.typeMismatch "BearerToken" v

