{-# OPTIONS_GHC -fno-warn-orphans #-}

module Settings.Cluster
  ( ClusterSettingsKey(..)
  , ClusterSetting(..)
  ) where

import ClassyPrelude.Yesod
import Web.HttpApiData

import Data.Kind (Type)

import Utils
import Data.Universe

import qualified Data.Aeson as Aeson

import qualified Crypto.Saltine.Core.SecretBox as SecretBox
import qualified Crypto.Saltine.Core.AEAD as AEAD
import qualified Crypto.Saltine.Class as Saltine

import Data.CryptoID.ByteString (CryptoIDKey)
import qualified Data.CryptoID.ByteString as CryptoID

import qualified Data.Binary as Binary
import qualified Data.ByteString.Base64.URL as Base64

import qualified Jose.Jwa as Jose
import qualified Jose.Jwk as Jose
import qualified Jose.Jwt as Jose

import Data.UUID (UUID)
import Control.Monad.Random.Class (MonadRandom(..))

import Control.Monad.Fail

import Model.Types.TH.PathPiece


data ClusterSettingsKey
  = ClusterCryptoIDKey
  | ClusterServerSessionKey
  | ClusterSecretBoxKey
  | ClusterJSONWebKeySet
  | ClusterId
  | ClusterMemcachedKey
  deriving (Eq, Ord, Enum, Bounded, Show, Read)

instance Universe ClusterSettingsKey
instance Finite ClusterSettingsKey

nullaryPathPiece ''ClusterSettingsKey $ camelToPathPiece' 1
pathPieceJSON ''ClusterSettingsKey
pathPieceJSONKey ''ClusterSettingsKey
derivePersistFieldPathPiece ''ClusterSettingsKey

instance ToHttpApiData ClusterSettingsKey where
  toUrlPiece = toPathPiece
instance FromHttpApiData ClusterSettingsKey where
  parseUrlPiece = maybe (Left "Could not parse url piece") Right . fromPathPiece


class (ToJSON (ClusterSettingValue key), FromJSON (ClusterSettingValue key)) => ClusterSetting (key :: ClusterSettingsKey) where
  type ClusterSettingValue key :: Type
  initClusterSetting :: forall m p. MonadIO m => p key -> m (ClusterSettingValue key)
  knownClusterSetting :: forall p. p key -> ClusterSettingsKey


instance ClusterSetting 'ClusterCryptoIDKey where
  type ClusterSettingValue 'ClusterCryptoIDKey = CryptoIDKey
  initClusterSetting _ = CryptoID.genKey
  knownClusterSetting _ = ClusterCryptoIDKey

instance ToJSON CryptoIDKey where
  toJSON = Aeson.String . decodeUtf8 . Base64.encode . toStrict . Binary.encode

instance FromJSON CryptoIDKey where
  parseJSON = Aeson.withText "CryptoIDKey" $ \t -> do
    bytes <- either fail (return . fromStrict) . Base64.decode $ encodeUtf8 t
    case Binary.decodeOrFail bytes of
      Left (_, _, err) -> fail err
      Right (bs, _, ret)
        | null bs -> return ret
        | otherwise -> fail $ show (length bs) ++ " extra bytes"


instance ClusterSetting 'ClusterServerSessionKey where
  type ClusterSettingValue 'ClusterServerSessionKey = AEAD.Key
  initClusterSetting _ = liftIO AEAD.newKey
  knownClusterSetting _ = ClusterServerSessionKey

instance ToJSON AEAD.Key where
  toJSON = Aeson.String . decodeUtf8 . Base64.encode . Saltine.encode

instance FromJSON AEAD.Key where
  parseJSON = Aeson.withText "Key" $ \t -> do
    bytes <- either fail return . Base64.decode $ encodeUtf8 t
    maybe (fail "Could not parse key") return $ Saltine.decode bytes


instance ClusterSetting 'ClusterSecretBoxKey where
  type ClusterSettingValue 'ClusterSecretBoxKey = SecretBox.Key
  initClusterSetting _ = liftIO SecretBox.newKey
  knownClusterSetting _ = ClusterSecretBoxKey

instance ToJSON SecretBox.Key where
  toJSON = Aeson.String . decodeUtf8 . Base64.encode . Saltine.encode

instance FromJSON SecretBox.Key where
  parseJSON = Aeson.withText "Key" $ \t -> do
    bytes <- either fail return . Base64.decode $ encodeUtf8 t
    maybe (fail "Could not parse key") return $ Saltine.decode bytes


instance ClusterSetting 'ClusterJSONWebKeySet where
  type ClusterSettingValue 'ClusterJSONWebKeySet = Jose.JwkSet
  initClusterSetting _ = liftIO $ do
    now <- getCurrentTime
    jwkSig <- Jose.generateSymmetricKey 32 (Jose.UTCKeyId now) Jose.Sig (Just $ Jose.Signed Jose.HS256)
    return $ Jose.JwkSet [jwkSig]
  knownClusterSetting _ = ClusterJSONWebKeySet


instance ClusterSetting 'ClusterId where
  type ClusterSettingValue 'ClusterId = UUID
  initClusterSetting _ = liftIO getRandom
  knownClusterSetting _ = ClusterId


instance ClusterSetting 'ClusterMemcachedKey where
  type ClusterSettingValue 'ClusterMemcachedKey = AEAD.Key
  initClusterSetting _ = liftIO AEAD.newKey
  knownClusterSetting _ = ClusterMemcachedKey
