{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
module Network.HTTP.Client.Core
    ( withResponse
    , httpLbs
    , httpNoBody
    , httpRaw
    , httpRaw'
    , requestAction
    , getModifiedRequestManager
    , responseOpen
    , responseClose
    , httpRedirect
    , httpRedirect'
    , withConnection
    , handleClosedRead
    ) where

import Network.HTTP.Types
import Network.HTTP.Client.Manager
import Network.HTTP.Client.Types
import Network.HTTP.Client.Headers
import Network.HTTP.Client.Body
import Network.HTTP.Client.Request
import Network.HTTP.Client.Response
import Network.HTTP.Client.Cookies
import Data.Maybe (fromMaybe, isJust)
import Data.Time
import Data.IORef
import Control.Exception
import qualified Data.ByteString.Lazy as L
import Data.Monoid
import Control.Monad (void)
import System.Timeout (timeout)
import System.IO.Unsafe (unsafePerformIO)
import Data.KeyedPool
import GHC.IO.Exception (IOException(..), IOErrorType(..))

-- | Perform a @Request@ using a connection acquired from the given @Manager@,
-- and then provide the @Response@ to the given function. This function is
-- fully exception safe, guaranteeing that the response will be closed when the
-- inner function exits. It is defined as:
--
-- > withResponse req man f = bracket (responseOpen req man) responseClose f
--
-- It is recommended that you use this function in place of explicit calls to
-- 'responseOpen' and 'responseClose'.
--
-- You will need to use functions such as 'brRead' to consume the response
-- body.
--
-- Since 0.1.0
withResponse :: Request
             -> Manager
             -> (Response BodyReader -> IO a)
             -> IO a
withResponse req man f = bracket (responseOpen req man) responseClose f

-- | A convenience wrapper around 'withResponse' which reads in the entire
-- response body and immediately closes the connection. Note that this function
-- performs fully strict I\/O, and only uses a lazy ByteString in its response
-- for memory efficiency. If you are anticipating a large response body, you
-- are encouraged to use 'withResponse' and 'brRead' instead.
--
-- Since 0.1.0
httpLbs :: Request -> Manager -> IO (Response L.ByteString)
httpLbs req man = withResponse req man $ \res -> do
    bss <- brConsume $ responseBody res
    return res { responseBody = L.fromChunks bss }

-- | A convenient wrapper around 'withResponse' which ignores the response
-- body. This is useful, for example, when performing a HEAD request.
--
-- Since 0.3.2
httpNoBody :: Request -> Manager -> IO (Response ())
httpNoBody req man = withResponse req man $ return . void


-- | Get a 'Response' without any redirect following.
httpRaw
     :: Request
     -> Manager
     -> IO (Response BodyReader)
httpRaw = fmap (fmap snd) . httpRaw'

-- | Get a 'Response' without any redirect following.
--
-- This extended version of 'httpRaw' also returns the potentially modified Request.
httpRaw'
     :: Request
     -> Manager
     -> IO (Request, Response BodyReader)
httpRaw' req0 m = do
    let req' = mSetProxy m req0
    (req, cookie_jar') <- case cookieJar req' of
        Just cj -> do
            now <- getCurrentTime
            return $ insertCookiesIntoRequest req' (evictExpiredCookies cj now) now
        Nothing -> return (req', Data.Monoid.mempty)
    res <- makeRequest req m
    case cookieJar req' of
        Just _ -> do
            now' <- getCurrentTime
            let (cookie_jar, _) = updateCookieJar res req now' cookie_jar'
            return (req, res {responseCookieJar = cookie_jar})
        Nothing -> return (req, res)

makeRequest
    :: Request
    -> Manager
    -> IO (Response BodyReader)
makeRequest req m = do
    action <- readIORef requestAction
    action req m

requestAction :: IORef (Request -> Manager -> IO (Response BodyReader))
{-# NOINLINE requestAction #-}
requestAction = unsafePerformIO (newIORef action)
  where
    action
        :: Request
        -> Manager
        -> IO (Response BodyReader)
    action req m = do
        (timeout', mconn) <- getConnectionWrapper
            (responseTimeout' req)
            (getConn req m)

        -- Originally, we would only test for exceptions when sending the request,
        -- not on calling @getResponse@. However, some servers seem to close
        -- connections after accepting the request headers, so we need to check for
        -- exceptions in both.
        ex <- try $ do
            cont <- requestBuilder (dropProxyAuthSecure req) (managedResource mconn)

            getResponse (mMaxHeaderLength m) (mMaxNumberHeaders m) timeout' req mconn cont

        case ex of
            -- Connection was reused, and might have been closed. Try again
            Left e | managedReused mconn && mRetryableException m e -> do
                managedRelease mconn DontReuse
                action req m
            -- Not reused, or a non-retry, so this is a real exception
            Left e -> do
              -- Explicitly release connection for all real exceptions:
              -- https://github.com/snoyberg/http-client/pull/454
              managedRelease mconn DontReuse
              throwIO e
            -- Everything went ok, so the connection is good. If any exceptions get
            -- thrown in the response body, just throw them as normal.
            Right res -> return res
      where
        getConnectionWrapper mtimeout f =
            case mtimeout of
                Nothing -> fmap ((,) Nothing) f
                Just timeout' -> do
                    before <- getCurrentTime
                    mres <- timeout timeout' f
                    case mres of
                        Nothing -> throwHttp ConnectionTimeout
                        Just mConn -> do
                            now <- getCurrentTime
                            let timeSpentMicro = diffUTCTime now before * 1000000
                                remainingTime = round $ fromIntegral timeout' - timeSpentMicro
                            if remainingTime <= 0
                                then do
                                    managedRelease mConn DontReuse
                                    throwHttp ConnectionTimeout
                                else return (Just remainingTime, mConn)

        responseTimeout' req =
            case responseTimeout req of
                ResponseTimeoutDefault ->
                    case mResponseTimeout m of
                        ResponseTimeoutDefault -> Just 30000000
                        ResponseTimeoutNone -> Nothing
                        ResponseTimeoutMicro u -> Just u
                ResponseTimeoutNone -> Nothing
                ResponseTimeoutMicro u -> Just u

-- | The used Manager can be overridden (by requestManagerOverride) and the used
-- Request can be modified (through managerModifyRequest). This function allows
-- to retrieve the possibly overridden Manager and the possibly modified
-- Request.
--
-- (In case the Manager is overridden by requestManagerOverride, the Request is
-- being modified by managerModifyRequest of the new Manager, not the old one.)
getModifiedRequestManager :: Manager -> Request -> IO (Manager, Request)
getModifiedRequestManager manager0 req0 = do
  let manager = fromMaybe manager0 (requestManagerOverride req0)
  req <- mModifyRequest manager req0
  return (manager, req)

-- | The most low-level function for initiating an HTTP request.
--
-- The first argument to this function gives a full specification
-- on the request: the host to connect to, whether to use SSL,
-- headers, etc. Please see 'Request' for full details.  The
-- second argument specifies which 'Manager' should be used.
--
-- This function then returns a 'Response' with a
-- 'BodyReader'.  The 'Response' contains the status code
-- and headers that were sent back to us, and the
-- 'BodyReader' contains the body of the request.  Note
-- that this 'BodyReader' allows you to have fully
-- interleaved IO actions during your HTTP download, making it
-- possible to download very large responses in constant memory.
--
-- An important note: the response body returned by this function represents a
-- live HTTP connection. As such, if you do not use the response body, an open
-- socket will be retained indefinitely. You must be certain to call
-- 'responseClose' on this response to free up resources.
--
-- This function automatically performs any necessary redirects, as specified
-- by the 'redirectCount' setting.
--
-- When implementing a (reverse) proxy using this function or relating
-- functions, it's wise to remove Transfer-Encoding:, Content-Length:,
-- Content-Encoding: and Accept-Encoding: from request and response
-- headers to be relayed.
--
-- Since 0.1.0
responseOpen :: Request -> Manager -> IO (Response BodyReader)
responseOpen inputReq manager' = do
  case validateHeaders (requestHeaders inputReq) of
    GoodHeaders -> return ()
    BadHeaders reason -> throwHttp $ InvalidRequestHeader reason
  (manager, req0) <- getModifiedRequestManager manager' inputReq
  wrapExc req0 $ mWrapException manager req0 $ do
    (req, res) <- go manager (redirectCount req0) req0
    checkResponse req req res
    mModifyResponse manager res
        { responseBody = wrapExc req0 (responseBody res)
        }
  where
    wrapExc :: Request -> IO a -> IO a
    wrapExc req0 = handle $ throwIO . toHttpException req0

    go manager0 count req' = httpRedirect'
      count
      (\req -> do
        (manager, modReq) <- getModifiedRequestManager manager0 req
        (req'', res) <- httpRaw' modReq manager
        let mreq = if redirectCount modReq == 0
              then Nothing
              else getRedirectedRequest req' req'' (responseHeaders res) (responseCookieJar res) (statusCode (responseStatus res))
        return (res, fromMaybe req'' mreq, isJust mreq))
      req'

-- | Redirect loop.
httpRedirect
     :: Int -- ^ 'redirectCount'
     -> (Request -> IO (Response BodyReader, Maybe Request)) -- ^ function which performs a request and returns a response, and possibly another request if there's a redirect.
     -> Request
     -> IO (Response BodyReader)
httpRedirect count0 http0 req0 = fmap snd $ httpRedirect' count0 http' req0
  where
    -- adapt callback API
    http' req' = do
        (res, mbReq) <- http0 req'
        return (res, fromMaybe req0 mbReq, isJust mbReq)

handleClosedRead :: SomeException -> IO L.ByteString
handleClosedRead se
    | Just ConnectionClosed <- fmap unHttpExceptionContentWrapper (fromException se)
        = return L.empty
    | Just (HttpExceptionRequest _ ConnectionClosed) <- fromException se
        = return L.empty
    | Just (IOError _ ResourceVanished _ _ _ _) <- fromException se
        = return L.empty
    | otherwise
        = throwIO se

-- | Redirect loop.
--
-- This extended version of 'httpRaw' also returns the Request potentially modified by @managerModifyRequest@.
httpRedirect'
     :: Int -- ^ 'redirectCount'
     -> (Request -> IO (Response BodyReader, Request, Bool)) -- ^ function which performs a request and returns a response, the potentially modified request, and a Bool indicating if there was a redirect.
     -> Request
     -> IO (Request, Response BodyReader)
httpRedirect' count0 http' req0 = go count0 req0 []
  where
    go count _ ress | count < 0 = throwHttp $ TooManyRedirects ress
    go count req' ress = do
        (res, req, isRedirect) <- http' req'
        if isRedirect then do
            -- Allow the original connection to return to the
            -- connection pool immediately by flushing the body.
            -- If the response body is too large, don't flush, but
            -- instead just close the connection.
            let maxFlush = 1024
            lbs <- brReadSome (responseBody res) maxFlush
                -- The connection may already be closed, e.g.
                -- when using withResponseHistory. See
                -- https://github.com/snoyberg/http-client/issues/169
                `Control.Exception.catch` handleClosedRead
            responseClose res

            -- And now perform the actual redirect
            go (count - 1) req (res { responseBody = lbs }:ress)
        else
            return (req, res)

-- | Close any open resources associated with the given @Response@. In general,
-- this will either close an active @Connection@ or return it to the @Manager@
-- to be reused.
--
-- Since 0.1.0
responseClose :: Response a -> IO ()
responseClose = runResponseClose . responseClose'

-- | Perform an action using a @Connection@ acquired from the given @Manager@.
--
-- You should use this only when you have to read and write interactively
-- through the connection (e.g. connection by the WebSocket protocol).
--
-- @since 0.5.13
withConnection :: Request -> Manager -> (Connection -> IO a) -> IO a
withConnection origReq man action = do
    mHttpConn <- getConn (mSetProxy man origReq) man
    action (managedResource mHttpConn) <* keepAlive mHttpConn
        `finally` managedRelease mHttpConn DontReuse
