module AWS.ApiGateway.RuntimeAPI.Types
		( ApiGatewayRequest(..)
		, ApiGatewayRequestContext(..)
		, ApiGatewayRequestHttpInfo(..)
		, agrBodyT
		, agrBodyJ
		, agrBodyBS
		, HeaderName(..)
		, mkHeaderName
		, ApiGatewayResponse(..)
		, ApiGatewayResponseBody(..)
		, mkApiGatewayResponse
		, mkApiGatewayResponseJSON
		, mkApiGatewayResponseStatus
		, ContentType
		) where

import           Control.DeepSeq            (NFData, NFData1)
import           Data.Aeson                 (FromJSON, FromJSON1, FromJSONKey,
                                             Options, ToJSON, ToJSON1,
                                             ToJSONKey)
import qualified Data.Aeson                 as JSON
import           Data.ByteString            (ByteString)
import qualified Data.ByteString.Base64     as BSB64
import qualified Data.ByteString.Char8      as C8
import qualified Data.ByteString.Lazy.Char8 as C8L
import           Data.CaseInsensitive       (CI)
import qualified Data.CaseInsensitive       as CI
import qualified Data.Char                  as Char
import           Data.Map                   (Map)
import           Data.String                (IsString)
import           Data.Text                  (Text)
import qualified Data.Text                  as T
import qualified Data.Text.Encoding.Base64  as TB64
import           Data.Time.Clock.System     (SystemTime (..))
import           Data.Time.LocalTime        (ZonedTime (..))
import           Data.Word                  (Word64)
import           GHC.Generics
import qualified Network.HTTP.Client        as HTTP
import qualified Network.HTTP.Types         as HTTP

-- | Newtype wrapper around header names to control JSON serialization and work
-- with 'Text' instead of 'ByteString'.
newtype HeaderName = HeaderName (CI Text) deriving (Show, Eq, Generic, NFData, Ord, FromJSONKey, ToJSONKey)

-- | Converts an arbitrary text value into a 'HeaderName'.
mkHeaderName :: Text -> HeaderName
mkHeaderName = HeaderName . CI.mk

instance ToJSON HeaderName where
	toJSON (HeaderName t) = JSON.toJSON $ CI.foldedCase t

instance FromJSON HeaderName where
	parseJSON = fmap (HeaderName . CI.mk @Text) . JSON.parseJSON

-- | A Version 2.0 API Gateway Request payload
data ApiGatewayRequest = ApiGatewayRequest
	{ agrRouteKey :: Text
	, agrRawPath :: Text
	, agrRawQueryString :: Text
	, agrCookies
		:: [Text]
	, agrHeaders -- ^ Repeated headers have values combined with commas.
		:: Map HeaderName Text
	, agrQueryStringParameters
		:: Map Text Text
	, agrRequestContext
		:: ApiGatewayRequestContext
	, agrBody -- ^ See 'agrBodyT', 'agrBodyBS', and 'agrBodyJ'
		:: JSON.Value
	, agrPathParameters
		:: Map Text Text
	, agrStageVariables
		:: Map Text Text
	, agrIsBase64Encoded :: Bool
	} deriving (Show, Generic, NFData, FromJSON)

-- | Provides a 'ByteString' of the body.  If 'agrIsBase64Encoded' is 'True' and the
-- body is JSON text, then the body is decoded from Base64.  Otherwise, the JSON value
-- of the body is encoded to 'ByteString'.
agrBodyBS :: (MonadFail m) => ApiGatewayRequest -> m ByteString
agrBodyBS ApiGatewayRequest{agrBody,agrIsBase64Encoded} =
	if agrIsBase64Encoded then
		case agrBody of
			JSON.String t ->
				case BSB64.decodeBase64 . C8.pack $ T.unpack t of
					Left err -> fail $ T.unpack err
					Right value -> return value
			value ->
				return . C8L.toStrict $ JSON.encode value
	else
		return . C8L.toStrict $ JSON.encode agrBody

-- | Provides a 'Text' of the body, decoding
agrBodyT :: (MonadFail m) => ApiGatewayRequest -> m Text
agrBodyT ApiGatewayRequest{agrBody,agrIsBase64Encoded} = case agrBody of
	JSON.String t ->
		if agrIsBase64Encoded then
			case TB64.decodeBase64 t of
				Left err -> fail $ T.unpack err
				Right value -> return value
		else
			return t
	value -> return . T.pack . C8L.unpack $ JSON.encode value

-- | Attempts to decode the body value into an instance of 'FromJSON'.
agrBodyJ :: (MonadFail m, FromJSON a) => ApiGatewayRequest -> m a
agrBodyJ agr = agrBodyBS agr >>= \bodybs ->
		case JSON.eitherDecode' $ C8L.fromStrict bodybs of
			Left err -> fail err
			Right val -> return val

-- | The "requestContext" field of the API Gateway Request payload
data ApiGatewayRequestContext = ApiGatewayRequestContext
	{ agrcAccountId :: Text
	, agrcApiId :: Text
	, agrcDomainName :: Text
	, agrcDomainPrefix :: Text
	, agrcHttp
		:: ApiGatewayRequestHttpInfo
	, agrcRequestId :: Text
	, agrcRouteKey :: Text
	, agrcStage :: Text
	, agrcTime :: ZonedTime
	, agrcTimeEpoch :: SystemTime
	} deriving (Show, Generic, NFData)

instance ToJSON ApiGatewayRequestContext where
	toJSON = JSON.genericToJSON jsonOptions'
	toEncoding = JSON.genericToEncoding jsonOptions'

instance FromJSON ApiGatewayRequestContext where
	parseJSON = JSON.genericParseJSON jsonOptions'

-- | Our implementation of how to map the field names to JSON labels
modifyFieldLabel :: String -> String
modifyFieldLabel str =
	case dropWhile Char.isLower str of
		[] -> str
		a:rest -> Char.toLower a : rest

jsonOptions' :: Options
jsonOptions' = JSON.defaultOptions
	{ JSON.fieldLabelModifier = modifyFieldLabel
	, JSON.omitNothingFields = True
	, JSON.unwrapUnaryRecords = True
	, JSON.tagSingleConstructors = False
	}

data ApiGatewayResponse = ApiGatewayResponse
	{ agrsStatus
		:: HTTP.Status
	, agrsBody
		:: ApiGatewayResponseBody
	, agrsHeaders
		:: Map HeaderName Text
	, agrsCookies
		:: Map Text Text
	}

type ContentType = Text

-- | The different ways to express the API Gateway response body.
-- See also 'mkApiGatewayResponseJSON' and 'mkApiGatewayResponseStatus'.
data ApiGatewayResponseBody
	= BodyEmpty -- ^ Empty body
	| BodyByteString ContentType C8L.ByteString -- ^ Body will be Base64 encoded bytes
	| BodyText ContentType Text -- ^ Body will be passed along directly
	| BodyStatusMessage -- ^ Body will contain the message associated with the status code

mkApiGatewayResponse :: ApiGatewayResponseBody -> ApiGatewayResponse
-- ^ Creates an API Gateway response with the provided body and an "OK 200" status code.
mkApiGatewayResponse body = ApiGatewayResponse
		{ agrsCookies = mempty
		, agrsHeaders = [ ( mkHeaderName "content-type", derivedContentType ) ]
		, agrsBody = body
		, agrsStatus = HTTP.ok200
		}
	where
		derivedContentType = case body of
			BodyEmpty -> "text/plain"
			BodyByteString contentType _ -> contentType
			BodyText contentType _ -> contentType
			BodyStatusMessage -> "text/plain"


mkApiGatewayResponseJSON :: (ToJSON a) => a -> ApiGatewayResponse
-- ^ Creates an API Gateway response with the body set to the argument encoded as JSON.
mkApiGatewayResponseJSON a = mkApiGatewayResponse body
	where
		body = BodyByteString "application/json" $ JSON.encode a

mkApiGatewayResponseStatus :: HTTP.Status -> ApiGatewayResponse
-- ^ Creates an API Gateway response with the status set to the argument.
-- If the status is `204 No Content` ('HTTP.noContent204'), then the body
-- is set to 'BodyEmpty'; otherwise, the body is set to 'BodyStatusMessage'.
mkApiGatewayResponseStatus status = (mkApiGatewayResponse body) { agrsStatus = status }
	where
		body =
			if status == HTTP.noContent204 then
				BodyEmpty
			else
				BodyStatusMessage

data ApiGatewayRequestHttpInfo = ApiGatewayRequestHttpInfo
	{ agrhMethod :: Text
	, agrhPath :: Text
	, agrhProtocol :: Text
	, agrhSourceIp :: Text
	, agrhUserAgent :: Text
	} deriving (Generic, NFData, Eq, Show)

instance ToJSON ApiGatewayRequestHttpInfo where
	toJSON = JSON.genericToJSON jsonOptions'
	toEncoding = JSON.genericToEncoding jsonOptions'

instance FromJSON ApiGatewayRequestHttpInfo where
	parseJSON = JSON.genericParseJSON jsonOptions'