-- Copyright (c) 2013-2014 PivotCloud, Inc.
--
-- Aws.Lambda.Core
--
-- Please feel free to contact us at licensing@pivotmail.com with any
-- contributions, additions, or other feedback; we would love to hear from
-- you.
--
-- Licensed under the Apache License, Version 2.0 (the "License"); you may
-- not use this file except in compliance with the License. You may obtain a
-- copy of the License at http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-- WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-- License for the specific language governing permissions and limitations
-- under the License.

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UnicodeSyntax #-}

module Aws.Lambda.Core
( -- * Client configuration
  LambdaConfiguration(..)
  -- ** Lenses
, lcRegion
, lcAccessKeyId
, lcSecretAccessKey

  -- * Client query
, LambdaQuery(..)
, lambdaQuery
, lambdaQuery'
  -- ** Lenses
, lqMethod
, lqPath
, lqParams
, lqBody

  -- * Transaction machinery
, LambdaTransaction(..)
, LambdaPayload(..)
, PagedLambdaTransaction(..)

  -- * Exceptions
, InvalidHttpMethodException
, _InvalidHttpMethodException
, InvalidRegionException
, _InvalidRegionException

  -- ** Exception Patterns
, pattern InvalidParameterValueException
, pattern ResourceNotFoundException
, pattern ServiceException
) where

import Aws.General
import Aws.Lambda.Internal.Constraints

import Control.Applicative
import Control.Lens
import Control.Lens.Action
import Control.Monad.Catch
import Control.Monad.Trans
import Control.Monad.Unicode

import qualified Data.Aeson as AE
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as LB
import qualified Data.Map as M
import Data.Monoid
import Data.Monoid.Unicode
import qualified Data.Text as T
import Data.Typeable
import Network.HTTP.Types
import Network.HTTP.Client
import qualified Network.Wreq as W
import qualified Network.Wreq.Types as WT
import Prelude.Unicode

data LambdaConfiguration
  = LambdaConfiguration
  { _lcRegion  !Region
  , _lcAccessKeyId  !B.ByteString
  , _lcSecretAccessKey  !B.ByteString
  } deriving (Eq, Show)

makeLenses ''LambdaConfiguration

lambdaOptions
   LambdaConfiguration
   W.Options
   W.Options
lambdaOptions lc =
  foldr () id $
    [ W.auth ?~ W.awsAuth W.AWSv4 (lc ^. lcAccessKeyId) (lc ^. lcSecretAccessKey)
    , W.header "Accept" .~ ["application/json"]
    ]

class LambdaPayload body where
  packagePayload
     body
     Σ (WT.Postable  WT.Putable)

instance LambdaPayload () where
  packagePayload () = Pack B.empty
instance LambdaPayload B.ByteString where
  packagePayload b = Pack b
instance LambdaPayload AE.Value where
  packagePayload x = Pack x

data LambdaQuery body
  = LambdaQuery
  { _lqBody  !body
  , _lqParams  !(M.Map T.Text T.Text)
  , _lqPath  ![T.Text]
  , _lqMethod  !StdMethod
  } deriving (Eq, Show)

makeLenses ''LambdaQuery

-- | A convenience constructor for a basic query.
--
lambdaQuery
   Monoid body
   StdMethod
   [T.Text]
   LambdaQuery body
lambdaQuery meth p = LambdaQuery
  { _lqMethod = meth
  , _lqPath = p
  , _lqBody = mempty
  , _lqParams = M.empty
  }

-- | A variant of 'lambdaQuery' that requires a body.
--
lambdaQuery'
   StdMethod
   [T.Text]
   body
   LambdaQuery body
lambdaQuery' meth p b = LambdaQuery
  { _lqMethod = meth
  , _lqPath = p
  , _lqBody = b
  , _lqParams = M.empty
  }

newtype InvalidRegionException
  = InvalidRegionException
  { _ireRegion  Region
  } deriving (Eq, Show, Typeable)
instance Exception InvalidRegionException

makePrisms ''InvalidRegionException

lambdaEndpointUrl
   MonadThrow m
   Region -- ^ The AWS region to target
   [T.Text] -- ^ The path of the AWS Lambda endpoint
   m String
lambdaEndpointUrl r e = do
  subdomain  case r of
    UsWest2  return "us-west-2"
    UsEast1  return "us-east-1"
    EuWest1  return "eu-west-1"
    _  throwM $ InvalidRegionException r
  return $
    "https://lambda."  subdomain  ".amazonaws.com/2014-11-13/"  (T.unpack $ T.intercalate "/" e)

liftThrow
   ( MonadThrow m
    , MonadIO m
    )
   IO α
   m α
liftThrow m = do
  either throwM return = do
    liftIO $ catch
      (fmap Right m)
      (\(e  SomeException)  return $ Left e)

newtype InvalidHttpMethodException
  = InvalidHttpMethodException
  { _ihmeMethod  StdMethod
  } deriving (Eq, Show, Typeable)
instance Exception InvalidHttpMethodException

makePrisms ''InvalidHttpMethodException

pattern InvalidParameterValueException msg
   StatusCodeException (Status 400 msg) _ _
pattern ResourceNotFoundException msg
   StatusCodeException (Status 400 msg) _ _
pattern ServiceException msg
   StatusCodeException (Status 500 msg) _ _

-- | A kludge to get around the fact that AWS Lambda sends back empty responses
-- to some requests.
--
asJSON'
   ( MonadThrow m
    , MonadCatch m
    , AE.FromJSON α
    )
   Response LB.ByteString
   m (Response α)
asJSON' resp =
  catch (W.asJSON resp) $ \(_  WT.JSONError)  do
    W.asJSON resp { responseBody = "{}" }

-- | A class for associating a request type with a response type.
--
class (LambdaPayload body, AE.FromJSON resp)  LambdaTransaction req body resp | req  resp body, resp  req where

  -- | Construct a 'LambdaQuery' object from the request data.
  --
  buildQuery
     req
     LambdaQuery body

  -- | Send the request to AWS Lambda.
  --
  runLambda
     ( MonadThrow m
      , MonadCatch m
      , MonadIO m
      )
     LambdaConfiguration
     req
     m resp
  runLambda cfg req = do
    let query = buildQuery req
        opts = lambdaOptions cfg W.defaults
          & W.params <>~ query ^. lqParams  to M.toList
        body = query ^. lqBody  to packagePayload

    url  lambdaEndpointUrl (cfg ^. lcRegion) (query ^. lqPath)
    spread body $ \payload  do
      resp  case query ^. lqMethod of
        GET  liftThrow $ W.getWith opts url
        POST  liftThrow $ W.postWith opts url payload
        PUT  liftThrow $ W.putWith opts url payload
        DELETE  liftThrow $ W.deleteWith opts url
        meth  throwM $ InvalidHttpMethodException meth
      resp ^! act asJSON'  W.responseBody

class (LambdaTransaction req body resp, Monoid acc)  PagedLambdaTransaction req body resp cur acc | req  resp cur acc where
  -- | To set the cursor in subsequent requests.
  --
  requestCursor  Setter' req (Maybe cur)

  -- | To get the cursor in respones.
  --
  responseCursor  Getter resp (Maybe cur)

  -- | To get the accumulating portion of the response data.
  --
  responseAccum  Getter resp acc

  -- | Exhaustively iterates a request to AWS lambda and returns the
  -- accumulated results.
  --
  pagedRunLambda
     ( MonadThrow m
      , MonadCatch m
      , MonadIO m
      , Functor m
      )
     LambdaConfiguration
     req
     m acc
  pagedRunLambda cfg req = do
    resp  runLambda cfg req
    case resp ^. responseCursor of
      Just cur 
        mappend (resp ^. responseAccum)
          <$> pagedRunLambda cfg (req & requestCursor ?~ cur)
      Nothing 
        return $
          resp ^. responseAccum