{-# LANGUAGE AllowAmbiguousTypes   #-}
{-# LANGUAGE DeriveFunctor         #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE RecordWildCards       #-}
-- |
-- Module      : Web.Stripe.StripeRequest
-- Copyright   : (c) David Johnson, 2014
-- Maintainer  : djohnson.m@gmail.com
-- Stability   : experimental
-- Portability : POSIX
module Web.Stripe.StripeRequest
  ( -- * Types
    Method(..)
  , Expandable(..)
  , ExpandParams(..)
  , Param(..)
  , Params
  , StripeRequest (..)
  , StripeReturn
  , StripeHasParam
  , ToStripeParam(..)
  , (-&-)
  , mkStripeRequest
  ) where

import           Control.Applicative ((<$>))
import           Data.ByteString    (ByteString)
import           Data.Monoid        ((<>))
import           Data.String        (fromString)
import           Data.Text          (Text)
import qualified Data.Text.Encoding as Text
import           Numeric            (showFFloat)
import           Web.Stripe.Types   (AccountBalance(..), AccountNumber(..),
                                     AddressCity(..), AddressCountry(..),
                                     ApplicationFeeId(..), AddressLine1(..),
                                     AddressLine2(..), AddressState(..),
                                     AddressZip(..), Amount(..), AmountOff(..),
                                     ApplicationFeeAmount(..),
                                     ApplicationFeePercent(..),
                                     AtPeriodEnd(..),
                                     AvailableOn(..), BankAccountId(..),
                                     CardId(..), CardNumber(..),
                                     Capture(..), ChargeId(..), Closed(..),
                                     CouponId(..),
                                     Country(..), Created(..), Currency(..),
                                     CustomerId(..), CVC(..), Date(..),
                                     DefaultCard(..), Description(..),
                                     Duration(..), DurationInMonths(..),
                                     Email(..), EndingBefore(..), EventId(..),
                                     Evidence(..), Expandable(..),
                                     ExpandParams(..), ExpMonth(..),
                                     ExpYear(..), Forgiven(..), Interval(..),
                                     IntervalCount(..),
                                     InvoiceId(..), InvoiceItemId(..),
                                     InvoiceLineItemId(..),
                                     IsVerified(..), MetaData(..), PlanId(..),
                                     PlanName(..), Prorate(..), Limit(..),
                                     MaxRedemptions(..), Name(..),
                                     NewBankAccount(..), NewCard(..),
                                     PercentOff(..), Quantity(..), ReceiptEmail(..),
                                     RecipientId(..), RecipientType(..), RedeemBy(..),
                                     RefundId(..),
                                     RefundApplicationFee(..), RefundReason(..),
                                     RoutingNumber(..), StartingAfter(..),
                                     StatementDescription(..), Source(..),
                                     SubscriptionId(..), TaxID(..),
                                     TaxPercent(..), TimeRange(..),
                                     TokenId(..), TransactionId(..),
                                     TransactionType(..), TransferId(..),
                                     TransferStatus(..), TrialEnd(..),
                                     TrialPeriodDays(..))
import           Web.Stripe.Util    (toBytestring, toExpandable,toMetaData,
                                     toSeconds, getParams, toText)

------------------------------------------------------------------------------
-- | HTTP Method
--
-- The other methods are not required by the Stripe API
data Method
  = DELETE
  | GET
  | POST
    deriving (Eq, Ord, Read, Show)

------------------------------------------------------------------------------
-- | HTTP Params
type Params = [(ByteString, ByteString)]

------------------------------------------------------------------------------
-- | used to set a specific key/value pair when the type is not enough
newtype Param k v = Param (k, v)

------------------------------------------------------------------------------
-- | Stripe Request holding `Method`, URL and `Params` for a Request. Also
-- includes the function needed to decode the response.
--
data StripeRequest a = StripeRequest
    { method      :: Method -- ^ Method of StripeRequest (i.e. `GET`, `PUT`, `POST`, `PUT`)
    , endpoint    :: Text   -- ^ Endpoint of StripeRequest
    , queryParams :: Params -- ^ Query Parameters of StripeRequest
    }

------------------------------------------------------------------------------
-- | convert a parameter to a key/value
class ToStripeParam param where
  toStripeParam :: param -> [(ByteString, ByteString)] -> [(ByteString, ByteString)]

instance ToStripeParam Amount where
  toStripeParam (Amount i) =
    (("amount", toBytestring i) :)

instance ToStripeParam AmountOff where
  toStripeParam (AmountOff i) =
    (("amount_off", toBytestring i) :)

instance ToStripeParam AccountBalance where
  toStripeParam (AccountBalance i) =
    (("account_balance", toBytestring i) :)

instance ToStripeParam AddressCity where
  toStripeParam (AddressCity txt) =
    (("address_city", Text.encodeUtf8 txt) :)

instance ToStripeParam AddressCountry where
  toStripeParam (AddressCountry txt) =
    (("address_country", Text.encodeUtf8 txt) :)

instance ToStripeParam AddressLine1 where
  toStripeParam (AddressLine1 txt) =
    (("address_line1", Text.encodeUtf8 txt) :)

instance ToStripeParam AddressLine2 where
  toStripeParam (AddressLine2 txt) =
    (("address_line2", Text.encodeUtf8 txt) :)

instance ToStripeParam AddressState where
  toStripeParam (AddressState txt) =
    (("address_state", Text.encodeUtf8 txt) :)

instance ToStripeParam AddressZip where
  toStripeParam (AddressZip txt) =
    (("address_zip", Text.encodeUtf8 txt) :)

instance ToStripeParam ApplicationFeeId where
  toStripeParam (ApplicationFeeId aid) =
    (("application_fee", Text.encodeUtf8 aid) :)

instance ToStripeParam ApplicationFeeAmount where
  toStripeParam (ApplicationFeeAmount cents) =
    (("application_fee", toBytestring cents) :)

instance ToStripeParam ApplicationFeePercent where
  toStripeParam (ApplicationFeePercent fee) =
    (("application_fee_percent", fromString $ showFFloat (Just 2) fee "") :)

instance ToStripeParam AvailableOn where
  toStripeParam (AvailableOn time) =
    (("available_on", toBytestring $ toSeconds time) :)

instance ToStripeParam AtPeriodEnd where
  toStripeParam (AtPeriodEnd p) =
    (("at_period_end", if p then "true" else "false") :)

instance ToStripeParam BankAccountId where
  toStripeParam (BankAccountId bid) =
    (("bank_account", Text.encodeUtf8 bid) :)

instance ToStripeParam Capture where
  toStripeParam (Capture b) =
    (("capture", if b then "true" else "false") :)

instance ToStripeParam CardId where
  toStripeParam (CardId cid) =
    (("card", Text.encodeUtf8 cid) :)

instance ToStripeParam CardNumber where
  toStripeParam (CardNumber num) =
    (("number", Text.encodeUtf8 num) :)

instance ToStripeParam ChargeId where
  toStripeParam (ChargeId cid) =
    (("charge", Text.encodeUtf8 cid) :)

instance ToStripeParam Closed where
  toStripeParam (Closed b) =
    (("closed", if b then "true" else "false") :)

instance ToStripeParam Created where
  toStripeParam (Created time) =
    (("created", toBytestring $ toSeconds time) :)

instance ToStripeParam Currency where
  toStripeParam currency =
    (("currency", toBytestring currency) :)

instance ToStripeParam CustomerId where
  toStripeParam (CustomerId cid) =
    (("customer", Text.encodeUtf8 cid) :)

instance ToStripeParam CouponId where
  toStripeParam (CouponId cid) =
    (("coupon", Text.encodeUtf8 cid) :)

instance ToStripeParam CVC where
  toStripeParam (CVC cvc) =
    (("cvc", Text.encodeUtf8 cvc) :)

instance ToStripeParam Date where
  toStripeParam (Date time) =
    (("created", toBytestring $ toSeconds time) :)

instance ToStripeParam DefaultCard where
  toStripeParam (DefaultCard (CardId cid)) =
    (("default_card", Text.encodeUtf8 cid) :)

instance ToStripeParam Description where
  toStripeParam (Description txt) =
    (("description", Text.encodeUtf8 txt) :)

instance ToStripeParam Duration where
  toStripeParam duration =
    (("duration", toBytestring duration) :)

instance ToStripeParam DurationInMonths where
  toStripeParam (DurationInMonths i) =
    (("duration_in_months", toBytestring i) :)

instance ToStripeParam Email where
  toStripeParam (Email txt) =
    (("email", Text.encodeUtf8 txt) :)

instance ToStripeParam EventId where
  toStripeParam (EventId eid) =
    (("event", Text.encodeUtf8 eid) :)

instance ToStripeParam Evidence where
  toStripeParam (Evidence txt) =
    (("evidence", Text.encodeUtf8 txt) :)

instance ToStripeParam ExpandParams where
  toStripeParam (ExpandParams params) =
    (toExpandable params ++)

instance ToStripeParam ExpMonth where
  toStripeParam (ExpMonth m) =
    (("exp_month", toBytestring m) :)

instance ToStripeParam ExpYear where
  toStripeParam (ExpYear y) =
    (("exp_year", toBytestring y) :)

instance ToStripeParam Forgiven where
  toStripeParam (Forgiven b) =
    (("forgiven", if b then "true" else "false") :)

instance ToStripeParam Interval where
  toStripeParam interval =
    (("interval", toBytestring interval) :)

instance ToStripeParam IntervalCount where
  toStripeParam (IntervalCount c) =
    (("interval_count", toBytestring c) :)

instance ToStripeParam InvoiceId where
  toStripeParam (InvoiceId txt) =
    (("invoice", Text.encodeUtf8 txt) :)

instance ToStripeParam InvoiceItemId where
  toStripeParam (InvoiceItemId txt) =
    (("id", Text.encodeUtf8 txt) :)

instance ToStripeParam InvoiceLineItemId where
  toStripeParam (InvoiceLineItemId txt) =
    (("line_item", Text.encodeUtf8 txt) :)

instance ToStripeParam IsVerified where
  toStripeParam (IsVerified b) =
    (("verified", if b then "true" else "false") :)

instance ToStripeParam Limit where
  toStripeParam (Limit i) =
    (("limit", toBytestring i) :)

instance ToStripeParam MaxRedemptions where
  toStripeParam (MaxRedemptions i) =
    (("max_redemptions", toBytestring i) :)

instance ToStripeParam Name where
  toStripeParam (Name txt) =
    (("name", Text.encodeUtf8 txt) :)

instance ToStripeParam NewBankAccount where
  toStripeParam NewBankAccount{..} =
    ((getParams
        [ ("bank_account[country]", Just $ (\(Country x) -> x) newBankAccountCountry)
        , ("bank_account[routing_number]", Just $ (\(RoutingNumber x) -> x) newBankAccountRoutingNumber)
        , ("bank_account[account_number]", Just $ (\(AccountNumber x) -> x) newBankAccountAccountNumber)
        ]) ++)

instance ToStripeParam NewCard where
  toStripeParam NewCard{..} =
    ((getParams
        [ ("card[number]", Just $ (\(CardNumber x) -> x) newCardCardNumber)
        , ("card[exp_month]", Just $ (\(ExpMonth x) -> toText x) newCardExpMonth)
        , ("card[exp_year]", Just $ (\(ExpYear x) -> toText x) newCardExpYear)
        , ("card[cvc]", (\(CVC x) -> x) <$> newCardCVC)
        , ("card[name]", getName <$> newCardName)
        , ("card[address_city]", (\(AddressCity x) -> x) <$> newCardAddressCity)
        , ("card[address_country]", (\(AddressCountry x) -> x) <$> newCardAddressCountry)
        , ("card[address_line1]", (\(AddressLine1 x) -> x) <$> newCardAddressLine1 )
        , ("card[address_line2]", (\(AddressLine2 x) -> x) <$> newCardAddressLine2 )
        , ("card[address_state]", (\(AddressState x) -> x) <$> newCardAddressState )
        , ("card[address_zip]", (\(AddressZip x) -> x) <$> newCardAddressZip )
        ]) ++)

instance ToStripeParam (Param Text Text) where
  toStripeParam (Param (k,v)) =
    ((Text.encodeUtf8 k, Text.encodeUtf8 v) :)

instance ToStripeParam PercentOff where
  toStripeParam (PercentOff i) =
    (("percent_off", toBytestring i) :)

instance ToStripeParam PlanId where
  toStripeParam (PlanId pid) =
    (("plan", Text.encodeUtf8 pid) :)

instance ToStripeParam PlanName where
  toStripeParam (PlanName txt) =
    (("name", Text.encodeUtf8 txt) :)

instance ToStripeParam Prorate where
  toStripeParam (Prorate p) =
    (("prorate", if p then "true" else "false") :)

instance ToStripeParam Quantity where
  toStripeParam (Quantity i) =
    (("quantity", toBytestring i) :)

instance ToStripeParam RecipientId where
  toStripeParam (RecipientId rid) =
    (("recipient", Text.encodeUtf8 rid) :)

instance ToStripeParam RedeemBy where
  toStripeParam (RedeemBy time) =
    (("redeem_by", toBytestring $ toSeconds time) :)

instance ToStripeParam RefundId where
  toStripeParam (RefundId fid) =
    (("refund", Text.encodeUtf8 fid) :)

instance ToStripeParam ReceiptEmail where
  toStripeParam (ReceiptEmail txt) =
    (("receipt_email", Text.encodeUtf8 txt) :)

instance ToStripeParam RecipientType where
  toStripeParam recipientType =
    (("type", toBytestring recipientType) :)

instance ToStripeParam a => ToStripeParam (Source a) where
  toStripeParam (Source param) =
    case toStripeParam param [] of
      [(_, p)] -> (("source", p) :)
      _        -> error "source applied to non-singleton"

instance ToStripeParam SubscriptionId where
  toStripeParam (SubscriptionId sid) =
    (("subscription", Text.encodeUtf8 sid) :)

instance ToStripeParam TaxID where
  toStripeParam (TaxID tid) =
    (("tax_id", Text.encodeUtf8 tid) :)

instance ToStripeParam TaxPercent where
  toStripeParam (TaxPercent tax) =
    (("tax_percent", fromString $ showFFloat (Just 2) tax "") :)

instance ToStripeParam a => ToStripeParam (TimeRange a) where
  toStripeParam (TimeRange{..}) =
    (case gt of
      Nothing -> id
      Just t  -> toRecord (toStripeParam t) "gt") .
    (case gte of
      Nothing -> id
      Just t  -> toRecord (toStripeParam t) "gte") .
    (case lt of
      Nothing -> id
      Just t  -> toRecord (toStripeParam t) "lt") .
    (case lte of
      Nothing -> id
      Just t  -> toRecord (toStripeParam t) "lte")
    where
      toRecord :: ([(ByteString, ByteString)] -> [(ByteString, ByteString)])
               -> ByteString
               -> ([(ByteString, ByteString)] -> [(ByteString, ByteString)])
      toRecord f n =
        case f [] of
          [(k,v)] -> ((k <> "[" <> n <> "]", v) :)
          lst'       -> error $ "toRecord in ToStripeRange (TimeRange a) expected exactly one element in this list. " ++ show lst'

instance ToStripeParam TokenId where
  toStripeParam (TokenId tid) =
    (("card", Text.encodeUtf8 tid) :)

instance ToStripeParam TrialEnd where
  toStripeParam (TrialEnd time) =
    (("trial_end", toBytestring $ toSeconds time) :)

instance ToStripeParam TransactionId where
  toStripeParam (TransactionId tid) =
    (("transaction", Text.encodeUtf8 tid) :)

instance ToStripeParam TransferId where
  toStripeParam (TransferId tid) =
    (("transfer", Text.encodeUtf8 tid) :)

instance ToStripeParam TransferStatus where
  toStripeParam transferStatus =
    (("status", toBytestring transferStatus) :)

instance ToStripeParam TrialPeriodDays where
  toStripeParam (TrialPeriodDays days) =
    (("trial_period_days", toBytestring days) :)

instance ToStripeParam MetaData where
  toStripeParam (MetaData kvs) =
    (toMetaData kvs ++)

instance ToStripeParam RefundApplicationFee where
  toStripeParam (RefundApplicationFee b) =
    (("refund_application_fee", if b then "true" else "false") :)

instance ToStripeParam RefundReason where
  toStripeParam reason =
    (("reason", case reason of
         RefundDuplicate -> "duplicate"
         RefundFraudulent -> "fraudulent"
         RefundRequestedByCustomer -> "requested_by_customer") :)

instance ToStripeParam StatementDescription where
  toStripeParam (StatementDescription txt) =
    (("statement_description", Text.encodeUtf8 txt) :)

instance ToStripeParam TransactionType where
  toStripeParam txn =
    (("type", case txn of
                ChargeTxn          -> "charge"
                RefundTxn          -> "refund"
                AdjustmentTxn      -> "adjustment"
                ApplicationFeeTxn  -> "application_fee"
                ApplicationFeeRefundTxn
                                   -> "application_fee_refund"
                TransferTxn        -> "transfer"
                TransferCancelTxn  -> "transfer_cancel"
                TransferFailureTxn -> "transfer_failure") :)


instance (ToStripeParam param) => ToStripeParam (StartingAfter param) where
  toStripeParam (StartingAfter param) =
    case toStripeParam param [] of
      [(_, p)] -> (("starting_after", p) :)
      _        -> error "StartingAfter applied to non-singleton"

instance (ToStripeParam param) => ToStripeParam (EndingBefore param) where
  toStripeParam (EndingBefore param) =
    case toStripeParam param [] of
      [(_, p)] -> (("ending_before", p) :)
      _        -> error "EndingBefore applied to non-singleton"

------------------------------------------------------------------------------
-- | indicate if a request allows an optional parameter
class (ToStripeParam param) => StripeHasParam request param where

------------------------------------------------------------------------------
-- | add an optional parameter to a `StripeRequest`
(-&-) :: StripeHasParam request param =>
         StripeRequest request
      -> param
      -> StripeRequest request
stripeRequest -&- param =
  stripeRequest { queryParams =
                     toStripeParam param (queryParams stripeRequest)
                }

------------------------------------------------------------------------------
-- | return type of stripe request
type family StripeReturn a :: *

------------------------------------------------------------------------------
-- | HTTP Params
--
-- helper function for building a 'StripeRequest'
mkStripeRequest
    :: Method
    -> Text
    -> Params
    -> StripeRequest a
mkStripeRequest m e q = StripeRequest m e q