{-# LANGUAGE OverloadedStrings, FlexibleContexts #-}
module Network.PayPal.NVP (
        -- * High-level interface
        Service,
        sandbox,
        live,
        submit,
        -- * Low-level interface
        prepareRequest,
        processResponse
    ) where

import Network.PayPal.Types as PayPal

import Control.Applicative
import Control.Arrow
import Control.Failure
import Control.Monad
import Control.Monad.Trans
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as C
import qualified Data.ByteString.Lazy as L
import Data.Maybe
import Data.Monoid
import Network.HTTP.Conduit as HTTP
import Network.HTTP.Types as HTTP
import System.IO.Unsafe
import Data.Conduit
import qualified Network.Wai as W


-- | A PayPal service.
data Service = Service (HTTP.Request IO) (HTTP.Request IO)

-- | PayPal sandbox.
sandbox :: Service
sandbox = Service
    -- URL for signature security
    (unsafePerformIO $ parseUrl "https://api-3t.sandbox.paypal.com/nvp")
    -- URL for certificate security
    (unsafePerformIO $ parseUrl "https://api.sandbox.paypal.com/nvp")

-- | PayPal live system.
live :: Service
live = Service
    -- URL for signature security
    (unsafePerformIO $ parseUrl "https://api-3t.paypal.com/nvp")
    -- URL for certificate security
    (unsafePerformIO $ parseUrl "https://api.paypal.com/nvp")

-- | Submit a request to PayPal.
submit :: (Failure HttpException m, MonadIO m, PayPalRequest req) =>
          Service
       -> Credentials
       -> req
       -> m (Either (Response L.ByteString) (PayPal.Status (PayPalResponse req)))
submit service cred req = do
    -- Re-using connections (i.e. not creating a new manager each time) fails with
    -- an exception for some reason:
    --
    -- too few bytes. Failed reading at byte position 1
    res <- liftIO $ withManager $ httpLbs (prepareRequest service cred req)
    processResponse req res

-- | Construct an HTTP request for the specified PayPal request.
prepareRequest :: (Monad m, PayPalRequest req) =>
                  Service
               -> Credentials
               -> req
               -> HTTP.Request m
prepareRequest (Service url _) (Credentials user pwd (Signature sig) version) req =
    urlEncodedBody vars url
  where
    vars =
        [("USER", user),
         ("PWD", pwd),
         ("VERSION", version),
         ("SIGNATURE", sig)] ++ toVariables req

processResponse :: (PayPalRequest req, MonadIO m) =>
                   req
                -> Response L.ByteString
                -> m (Either (Response L.ByteString) (PayPal.Status (PayPalResponse req)))
processResponse req res = do
    let Status statusCode _ = responseStatus res
        body = responseBody res
    return $ case statusCode of
        200 -> Right . decodeResponseChecking
                     . map (second (fromMaybe mempty))
                     . parseQuery
                     . (mconcat . L.toChunks) $ body
        _   -> Left res