{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}

{- |

Module      : Database.Couch.RequestBuilder
Description : Routines for creating the Request to send to CouchDB
Copyright   : Copyright (c) 2015, Michael Alan Dorman
License     : MIT
Maintainer  : mdorman@jaunder.io
Stability   : experimental
Portability : POSIX

-}

module Database.Couch.RequestBuilder where

import           Control.Monad        (return, (>>))
import           Control.Monad.Reader (Reader, ask, runReader)
import           Control.Monad.State  (StateT, execStateT, get, put)
import           Data.Aeson           (ToJSON, encode)
import           Data.ByteString      (ByteString, intercalate, null)
import           Data.Default         (def)
import           Data.Eq              ((==))
import           Data.Function        (on, ($), (.))
import           Data.List            (unionBy)
import           Data.Maybe           (Maybe (Just), maybe)
import           Data.Monoid          (mempty, (<>))
import           Data.Tuple           (fst)
import           Database.Couch.Types (Context, Credentials (Basic), DocId,
                                       DocRev, ctxCookies, ctxCred, reqDb,
                                       reqDocId, reqDocRev, reqHost,
                                       reqPassword, reqPort, reqUser)
import           Network.HTTP.Client  (Request, RequestBody (RequestBodyLBS),
                                       applyBasicAuth, cookieJar, host, method,
                                       path, port, requestBody, requestHeaders,
                                       setQueryString)
import           Network.HTTP.Types   (RequestHeaders, hAccept, hContentType)

-- | The state of our request as it's being built
data BuilderState =
  BuilderState {
    -- FIXME: Does it make more sense to hold all the individual
    -- pieces explicitly, and then just have a BuilderState -> Request
    -- function?
    -- | The base request being built
    bsRequest      :: Request,
    -- | The request itself only stores the 'queryString', so we accumulate pairs during construction, and use them to set the query string at the end.
    bsQueryParam   :: [(ByteString, Maybe ByteString)],
    -- | If this is set, it will be prepended to the path.
    bsDb           :: ByteString,
    -- | Again, stored this way for ease of manipulation, then properly assembled at the end.
    bsPathSegments :: [ByteString]
    }

-- | A type synonym for our builder monad
type RequestBuilder = StateT BuilderState (Reader Context)

-- | Given a 'Context', run our monadic builder function to produce a 'Request'.
runBuilder :: RequestBuilder () -> Context -> Request
runBuilder builder context =
  finalize (runReader (execStateT (defaultRequest >> builder) (BuilderState def [] mempty [])) context)

-- | This actually takes the 'BuilderState' and does the assembly of the various state bits into a single 'Request'.
finalize :: BuilderState -> Request
finalize (BuilderState r q d p) =
  setQueryString q r { path = calculatedPath }
  where
    calculatedPath =  "/" <> intercalate "/" ((if null d then [] else [d]) <> p)

{- | The default set of modifications applied to the request.

* The host/port connection information is set

* The 'Accept' header is set to 'application/json'

* The 'Content-Type' headers is set to 'application/json'

* Any authentication session in the cookie jar is set

* Any Basic Authentication information is applied

Any or all of these may be overridden, but probably shouldn't be.

-}
defaultRequest :: RequestBuilder ()
defaultRequest = do
  defaultHeaders [(hAccept, "application/json"), (hContentType, "application/json")]
  setAuth
  setConnection
  setCookieJar
  setMethod "GET"


-- * Applying 'Context' to the 'Request'

-- | Choose the database for the 'Request', based on what's in the 'Context'.  This is the one thing that could arguably throw an error.
selectDb :: RequestBuilder ()
selectDb = do
  c <- ask
  (BuilderState r q _ p) <- get
  put $ BuilderState r q (reqDb c) p

-- | Set the appropriate authentication markers on the 'Request', based on what's in the 'Context'.
setAuth :: RequestBuilder ()
setAuth = do
  c <- ask
  maybe (return ()) doApply (ctxCred c)
  where
    doApply cred = do
      (BuilderState r q d p) <- get
      put $ BuilderState (applyCred cred r) q d p
    applyCred (Basic u p) = applyBasicAuth (reqUser u) (reqPassword p)

-- | Set the host and port for the 'Request', based on what's in the 'Context'.
setConnection :: RequestBuilder ()
setConnection = do
  c <- ask
  (BuilderState r q d p) <- get
  put $ BuilderState r { host = reqHost c, port = reqPort c } q d p

-- | Set the 'CookieJar' for the 'Request', based on what's in the 'Context'.
setCookieJar :: RequestBuilder ()
setCookieJar = do
  c <- ask
  (BuilderState r q d p) <- get
  put $ BuilderState r { cookieJar = Just $ ctxCookies c } q d p

-- * Setting Headers

-- | Add headers to a 'Request', leaving existing instances undisturbed.
addHeaders :: RequestHeaders -> RequestBuilder ()
addHeaders new = do
  (BuilderState r q d p) <- get
  let headers = requestHeaders r
  put $ BuilderState r { requestHeaders = headers <> new } q d p

-- | Add headers to a 'Request', if they aren't already present.
defaultHeaders :: RequestHeaders -> RequestBuilder ()
defaultHeaders new = do
  (BuilderState r q d p) <- get
  let headers = requestHeaders r
  put $ BuilderState r { requestHeaders = unionBy ((==) `on` fst) headers new } q d p

-- | Set headers on the 'Request', overriding any existing instances.
setHeaders :: RequestHeaders -> RequestBuilder ()
setHeaders new = do
  (BuilderState r q d p) <- get
  let headers = requestHeaders r
  put $ BuilderState r { requestHeaders = unionBy ((==) `on` fst) new headers } q d p

-- * Setting Query Parameters

-- | Add query parameters to a 'Request', leaving existing parameters undisturbed.
addQueryParam :: [(ByteString, Maybe ByteString)] -> RequestBuilder ()
addQueryParam new = do
  (BuilderState r q d p) <- get
  put $ BuilderState r (q <> new) d p

-- | Add query parameters to a 'Request', if they aren't already present
defaultQueryParam :: [(ByteString, Maybe ByteString)] -> RequestBuilder ()
defaultQueryParam new = do
  (BuilderState r q d p) <- get
  put $ BuilderState r (unionBy ((==) `on` fst) q new) d p

-- | Set query parameters on the 'Request', overriding any existing instances.
setQueryParam :: [(ByteString, Maybe ByteString)] -> RequestBuilder ()
setQueryParam new = do
  (BuilderState r q d p) <- get
  put $ BuilderState r (unionBy ((==) `on` fst) new q) d p

-- * Setting the document path

-- | Add a path segment to the 'Request'.  This is only appropriate for static paths.
addPath :: ByteString -> RequestBuilder ()
addPath new = do
  (BuilderState r q d p) <- get
  put $ BuilderState r q d (p <> [new])

-- | Add a path segment to the 'Request', given a 'DocId'.
selectDoc :: DocId -> RequestBuilder ()
selectDoc = addPath . reqDocId

-- * Handling optional revision information

-- | Set the rev for the 'Request'.
addRev :: DocRev -> RequestBuilder ()
addRev rev =
  setHeaders [("ETag", reqDocRev rev)]

-- | Set the rev for the 'Request' if you have it.
maybeAddRev :: Maybe DocRev -> RequestBuilder ()
maybeAddRev =
  maybe (return ()) addRev

-- * Miscellaneous request options

-- | Set the body of the request to the encoded JSON value
setJsonBody :: ToJSON a
            => a -- ^ The document content
            -> RequestBuilder ()
setJsonBody new = do
  (BuilderState r q d p) <- get
  put $ BuilderState r { requestBody = RequestBodyLBS $ encode new } q d p

-- | Set the method for the 'Request'.
setMethod :: ByteString -> RequestBuilder ()
setMethod m = do
  (BuilderState r q d p) <- get
  put $ BuilderState r { method = m } q d p