{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}

module Model.Tokens.Upload
  ( UploadNonce, newUploadNonce
  , UploadToken(..)
  , _uploadTokenIdentifier, _uploadTokenNonce, _uploadTokenIssuedBy, _uploadTokenIssuedAt, _uploadTokenExpiresAt, _uploadTokenStartsAt, _uploadTokenConfig
  , UploadTokenState(..)
  , _uploadTokenStateHashState, _uploadTokenStateManifest
  , UploadTokenStateHashState(..)
  , _utsHashStateNonce, _utsHashStateState
  ) where

import ClassyPrelude.Yesod

import Model.Tokens.Lens
import Model
import Utils.Lens

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

import Data.Time.Clock.POSIX

import Control.Monad.Fail

import qualified Data.Aeson as JSON
import Data.Aeson.TH
import Utils.PathPiece

import qualified Data.HashMap.Strict as HashMap

import qualified Crypto.Saltine.Core.SecretBox as SecretBox
import qualified Data.ByteString.Base64.URL as Base64
import qualified Crypto.Saltine.Class as Saltine
import qualified Crypto.Saltine.Internal.SecretBox as Saltine

import qualified Data.ByteString as BS


data UploadToken = UploadToken
  { uploadTokenIdentifier :: TokenId
  , uploadTokenNonce      :: UploadNonce
  , uploadTokenIssuedAt   :: UTCTime
  , uploadTokenIssuedBy   :: InstanceId
  , uploadTokenIssuedFor  :: ClusterId
  , uploadTokenExpiresAt
  , uploadTokenStartsAt   :: Maybe UTCTime
  , uploadTokenConfig     :: FileField FileReference
  , uploadTokenState      :: Maybe UploadTokenState
  } deriving (Eq, Ord, Show, Generic, Typeable)

data UploadTokenState = UploadTokenState
  { uploadTokenStateHashState :: UploadTokenStateHashState
  , uploadTokenStateManifest  :: Seq FileContentChunkReference
  } deriving (Eq, Ord, Show, Generic, Typeable)

data UploadTokenStateHashState = UploadTokenStateHashState
  { utsHashStateNonce :: SecretBox.Nonce
  , utsHashStateState :: ByteString
  } deriving (Eq, Ord, Show, Generic, Typeable)


makeLenses_ ''UploadToken
instance HasTokenIdentifier UploadToken TokenId where
  _tokenIdentifier = _uploadTokenIdentifier
instance HasTokenIssuedBy UploadToken InstanceId where
  _tokenIssuedBy = _uploadTokenIssuedBy
instance HasTokenIssuedFor UploadToken ClusterId where
  _tokenIssuedFor = _uploadTokenIssuedFor
instance HasTokenIssuedAt UploadToken UTCTime where
  _tokenIssuedAt = _uploadTokenIssuedAt
instance HasTokenExpiresAt UploadToken (Maybe UTCTime) where
  _tokenExpiresAt = _uploadTokenExpiresAt
instance HasTokenStartsAt UploadToken (Maybe UTCTime) where
  _tokenStartsAt = _uploadTokenStartsAt
makeLenses_ ''UploadTokenState
makeLenses_ ''UploadTokenStateHashState


deriveJSON defaultOptions
  { fieldLabelModifier = camelToPathPiece' 3
  } ''UploadTokenState

instance ToJSON UploadTokenStateHashState where
  toJSON UploadTokenStateHashState{..} = JSON.String . decodeUtf8 . Base64.encode $ Saltine.encode utsHashStateNonce <> utsHashStateState

instance FromJSON UploadTokenStateHashState where
  parseJSON = JSON.withText "UploadTokenStateHashState" $ \t -> do
    decoded <- either (const $ fail "Invalid base64") return . Base64.decode $ encodeUtf8 t
    unless (BS.length decoded >= Saltine.secretbox_noncebytes + Saltine.secretbox_macbytes) $
      fail "Too short"
    let (nonceBS, utsHashStateState) = BS.splitAt Saltine.secretbox_noncebytes decoded
    utsHashStateNonce <- maybe (fail "Invalid nonce") return $ Saltine.decode nonceBS
    return UploadTokenStateHashState{..}

instance ToJSON UploadToken where
  toJSON UploadToken{..} = JSON.object . catMaybes $
      [ pure $ "config" JSON..= uploadTokenConfig
      , fmap ("state" JSON..=) uploadTokenState
      ] ++ let JSON.Object hm = toJSON Jose.JwtClaims{..} in (pure <$> HashMap.toList hm)
    where jwtIss = Just $ toPathPiece uploadTokenIssuedBy
          jwtSub = Just $ toPathPiece uploadTokenNonce
          jwtAud = Just . pure $ toPathPiece uploadTokenIssuedFor
          jwtExp = IntDate . utcTimeToPOSIXSeconds <$> uploadTokenExpiresAt
          jwtNbf = IntDate . utcTimeToPOSIXSeconds <$> uploadTokenStartsAt
          jwtIat = Just . IntDate $ utcTimeToPOSIXSeconds uploadTokenIssuedAt
          jwtJti = Just $ toPathPiece uploadTokenIdentifier

instance FromJSON UploadToken where
  parseJSON val = flip (JSON.withObject "UploadToken") val $ \o -> do
    Jose.JwtClaims{..} <- parseJSON val

    uploadTokenIdentifier <- parseMaybe "uploadTokenIdentfier" $
      fromPathPiece =<< jwtJti
    uploadTokenNonce <- parseMaybe "uploadTokenNonce" $
      fromPathPiece =<< jwtSub
    uploadTokenIssuedAt <- parseMaybe "uploadTokenIssuedAt" $
      unIntDate <$> jwtIat
    uploadTokenIssuedBy <- parseMaybe "uploadTokenIssuedBy" $
      fromPathPiece =<< jwtIss
    uploadTokenIssuedFor <- parseMaybe "uploadTokenIssuedFor" $ do
      [aud] <- jwtAud
      fromPathPiece aud
    let uploadTokenExpiresAt = unIntDate <$> jwtExp
        uploadTokenStartsAt = unIntDate <$> jwtNbf

    uploadTokenConfig <- o JSON..: "config"
    uploadTokenState <- o JSON..:? "state"

    return UploadToken{..}
    where
      parseMaybe errId = maybe (fail $ "Could not parse " <> errId) return
      unIntDate (IntDate posix) = posixSecondsToUTCTime posix
