{-# LANGUAGE DeriveDataTypeable #-}
module Database.Bloodhound.Auth.Amazonka.Internal where


-------------------------------------------------------------------------------
import           Control.Applicative         as A
import           Control.Exception
import           Data.Time.Clock
import           Data.Typeable
import           Network.AWS.Data.Body
import           Network.AWS.Data.ByteString
import           Network.AWS.Data.Path
import           Network.AWS.Data.Query
import qualified Network.AWS.Data.Query      as A
import           Network.AWS.ElasticSearch   (elasticSearch)
import           Network.AWS.Types           (AuthEnv, Region, sgRequest,
                                              sgSign)
import qualified Network.AWS.Types           as A
import           Network.HTTP.Client
import           Network.HTTP.Types.Method   (Method, parseMethod)
import           URI.ByteString
-------------------------------------------------------------------------------


-- | Request hook to install into your 'BHEnv'. Does not handle
-- streaming request bodies, which should not be an issue for
-- Bloodhound. The exception cases handled by 'EsAmazonkaAuthError'
-- are truly exceptional and should probably be thrown.
--
-- @
--    env <- newEnv region Discover
--    let auth = env ^. envAuth
--    let hook req = withAuth auth $ \ae ->
--                     either (liftIO . throwIO) return =<< amazonkaAuthHook ae region req
--    mgr <- newManager tlsManagerSettings
--    let bhe = (mkBHEnv server mgr) { bhRequestHook = hook }
-- @
amazonkaAuthHook :: AuthEnv -> Region -> Request -> IO (Either EsAmazonkaAuthError Request)
amazonkaAuthHook ae reg req = amazonkaAuthHook' ae reg req A.<$> getCurrentTime


-------------------------------------------------------------------------------
amazonkaAuthHook' :: AuthEnv -> Region -> Request -> UTCTime -> Either EsAmazonkaAuthError Request
amazonkaAuthHook' ae reg req now = toReq <$> toAwsRequest req reg
  where algo = sgSign (A._svcSigner elasticSearch)
        toReq req' = restoreTimeout (decodePath (sgRequest (algo req' ae reg now)))
        -- We decode the path because for some reason AWS ES actually
        -- doesn't want the path url encoded. If you do, it will
        -- expect double-encoding for the canonical uri. If you
        -- double, it will expect triple and so-on.
        decodePath x = x { path = urlDecode True (path x)}
        restoreTimeout x = x { responseTimeout = responseTimeout req }


-------------------------------------------------------------------------------
toAwsRequest :: Request -> Region -> Either EsAmazonkaAuthError (A.Request a)
toAwsRequest r reg = do
  meth <- either (const (Left badMethod)) Right (parseMethod bsMeth)
  rqb <- toRQBody (requestBody r)
  q <- toQS (queryString r)
  return (A.Request { A._rqService = svc
                    , A._rqMethod = meth
                    , A._rqPath = rawPath (path r)
                    , A._rqQuery = q
                    , A._rqHeaders = requestHeaders r
                    , A._rqBody = rqb})
  where bsMeth = method r
        badMethod = InvalidStdMethod bsMeth
        svc = elasticSearch { A._svcEndpoint = const endpoint}
        endpoint = requestEndpoint r reg


-------------------------------------------------------------------------------
toQS :: ByteString -> Either EsAmazonkaAuthError A.QueryString
toQS bs = case parseRelativeRef laxURIParserOptions bs of
            Right rr -> Right (go (rrQuery rr))
            Left _   -> Left MalformedQueryString
  where go q = QList [ QPair k (QValue (Just v)) | (k, v) <- queryPairs q]

-------------------------------------------------------------------------------
requestEndpoint :: Request -> Region -> A.Endpoint
requestEndpoint r reg = A.Endpoint { A._endpointHost = host r
                                   , A._endpointSecure = secure r
                                   , A._endpointPort = port r
                                   , A._endpointScope = toBS reg}

-------------------------------------------------------------------------------
-- | These edge cases shouldn't come up in normal operation. The best
-- course of action is probably to throw these as an exception.
data EsAmazonkaAuthError = InvalidStdMethod Method
                         | StreamingBodyNotSupported
                         | MalformedQueryString deriving (Show, Eq, Typeable)


instance Exception EsAmazonkaAuthError

-------------------------------------------------------------------------------
toRQBody :: RequestBody -> Either EsAmazonkaAuthError RqBody
toRQBody (RequestBodyLBS b) = Right (toBody b)
toRQBody (RequestBodyBS b)  = Right (toBody b)
toRQBody _                  = Left StreamingBodyNotSupported