-- |
-- Module:          ConfCrypt.Types
-- Copyright:       (c) 2018 Chris Coffey
--                  (c) 2018 CollegeVine
-- License:         MIT
-- Maintainer:      Chris Coffey
-- Stability:       experimental
-- Portability:     portable
--
-- Core types and some small helper functions used to construct ConfCrypt.
module ConfCrypt.Types (
    -- * Core types
    ConfCryptM,
    -- ** Errors
    ConfCryptError(..),
    -- ** Runtime Environment
    ConfCryptFile(..),
    Parameter(..),
    -- ** File Format
    ConfCryptElement(..),
    LineNumber(..),
    SchemaType(..),
    ParamLine(..),
    Schema(..),

    -- ** Key constraints
    LocalKey,
    KMSKey,

    -- * Helpers
    unWrapSchema,
    isParameter,
    typeToOutputString,
    parameterToLines
) where

import Conduit (ResourceT)
import Control.Monad.Reader (MonadReader, ReaderT, runReaderT)
import Control.Monad.Except (MonadError, ExceptT, runExceptT)
import Control.Monad.Writer (MonadWriter, WriterT, execWriterT)
import Control.DeepSeq (NFData)
import qualified Crypto.PubKey.RSA.Types as RSA
import GHC.Generics (Generic)
import qualified Data.Text as T
import qualified Data.Map.Strict as M

-- | The core transformer stack for ConfCrypt. The most important parts are the 'ReaderT' and
-- 'ResourceT', as the 'WriterT' and 'ExceptT' can both be replaced with explicit return types.
type ConfCryptM m ctx =
    ReaderT (ConfCryptFile, ctx) (
            WriterT [T.Text] (
                ExceptT ConfCryptError (
                    ResourceT m)
                )
        )

-- | The possible errors produced during a confcrypt operation.
data ConfCryptError
    = ParserError T.Text
    | NonRSAKey
    | KeyUnpackingError T.Text
    | DecryptionError T.Text
    | AWSDecryptionError T.Text
    | AWSEncryptionError T.Text
    | EncryptionError RSA.Error
    | MissingLine T.Text
    | UnknownParameter T.Text
    | WrongFileAction T.Text
    | CleanupError T.Text
    deriving (Show, Generic, Eq, Ord)

instance Ord RSA.Error where
    (<=) l r = show l <= show r

-- | As indicated in the Readme, a ConfCrypt file
data ConfCryptFile =
    ConfCryptFile {
        fileName :: T.Text,
        fileContents :: M.Map ConfCryptElement LineNumber,
        parameters :: [Parameter]
        } deriving (Show, Generic, NFData)

-- | The syntax used to describe a confcrypt file. A line in a confcrypt file may be one of 'Schema',
-- 'ParamLine', or comment. The grammar itself is described in the readme and 'Confcrypt.Parser'.
data ConfCryptElement
    = SchemaLine Schema
    | CommentLine {cText ::T.Text}
    | ParameterLine  ParamLine
    deriving (Show, Generic, NFData)

-- | this implementation means that there can only be a single parameter or schema with the same name.
-- Attempting to add multiple with the same name is undefined behavior and will result in missing data.
instance Eq ConfCryptElement where
    (==) (SchemaLine l) (SchemaLine r) = sName l == sName r
    (==) (ParameterLine l) (ParameterLine r) = pName l == pName r
    (==) (CommentLine l) (CommentLine r) = l == r
    (==) _ _ = False

-- | In order to
instance Ord ConfCryptElement where
    (<=) (SchemaLine l) (SchemaLine r) = sName l <= sName r
    (<=) (SchemaLine l) (CommentLine _) = False
    (<=) (SchemaLine l) (ParameterLine _) = True
    (<=) (ParameterLine l) (ParameterLine r) = pName l <= pName r
    (<=) (ParameterLine l) (CommentLine _) = False
    (<=) (ParameterLine l) (SchemaLine _) = False
    (<=) (CommentLine l) (CommentLine  r) = l <= r
    (<=) (CommentLine l) (ParameterLine _) = True
    (<=) (CommentLine l) (SchemaLine _) = True

-- | A parameter consists of both a 'ParamLine' and 'Schema' line from the confcr
data Parameter = Parameter {paramName :: T.Text, paramValue :: T.Text, paramType :: Maybe SchemaType}
    deriving (Eq, Ord, Show, Generic, NFData)

-- | A parsed parameter line from a confcrypt file
data ParamLine = ParamLine {pName :: T.Text, pValue :: T.Text}
    deriving (Eq, Ord, Show, Generic, NFData)

-- | A parsed schema line from a confcrypt file
data Schema = Schema {sName :: T.Text, sType :: SchemaType}
    deriving (Eq, Ord, Show, Generic, NFData)

-- | Self explanitory
newtype LineNumber = LineNumber Int
    deriving (Eq, Ord, Show, Generic, NFData)

-- | Indicates which types a
data SchemaType
    = CString -- ^ Maps to 'String'
    | CInt -- ^ Maps to 'Int'
    | CBoolean -- ^ Maps to 'Bool'
    deriving (Eq, Ord, Show, Generic, NFData, Read)


-- | A special purpose 'Show' function for convert
typeToOutputString ::
    SchemaType
    -> T.Text
typeToOutputString CString = "String"
typeToOutputString CInt = "Int"
typeToOutputString CBoolean = "Boolean"

-- | Convert a parameter into a 'ParameterLine' and 'SchemaLine' if possible.
parameterToLines ::
    Parameter
    -> (ParamLine, Maybe Schema)
parameterToLines Parameter {paramName, paramValue, paramType} =
    (ParamLine paramName paramValue, Schema paramName <$> paramType)

-- | Checks whether the provided line from a confcrypt file is a 'Parameter'
isParameter :: ConfCryptElement -> Bool
isParameter (ParameterLine _) = True
isParameter _ = False

-- | Attempts to unwrap a line from a confcrypt file into a 'Schema'
unWrapSchema :: ConfCryptElement -> Maybe Schema
unWrapSchema (SchemaLine s) = Just s
unWrapSchema _ = Nothing

-- | This constraint provides a type-level check that the wrapped key type is local to the
-- current machine. For use with things like RSA keys.
class LocalKey key

-- | This constraint provides a type-level check that the wrapped key type exists off-system inside
-- an externally provided Key Management System (KMS). For use with AWS KMS or Azure KMS.
class KMSKey key