-- | Hackage-specific wrappers around the Util.JSON module
{-# LANGUAGE CPP #-}
module Hackage.Security.JSON (
    -- * Deserialization errors
    DeserializationError(..)
  , validate
  , verifyType
    -- * MonadKeys
  , MonadKeys(..)
  , addKeys
  , withKeys
  , lookupKey
  , readKeyAsId
    -- * Reader monads
  , ReadJSON_Keys_Layout
  , ReadJSON_Keys_NoLayout
  , ReadJSON_NoKeys_NoLayout
  , runReadJSON_Keys_Layout
  , runReadJSON_Keys_NoLayout
  , runReadJSON_NoKeys_NoLayout
    -- ** Utility
  , parseJSON_Keys_Layout
  , parseJSON_Keys_NoLayout
  , parseJSON_NoKeys_NoLayout
  , readJSON_Keys_Layout
  , readJSON_Keys_NoLayout
  , readJSON_NoKeys_NoLayout
    -- * Writing
  , WriteJSON
  , runWriteJSON
    -- ** Utility
  , renderJSON
  , renderJSON_NoLayout
  , writeJSON
  , writeJSON_NoLayout
  , writeKeyAsId
    -- * Re-exports
  , module Hackage.Security.Util.JSON
  ) where

import Prelude
import Control.Arrow (first, second)
import Control.Exception
import Control.Monad (unless, liftM)
import Control.Monad.Except (MonadError, Except, ExceptT, runExcept, runExceptT, throwError)
import Control.Monad.Reader (MonadReader, Reader, runReader, local, ask)
import Data.Functor.Identity
import qualified Data.ByteString.Lazy as BS.L

import Hackage.Security.Key
import Hackage.Security.Key.Env (KeyEnv)
import Hackage.Security.TUF.Layout.Repo
import Hackage.Security.Util.JSON
import Hackage.Security.Util.Path
import Hackage.Security.Util.Pretty
import Hackage.Security.Util.Some
import Text.JSON.Canonical
import qualified Hackage.Security.Key.Env as KeyEnv

{-------------------------------------------------------------------------------
  Deserialization errors
-------------------------------------------------------------------------------}

data DeserializationError =
    -- | Malformed JSON has syntax errors in the JSON itself
    -- (i.e., we cannot even parse it to a JSValue)
    DeserializationErrorMalformed String

    -- | Invalid JSON has valid syntax but invalid structure
    --
    -- The string gives a hint about what we expected instead
  | DeserializationErrorSchema String

    -- | The JSON file contains a key ID of an unknown key
  | DeserializationErrorUnknownKey KeyId

    -- | Some verification step failed
  | DeserializationErrorValidation String

    -- | Wrong file type
    --
    -- Records actual and expected types.
  | DeserializationErrorFileType String String

deriving instance Show DeserializationError
instance Exception DeserializationError where displayException = pretty

instance Pretty DeserializationError where
  pretty (DeserializationErrorMalformed str) =
      "Malformed: " ++ str
  pretty (DeserializationErrorSchema str) =
      "Schema error: " ++ str
  pretty (DeserializationErrorUnknownKey kId) =
      "Unknown key: " ++ keyIdString kId
  pretty (DeserializationErrorValidation str) =
      "Invalid: " ++ str
  pretty (DeserializationErrorFileType actualType expectedType) =
         "Expected file of type " ++ show expectedType
      ++ " but got file of type " ++ show actualType

validate :: MonadError DeserializationError m => String -> Bool -> m ()
validate _   True  = return ()
validate msg False = throwError $ DeserializationErrorValidation msg

verifyType :: (ReportSchemaErrors m, MonadError DeserializationError m)
           => JSValue -> String -> m ()
verifyType enc expectedType = do
    actualType <- fromJSField enc "_type"
    unless (actualType == expectedType) $
      throwError $ DeserializationErrorFileType actualType expectedType

{-------------------------------------------------------------------------------
  Access to keys
-------------------------------------------------------------------------------}

-- | MonadReader-like monad, specialized to key environments
class (ReportSchemaErrors m, MonadError DeserializationError m) => MonadKeys m where
  localKeys :: (KeyEnv -> KeyEnv) -> m a -> m a
  askKeys   :: m KeyEnv

readKeyAsId :: MonadKeys m => JSValue -> m (Some PublicKey)
readKeyAsId (JSString kId) = lookupKey (KeyId kId)
readKeyAsId val            = expected' "key ID" val

addKeys :: MonadKeys m => KeyEnv -> m a -> m a
addKeys keys = localKeys (KeyEnv.union keys)

withKeys :: MonadKeys m => KeyEnv -> m a -> m a
withKeys keys = localKeys (const keys)

lookupKey :: MonadKeys m => KeyId -> m (Some PublicKey)
lookupKey kId = do
    keyEnv <- askKeys
    case KeyEnv.lookup kId keyEnv of
      Just key -> return key
      Nothing  -> throwError $ DeserializationErrorUnknownKey kId

{-------------------------------------------------------------------------------
  Reading
-------------------------------------------------------------------------------}

newtype ReadJSON_Keys_Layout a = ReadJSON_Keys_Layout {
    unReadJSON_Keys_Layout :: ExceptT DeserializationError (Reader (RepoLayout, KeyEnv)) a
  }
  deriving ( Functor
           , Applicative
           , Monad
           , MonadError DeserializationError
           )

newtype ReadJSON_Keys_NoLayout a = ReadJSON_Keys_NoLayout {
    unReadJSON_Keys_NoLayout :: ExceptT DeserializationError (Reader KeyEnv) a
  }
  deriving ( Functor
           , Applicative
           , Monad
           , MonadError DeserializationError
           )

newtype ReadJSON_NoKeys_NoLayout a = ReadJSON_NoKeys_NoLayout {
    unReadJSON_NoKeys_NoLayout :: Except DeserializationError a
  }
  deriving ( Functor
           , Applicative
           , Monad
           , MonadError DeserializationError
           )

instance ReportSchemaErrors ReadJSON_Keys_Layout where
  expected str mgot = throwError $ expectedError str mgot
instance ReportSchemaErrors ReadJSON_Keys_NoLayout where
  expected str mgot = throwError $ expectedError str mgot
instance ReportSchemaErrors ReadJSON_NoKeys_NoLayout where
  expected str mgot = throwError $ expectedError str mgot

expectedError :: Expected -> Maybe Got -> DeserializationError
expectedError str mgot = DeserializationErrorSchema msg
  where
    msg = case mgot of
            Nothing  -> "Expected " ++ str
            Just got -> "Expected " ++ str ++ " but got " ++ got

instance MonadReader RepoLayout ReadJSON_Keys_Layout where
  ask         = ReadJSON_Keys_Layout $ fst `liftM` ask
  local f act = ReadJSON_Keys_Layout $ local (first f) act'
    where
      act' = unReadJSON_Keys_Layout act

instance MonadKeys ReadJSON_Keys_Layout where
  askKeys         = ReadJSON_Keys_Layout $ snd `liftM` ask
  localKeys f act = ReadJSON_Keys_Layout $ local (second f) act'
    where
      act' = unReadJSON_Keys_Layout act

instance MonadKeys ReadJSON_Keys_NoLayout where
  askKeys         = ReadJSON_Keys_NoLayout $ ask
  localKeys f act = ReadJSON_Keys_NoLayout $ local f act'
    where
      act' = unReadJSON_Keys_NoLayout act

runReadJSON_Keys_Layout :: KeyEnv
                        -> RepoLayout
                        -> ReadJSON_Keys_Layout a
                        -> Either DeserializationError a
runReadJSON_Keys_Layout keyEnv repoLayout act =
    runReader (runExceptT (unReadJSON_Keys_Layout act)) (repoLayout, keyEnv)

runReadJSON_Keys_NoLayout :: KeyEnv
                          -> ReadJSON_Keys_NoLayout a
                          -> Either DeserializationError a
runReadJSON_Keys_NoLayout keyEnv act =
    runReader (runExceptT (unReadJSON_Keys_NoLayout act)) keyEnv

runReadJSON_NoKeys_NoLayout :: ReadJSON_NoKeys_NoLayout a
                            -> Either DeserializationError a
runReadJSON_NoKeys_NoLayout act =
    runExcept (unReadJSON_NoKeys_NoLayout act)

{-------------------------------------------------------------------------------
  Utility
-------------------------------------------------------------------------------}

parseJSON_Keys_Layout :: FromJSON ReadJSON_Keys_Layout a
                      => KeyEnv
                      -> RepoLayout
                      -> BS.L.ByteString
                      -> Either DeserializationError a
parseJSON_Keys_Layout keyEnv repoLayout bs =
    case parseCanonicalJSON bs of
      Left  err -> Left (DeserializationErrorMalformed err)
      Right val -> runReadJSON_Keys_Layout keyEnv repoLayout (fromJSON val)

parseJSON_Keys_NoLayout :: FromJSON ReadJSON_Keys_NoLayout a
                        => KeyEnv
                        -> BS.L.ByteString
                        -> Either DeserializationError a
parseJSON_Keys_NoLayout keyEnv bs =
    case parseCanonicalJSON bs of
      Left  err -> Left (DeserializationErrorMalformed err)
      Right val -> runReadJSON_Keys_NoLayout keyEnv (fromJSON val)

parseJSON_NoKeys_NoLayout :: FromJSON ReadJSON_NoKeys_NoLayout a
                          => BS.L.ByteString
                          -> Either DeserializationError a
parseJSON_NoKeys_NoLayout bs =
    case parseCanonicalJSON bs of
      Left  err -> Left (DeserializationErrorMalformed err)
      Right val -> runReadJSON_NoKeys_NoLayout (fromJSON val)

readJSON_Keys_Layout :: ( FsRoot root
                        , FromJSON ReadJSON_Keys_Layout a
                        )
                     => KeyEnv
                     -> RepoLayout
                     -> Path root
                     -> IO (Either DeserializationError a)
readJSON_Keys_Layout keyEnv repoLayout fp = do
    withFile fp ReadMode $ \h -> do
      bs <- BS.L.hGetContents h
      evaluate $ parseJSON_Keys_Layout keyEnv repoLayout bs

readJSON_Keys_NoLayout :: ( FsRoot root
                          , FromJSON ReadJSON_Keys_NoLayout a
                          )
                       => KeyEnv
                       -> Path root
                       -> IO (Either DeserializationError a)
readJSON_Keys_NoLayout keyEnv fp = do
    withFile fp ReadMode $ \h -> do
      bs <- BS.L.hGetContents h
      evaluate $ parseJSON_Keys_NoLayout keyEnv bs

readJSON_NoKeys_NoLayout :: ( FsRoot root
                            , FromJSON ReadJSON_NoKeys_NoLayout a
                            )
                         => Path root
                         -> IO (Either DeserializationError a)
readJSON_NoKeys_NoLayout fp = do
    withFile fp ReadMode $ \h -> do
      bs <- BS.L.hGetContents h
      evaluate $ parseJSON_NoKeys_NoLayout bs

{-------------------------------------------------------------------------------
  Writing
-------------------------------------------------------------------------------}

newtype WriteJSON a = WriteJSON {
    unWriteJSON :: Reader RepoLayout a
  }
  deriving ( Functor
           , Applicative
           , Monad
           , MonadReader RepoLayout
           )

runWriteJSON :: RepoLayout -> WriteJSON a -> a
runWriteJSON repoLayout act = runReader (unWriteJSON act) repoLayout

{-------------------------------------------------------------------------------
  Writing: Utility
-------------------------------------------------------------------------------}

-- | Render to canonical JSON format
renderJSON :: ToJSON WriteJSON a => RepoLayout -> a -> BS.L.ByteString
renderJSON repoLayout = renderCanonicalJSON . runWriteJSON repoLayout . toJSON

-- | Variation on 'renderJSON' for files that don't require the repo layout
renderJSON_NoLayout :: ToJSON Identity a => a -> BS.L.ByteString
renderJSON_NoLayout = renderCanonicalJSON . runIdentity . toJSON

writeJSON :: ToJSON WriteJSON a => RepoLayout -> Path Absolute -> a -> IO ()
writeJSON repoLayout fp = writeLazyByteString fp . renderJSON repoLayout

writeJSON_NoLayout :: ToJSON Identity a => Path Absolute -> a -> IO ()
writeJSON_NoLayout fp = writeLazyByteString fp . renderJSON_NoLayout

writeKeyAsId :: Some PublicKey -> JSValue
writeKeyAsId = JSString . keyIdString . someKeyId
