--------------------------------------------------------------------------------
-- Haskell bindings for the Monzo API                                         --
-- Written by Michael B. Gale (michael.gale@cl.cam.ac.uk)                     --
--------------------------------------------------------------------------------

module Monzo.API (
    MonzoAuth,

    TransactionsAPI,
    FeedAPI,
    WebhooksAPI,
    AttachmentsAPI,
    MonzoAPI,

    monzoAPI,

    Monzo,

    withMonzo,
    withMonzoAt,

    listAccounts,
    getBalance,
    getTransaction,
    listTransactions,
    annotateTransaction,
    createFeedItem,
    registerWebhook,
    listWebhooks,
    deleteWebhook,
    uploadAttachment,
    registerAttachment,
    removeAttachment
) where

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

import GHC.TypeLits

import qualified Data.ByteString.Internal as BS
import Data.Proxy
import Data.Monoid ((<>))

import Control.Monad.Trans
import Control.Monad.Trans.Except
import Control.Monad.Trans.Reader

import Network.HTTP.Client (Manager, newManager)
import Network.HTTP.Client.TLS

import Servant.API hiding (addHeader)
import Servant.Common.Req (Req(..), addHeader)
import Servant.Client

import Monzo.Types

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

data MonzoAuth

instance HasClient api => HasClient (MonzoAuth :> api) where
    type Client (MonzoAuth :> api) = String -> Client api

    clientWithRoute Proxy req val =
        clientWithRoute (Proxy :: Proxy api) (monzoAuthReq val req)

monzoAuthReq :: String -> Req -> Req
monzoAuthReq token = addHeader "Authorization" ("Bearer " <> token)

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

-- | Adds pagination options to an API.
type Paginated c cts a =
    QueryParam "limit" Int :>
    QueryParam "since" Since :>
    QueryParam "before" Timestamp :>
    c cts a

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

-- | A type representing Monzo's transactions API.
type TransactionsAPI =
      Capture "transaction_id" TransactionID :>
      QueryParam "expand[]" String :>
      MonzoAuth :>
      Get '[JSON] TransactionResponse
 :<|> QueryParam "account_id" AccountID :>
      MonzoAuth :>
      Paginated Get '[JSON] Transactions
 :<|> Capture "transaction_id" TransactionID :>
      ReqBody '[FormUrlEncoded] Metadata :>
      MonzoAuth :>
      Patch '[JSON] TransactionResponse

-- | A type representing Monzo's feed API.
type FeedAPI =
      ReqBody '[FormUrlEncoded] (FeedItem BasicItem) :>
      MonzoAuth :>
      Post '[JSON] Empty

-- | A type representing Monzo's webhooks API.
type WebhooksAPI =
      ReqBody '[FormUrlEncoded] Webhook :>
      MonzoAuth :>
      Post '[JSON] Webhook
 :<|> QueryParam "account_id" AccountID :>
      MonzoAuth :>
      Get '[JSON] Webhooks
 :<|> Capture "webhook_id" WebhookID :>
      MonzoAuth :>
      Delete '[JSON] Empty

-- | A type representing Monzo's attachments API.
type AttachmentsAPI =
      "upload" :>
      ReqBody '[FormUrlEncoded] FileUploadReq :>
      MonzoAuth :>
      Post '[JSON] FileUploadRes
 :<|> "register" :>
      ReqBody '[FormUrlEncoded] Attachment :>
      MonzoAuth :>
      Post '[JSON] Attachment
 :<|> "deregister" :>
      ReqBody '[FormUrlEncoded] AttachmentID :>
      MonzoAuth :>
      Post '[JSON] Empty

-- | A type representing the Monzo API.
type MonzoAPI =
      "accounts" :> MonzoAuth :> Get '[JSON] AccountsResponse
 :<|> "balance" :> QueryParam "account_id" AccountID :> MonzoAuth :> Get '[JSON] Balance
 :<|> "transactions" :> TransactionsAPI
 :<|> "feed" :> FeedAPI
 :<|> "webhooks" :> WebhooksAPI
 :<|> "attachment" :> AttachmentsAPI

-- | `MonzoAPI` serves as a proxy value for the `MonzoAPI` type.
monzoAPI :: Proxy MonzoAPI
monzoAPI = Proxy

-- | The URL of the Monzo API.
monzoURL :: BaseUrl
monzoURL = BaseUrl Https "api.getmondo.co.uk" 443 "/"

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

-- | The type of computations which interact with Monzo's API.
type Monzo = ReaderT String (ReaderT Manager (ReaderT BaseUrl (ExceptT ServantError IO)))

-- | `withMonzo token f` runs a computation `f` which interacts with the Monzo
--   API and is authenticated by an OAuth token `token`.
withMonzo :: String -> Monzo a -> IO (Either ServantError a)
withMonzo = withMonzoAt monzoURL

-- | `withMonzoAt baseurl token f` runs a computation `f` which interacts with
--   the Monzo API and is authenticated by an OAuth token `token`, assuming
--   that the Monzo API can be found at `baseurl`.
withMonzoAt :: BaseUrl -> String -> Monzo a -> IO (Either ServantError a)
withMonzoAt url tkn m = do
    manager <- newManager tlsManagerSettings
    runExceptT (runReaderT (runReaderT (runReaderT m tkn) manager) url)

-- | `credential` retrieves the OAuth token used to make API requests.
credential :: Monzo String
credential = ask

-- | `manager` retrives the HTTP client used to make API requests.
manager :: Monzo Manager
manager = lift ask

-- | `baseurl` retrives the URL at which the API is located.
baseurl :: Monzo BaseUrl
baseurl = lift $ lift ask

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

accountsGET
    :<|> balanceGET
    :<|> transactionAPI
    :<|> feedAPI
    :<|> webHookAPI
    :<|> attachmentAPI = client monzoAPI

transactionGET
    :<|> transactionsGET
    :<|> transactionPATCH = transactionAPI

feedItemPOST = feedAPI

webHookPOST
    :<|> webHookGET
    :<|> webHookDELETE = webHookAPI

uploadPOST
    :<|> registerAttachmentPOST
    :<|> removeAttachmentPOST = attachmentAPI

-- | `handleAPI f` retrieves the implicit parameters from a `Monzo a`
--   computation and passes them on explicitly to `f`.
handleAPI :: (String -> Manager -> BaseUrl -> ExceptT ServantError IO a) -> Monzo a
handleAPI api = do
    mgr <- manager
    url <- baseurl
    tkn <- credential
    lift $ lift $ lift $ api tkn mgr url

-- | `listAccounts` lists all of the current user's accounts.
listAccounts :: Monzo [Account]
listAccounts = accounts <$> handleAPI accountsGET

-- | `getBalance acc` returns `acc`'s current balance.
getBalance :: Account -> Monzo Balance
getBalance Account{..} = handleAPI $ balanceGET (Just accountID)

-- | `getTransaction trans expandMerchant` retrives a transaction `trans` and
--   optionally expands information about the merchant if `expandMerchant` is
--   `True`.
getTransaction :: Transaction -> Bool -> Monzo Transaction
getTransaction Transaction{..} expandMerchant =
    transaction <$> handleAPI (transactionGET transactionID ex)
    where
        ex = if expandMerchant then Just "merchant" else Nothing

-- | `listTransactions acc opts` lists all transactions for `acc`.
listTransactions :: Account -> PageOptions -> Monzo [Transaction]
listTransactions Account{..} opts = transactions <$> handleAPI (\tkn ->
    transactionsGET (Just accountID) tkn
        (limit opts) (since opts) (Timestamp <$> before opts))

-- | `annotateTransaction trans metadata` annotates `trans` with some metadata.
annotateTransaction :: Transaction -> Metadata -> Monzo Transaction
annotateTransaction Transaction{..} meta = transaction <$>
    handleAPI (transactionPATCH transactionID meta)

-- | `createFeedItem item` creates a new feed item.
createFeedItem :: FeedItem BasicItem -> Monzo ()
createFeedItem item = toUnit <$> handleAPI (feedItemPOST item)

-- | `registerWebhook webhook` registers `webhook`.
registerWebhook :: Webhook -> Monzo Webhook
registerWebhook hook = handleAPI $ webHookPOST hook

-- | `listWebhooks account` lists all webhooks for `account`.
listWebhooks :: Account -> Monzo Webhooks
listWebhooks Account{..} = handleAPI $ webHookGET (Just accountID)

-- `deleteWebhook webhook` removes `webhook`.
deleteWebhook :: WebhookID -> Monzo ()
deleteWebhook hookID = toUnit <$> handleAPI (webHookDELETE hookID)

-- `uploadAttachment req` makes a request to upload an attachment.
uploadAttachment :: FileUploadReq -> Monzo FileUploadRes
uploadAttachment req = handleAPI $ uploadPOST req

-- `registerAttachment attachment` registers `attachment` for its transaction.
registerAttachment :: Attachment -> Monzo Attachment
registerAttachment att = handleAPI $ registerAttachmentPOST att

-- | `removeAttachment attachment` deletes `attachment` from its transaction.
removeAttachment :: AttachmentID -> Monzo ()
removeAttachment attID = toUnit <$> handleAPI (removeAttachmentPOST attID)

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