{-# OPTIONS_GHC -Wall #-}

{- | Typical use of this module:

1. Run 'get', 'post', or 'delete' to get a 'WreqResponse'.
2. Use 'wreqResponse' to convert the Wreq response to a 'Response'.
3. Use 'responseValue' to obtain the response payload as an Aeson
   'Data.Aeson.Value' (or an 'Error' if the request was not successful). -}

module Stripe.Wreq
  (
  -- * Request
  -- ** GET
    get, Get (..)
  -- ** POST
  , post, Post (..)
  -- ** DELETE
  , delete, Delete (..)

  -- * Response
  , WreqResponse, Response (..)
  , wreqResponse, responseValue, responseValueError

  -- * Error
  , Error (..), UserMessage (..), LogMessage (..), userError, logError

  -- * Status code
  , StatusCode (..)
  -- ** Predicates
  -- $predicates
  , isSuccess, isError, isClientError, isServerError
  -- ** Client error codes
  -- $constants
  , badRequest400, unauthorized401, requestFailed402
  , notFound404, conflict409, tooManyRequests429

  -- * Re-exports from Wreq
  , FormParam (..), Session, Network.Wreq.Session.newAPISession

  ) where

-- aeson
import qualified Data.Aeson

-- base
import qualified Control.Exception
import           Control.Monad     ((>=>))
import qualified Data.Bifunctor
import qualified Data.Semigroup
import           Prelude           hiding (userError)

-- bytestring
import qualified Data.ByteString
import qualified Data.ByteString.Lazy

-- lens
import Control.Lens ((&), (.~), (?~), (^.))

-- stripe-concepts
import Stripe.Concepts (ApiSecretKey (..))

-- text
import           Data.Text (Text)
import qualified Data.Text

-- unordered-containers
import qualified Data.HashMap.Strict

-- wreq
import           Network.Wreq         (FormParam (..))
import qualified Network.Wreq
import           Network.Wreq.Session (Session)
import qualified Network.Wreq.Session

------------------------------------------------------------

{- | An HTTP status code returned by Stripe.

"Stripe uses conventional HTTP response codes to indicate the success or failure
of an API request." - <https://stripe.com/docs/api#errors Stripe> -}

newtype StatusCode = StatusCode Int deriving Eq

{- $predicates

Some basic functions for interpreting status codes. -}

{- | "Codes in the 2xx range indicate success." -
<https://stripe.com/docs/api#errors Stripe> -}

isSuccess :: StatusCode -> Bool
isSuccess (StatusCode x) = x >= 200 && x < 300

{- | @isError x@ is equivalent to @'isClientError' x || 'isServerError' x@. -}

isError :: StatusCode -> Bool
isError (StatusCode x) = x >= 400 && x < 600

{- | "Codes in the 4xx range indicate an error that failed given the information
provided (e.g., a required parameter was omitted, a charge failed, etc.)." -
<https://stripe.com/docs/api#errors Stripe> -}

isClientError :: StatusCode -> Bool
isClientError (StatusCode x) = x >= 400 && x < 500

{- | "Codes in the 5xx range indicate an error with Stripe's servers." -
<https://stripe.com/docs/api#errors Stripe> -}

isServerError :: StatusCode -> Bool
isServerError (StatusCode x) = x >= 500 && x < 600

{- $constants

Constants for each of the error codes enumerated in the Stripe API
documentation, for your convenience. -}

{- | 400 - Bad Request

"The request was unacceptable, often due to missing a required parameter." -
<https://stripe.com/docs/api#errors Stripe> -}

badRequest400 :: StatusCode
badRequest400 = StatusCode 400

{- | 401 - Unauthorized

"No valid API key provided." - <https://stripe.com/docs/api#errors Stripe> -}

unauthorized401 :: StatusCode
unauthorized401 = StatusCode 401

{- | 402 - Request Failed

"The parameters were valid but the request failed." -
<https://stripe.com/docs/api#errors Stripe> -}

requestFailed402 :: StatusCode
requestFailed402 = StatusCode 402

{- | 404 - Not Found

"The requested resource doesn't exist." -
<https://stripe.com/docs/api#errors Stripe> -}

notFound404 :: StatusCode
notFound404 = StatusCode 404

{- | 409 - Conflict

"The request conflicts with another request (perhaps due to using the same
idempotent key)." - <https://stripe.com/docs/api#errors Stripe> -}

conflict409 :: StatusCode
conflict409 = StatusCode 409

{- | 429 - Too Many Requests

"Too many requests hit the API too quickly. We recommend an exponential backoff
of your requests." - <https://stripe.com/docs/api#errors Stripe> -}

tooManyRequests429 :: StatusCode
tooManyRequests429 = StatusCode 429

------------------------------------------------------------

data Get =
  Get
    { getPath     :: [Text]          -- ^ URL path components
    , getParams   :: [(Text, Text)]  -- ^ Query params
    }

data Post =
  Post
    { postPath     :: [Text]         -- ^ URL path components
    , postParams   :: [FormParam]    -- ^ Parameters to send in the request body
    }

data Delete =
  Delete
    { deletePath   :: [Text]         -- ^ URL path components
    , deleteParams :: [(Text, Text)] -- ^ Query params
    }

get :: Session -> ApiSecretKey -> Get -> IO WreqResponse
get session key x = Network.Wreq.Session.getWith opts session url
  where
    url = makeUrl (getPath x)
    opts = wreqDefaults & Network.Wreq.auth ?~ auth key
                        & Network.Wreq.params .~ (getParams x)

post :: Session -> ApiSecretKey -> Post -> IO WreqResponse
post session key x = Network.Wreq.Session.postWith opts session url params
  where
    url = makeUrl (postPath x)
    params = postParams x
    opts = wreqDefaults & Network.Wreq.auth ?~ auth key

delete :: Session -> ApiSecretKey -> Delete -> IO WreqResponse
delete session key x = Network.Wreq.Session.deleteWith opts session url
  where
    url = makeUrl (deletePath x)
    opts = wreqDefaults & Network.Wreq.auth ?~ auth key
                        & Network.Wreq.params .~ (deleteParams x)

urlBase :: Text
urlBase = Data.Text.pack "https://api.stripe.com/v1"

makeUrl :: [Text] -> String
makeUrl =
    Data.Text.unpack
    . Data.Text.intercalate (Data.Text.pack "/")
    . (urlBase :)

wreqDefaults :: Network.Wreq.Options
wreqDefaults = Network.Wreq.defaults & noCheckResponse

{- | Set a "response checker" that overrides Wreq's default one which causes
exceptions to be thrown for non-2xx HTTP status codes -}

noCheckResponse :: Network.Wreq.Options -> Network.Wreq.Options
noCheckResponse = Network.Wreq.checkResponse ?~ (\_ _ -> return ())

{- | Represent a Stripe API key as a Wreq 'Network.Wreq.Auth' value.

"Authentication to the API is performed via HTTP Basic Auth. Provide your API
key as the basic auth username value. You do not need to provide a password." -
<https://stripe.com/docs/api#authentication Stripe> -}

auth :: ApiSecretKey -> Network.Wreq.Auth
auth (ApiSecretKey key) = Network.Wreq.basicAuth key Data.ByteString.empty

------------------------------------------------------------

{- | An error message suitable for being shown to a user. -}

newtype UserMessage = UserMessage Text deriving (Eq, Show)

{- | An error message that should go into an error log, /not/ shown to a user.
-}

newtype LogMessage = LogMessage Text deriving (Eq, Show)

data Error =
  Error
    { userMessages :: [UserMessage]
    , logMessages  :: [LogMessage]
    }
    deriving (Eq, Show)

instance Data.Semigroup.Semigroup Error
  where
    Error x y <> Error x' y' =
        Error
            ((Data.Semigroup.<>) x x')
            ((Data.Semigroup.<>) y y')

instance Monoid Error
  where
    mappend = (Data.Semigroup.<>)
    mempty = Error mempty mempty

instance Control.Exception.Exception Error

userError
    :: Text -- ^ An error message intended to be shown to a user.
    -> Error

userError x = Error { userMessages = [UserMessage x], logMessages = [] }

logError
    :: Text -- ^ An error message intended to go into a log file,
            --   /not/ to be shown to a user.
    -> Error

logError x = Error { userMessages = [], logMessages = [LogMessage x] }

------------------------------------------------------------

type WreqResponse = Network.Wreq.Response Data.ByteString.Lazy.ByteString

data Response =
  Response
    { responseBody :: Either Text Data.Aeson.Value
        -- ^ Every Stripe response should have a JSON body; but if not, this
        --   will be a 'Left' value with an error message from the JSON parser.
    , responseCode :: StatusCode
        -- ^ The status code of the HTTP response.
    }

{- | Convert a 'WreqResponse' into a 'Response' by parsing the JSON response
body (the Stripe API always returns JSON) and getting the HTTP status code. -}

wreqResponse :: WreqResponse -> Response
wreqResponse r =
  Response
    { responseBody =
        r ^. Network.Wreq.responseBody
           & Data.Aeson.eitherDecode
           & Data.Bifunctor.first Data.Text.pack
    , responseCode =
        r ^. Network.Wreq.responseStatus
           . Network.Wreq.statusCode
           & StatusCode
    }

{- | Interpret a response, returning 'Right' with the parsed JSON payload if
everything is okay, or 'Left' with an error if the response contains any
indication that something went wrong. -}

responseValue :: Response -> Either Error Data.Aeson.Value
responseValue r =
    case (responseBody r) of
        Left e    -> Left (logError e)
        Right val ->
            case isSuccess (responseCode r) of
                True  -> Right val
                False -> Left (responseValueError val)

{- | If the response object looks like this:

> {
>   "error": {
>       "type": "card_error",
>       "message": "..."
>     }
>   }
> }

then we use the value of the @message@ field as a 'UserMessage'. Otherwise it is
a 'LogMessage'.

"@message@: A human-readable message providing more details about the error. For
card errors, these messages can be shown to your users. [...] Card errors are
the most common type of error you should expect to handle. They result when the
user enters a card that can't be charged for some reason." -
<https://stripe.com/docs/api#errors Stripe> -}

responseValueError :: Data.Aeson.Value -> Error
responseValueError val
    | isCardError  =  foldMap userError (msg val)
    | otherwise    =  foldMap logError  (msg val)
  where

    isCardError = typ val == Just (Data.Text.pack "card_error")

    msg = aesonAttr "error" >=> aesonAttr "message" >=> aesonText
    typ = aesonAttr "error" >=> aesonAttr "type"    >=> aesonText

------------------------------------------------------------

-- Internal Aeson decoding functions

aesonAttr :: String -> Data.Aeson.Value -> Maybe Data.Aeson.Value
aesonAttr x = aesonObject >=> Data.HashMap.Strict.lookup (Data.Text.pack x)

aesonObject :: Data.Aeson.Value -> Maybe Data.Aeson.Object
aesonObject (Data.Aeson.Object x) = Just x
aesonObject _ = Nothing

aesonText :: Data.Aeson.Value -> Maybe Text
aesonText (Data.Aeson.String x) = Just x
aesonText _ = Nothing