{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE CPP               #-}
{-# LANGUAGE OverloadedStrings #-}

module Web.Stripe.Client.HttpClient
       (
         StripeRequest(..)
       , StripeError(..)
       , StripeConfig(..)

       , stripe
       , stripeManager
       , stripeConn

         -- * low-level
       , withConnection
       , withManager
       , callAPI

       ) where

import qualified Control.Arrow
import qualified Data.ByteString.Lazy     as BSL
import qualified Data.Text.Encoding       as TE
import qualified Network.HTTP.Types       as Http

import Data.Aeson               as A
import Data.ByteString          (ByteString)
import Data.Monoid              ((<>))
#if MIN_VERSION_http_client(0,5,13)
import Network.HTTP.Client      as Http hiding (withManager, withConnection)
#else
import Network.HTTP.Client      as Http hiding (withManager)
#endif
import Network.HTTP.Client.TLS  as TLS

import qualified Web.Stripe.StripeRequest as S

import Web.Stripe.Client (APIVersion (..), StripeConfig (..),
                          StripeError (..), StripeKey (..),
                          StripeRequest, StripeReturn,
                          attemptDecode, handleStream,
                          parseFail, toBytestring, unknownCode)


-- | Create a request to 'Stripe's API.
--
-- This function uses the global TLS manager from @http-client-tls@
-- via 'getGlobalManager'.
stripe :: FromJSON (StripeReturn a)
       => StripeConfig
       -> StripeRequest a
       -> IO (Either StripeError (StripeReturn a))
stripe config request = do
    man <- TLS.getGlobalManager
    callAPI man fromJSON config request

-- | Create a request to 'Stripe's API using a 'Manager'.
stripeManager :: FromJSON (StripeReturn a)
              => Manager
              -> StripeConfig
              -> StripeRequest a
              -> IO (Either StripeError (StripeReturn a))
stripeManager manager config request = callAPI manager fromJSON config request

-- | Create a request to 'Stripe's API using a 'Manager'.
--
-- This function is used to maintain compatibility w/
-- @stripe-http-streams@. However, the terminology in @http-streams@
-- uses 'Connection' whereas @http-client@ uses connection 'Manager'.
stripeConn :: FromJSON (StripeReturn a)
           => Manager
           -> StripeConfig
           -> StripeRequest a
           -> IO (Either StripeError (StripeReturn a))
stripeConn = stripeManager

withConnection :: (Manager -> IO (Either StripeError a))
               -> IO (Either StripeError a)
withConnection = withManager

withManager :: (Manager -> IO (Either StripeError a))
            -> IO (Either StripeError a)
withManager m = do

    -- @http-client@ has a set of deprecated `withManager` functions
    -- that are not necessary to safely prevent a 'Manager' from
    -- leaking resources. "Manager's will be closed and shutdown
    -- automatically (and safely) via gargage collection.
    manager <- TLS.getGlobalManager
    m manager

-- | Create a request to 'Stripe's API using an existing 'Manager'
--
-- This is a low-level function. In most cases you probably want to
-- use 'stripe' or 'stripeManager'.
callAPI :: Manager
        -> (Value -> Result b)
        -> StripeConfig
        -> StripeRequest a
        -> IO (Either StripeError b)
callAPI man fromJSON' config stripeRequest = do

    res <- httpLbs mkStripeRequest man

    let status = Http.statusCode (Http.responseStatus res)

    if not (attemptDecode status) then
        return unknownCode

    else do
        case A.eitherDecode (Http.responseBody res) of
            Left e  -> pure $ parseFail e
            Right a -> pure $ handleStream fromJSON' status $ return a
  where
    mkStripeRequest =

        let req = Http.applyBasicAuth (getStripeKey (secretKey config)) mempty $
                  defaultRequest {
                    Http.method = m2m (S.method stripeRequest)
                  , Http.secure = True
                  , Http.host = "api.stripe.com"
                  , Http.port = 443
                  , Http.path = "/v1/" <> TE.encodeUtf8 (S.endpoint stripeRequest)
                  , Http.requestHeaders = [
                        ("Stripe-Version", toBytestring stripeVersion)
                      , ("Connection", "Keep-Alive")
                      ]
                  , Http.checkResponse = \_ _ -> return ()
                  }

            stripeQueryParams = fmap
                                  (Control.Arrow.second Just)
                                  (S.queryParams stripeRequest)

        in if S.GET == S.method stripeRequest then
               Http.setQueryString stripeQueryParams req
           else
               urlEncodeBody (S.queryParams stripeRequest) req

m2m :: S.Method -> Http.Method
m2m S.GET    = Http.methodGet
m2m S.POST   = Http.methodPost
m2m S.DELETE = Http.methodDelete

-- | This function is used instead of http-client's built-in 'urlEncodedBody' as
-- the request method is set explicitly to POST in 'urlEncodeBody' but Stripe
-- uses POST\/PUT\/DELETE. A PR should be submitted to http-client to fix
-- eventually.
urlEncodeBody :: [(ByteString, ByteString)] -> Request -> Request
urlEncodeBody headers req = req {
      requestBody = RequestBodyLBS (BSL.fromChunks body)
    , requestHeaders =
        ("Content-Type", "application/x-www-form-urlencoded")
      : filter (\(x, _) -> x /= "Content-Type") (requestHeaders req)
    }
  where
    body = pure (Http.renderSimpleQuery False headers)

stripeVersion :: APIVersion
stripeVersion = V20141007