-- | This module bridges the minimal calls in the
--   Client module, adds specific headers required by
--   the API, and passes them to the more generic calls
--   of the OAuth2 module.
--
--   It also includes auxiliary funtions for handling
--   its main return type: @ResponseData@.

module PokitDok.Requests
    ( pokitdokGetRequest
    , pokitdokDeleteRequest
    , pokitdokPostRequest
    , pokitdokPutRequest
    , pokitdokMultipartRequest
    , activateKeyWithAuthCode
    , refreshExpired
    , isExpired'
    , assertValid
    , getJSONIO
    ) where

import qualified Codec.Binary.Base64.String as B64
import qualified System.IO.Strict           as SIO
import System.Directory (getCurrentDirectory)
import System.Info (os)
import Data.Time.Clock
import Data.Time.Clock.POSIX (getPOSIXTime)
import Data.Time.Calendar (fromGregorian)
import Data.Maybe (fromJust)
import Data.Hex (hex)
import Network.HTTP.Base (httpPackageVersion)

import PokitDok.OAuth2

-- * Middleman Network Calls

-- | Sends a GET request to the server with the given query parameters.
pokitdokGetRequest
    :: OAuth2          -- ^ An OAuth2 credential.
    -> String          -- ^ The request url path.
    -> Parameters      -- ^ The query parameters.
    -> IO ResponseData -- ^ The response from the server.
pokitdokGetRequest key = getRequest headers
    where headers = [bearerH . fromJust $ oauthAccessToken key, userAgentH]

-- | Sends a DELETE request to the server with the given query parameters.
pokitdokDeleteRequest
    :: OAuth2          -- ^ A credential.
    -> String          -- ^ The request url path.
    -> IO ResponseData -- ^ Response from the server.
pokitdokDeleteRequest key = deleteRequest headers
    where headers = [bearerH . fromJust $ oauthAccessToken key, userAgentH]

-- | Sends a POST request to the server and posts the json string.
pokitdokPostRequest
    :: OAuth2          -- ^ Credentials.
    -> String          -- ^ The request url path.
    -> String          -- ^ JSON object String.
    -> IO ResponseData -- ^ Response from the server.
pokitdokPostRequest key = postRequest headers
    where headers = [bearerH . fromJust $ oauthAccessToken key, userAgentH]

-- | Sends a PUT request with JSON data.
pokitdokPutRequest
    :: OAuth2          -- ^ Credentials.
    -> String          -- ^ The request url path.
    -> String          -- ^ A JSON object String.
    -> IO ResponseData -- ^ Response from the server.
pokitdokPutRequest key = putRequest headers
    where headers = [bearerH . fromJust $ oauthAccessToken key, userAgentH]

-- | A multipart request that uploads a file.
pokitdokMultipartRequest
    :: OAuth2          -- ^ Credentials.
    -> String          -- ^ The request url path.
    -> Parameters      -- ^ Post data.
    -> String          -- ^ File system path of file to be posted.
    -> IO ResponseData -- ^ Response from the server.
pokitdokMultipartRequest auth url params pPath = do
    postPath <- format pPath
    boundary <- multipartBoundary
    multipartRequest headers url params postPath boundary
    where
    headers = [bearerH . fromJust $ oauthAccessToken auth, userAgentH]
    osChar =
        if os == "mingw"
        then '\\'
        else '/'
    format file@('C':_) = return file
    format file@('/':_) = return file
    format file = getCurrentDirectory >>= return . (++ [osChar] ++ file)

-- * Auxiliary Functions

-- The boundary of the PD multipart file upload.
multipartBoundary :: IO String
multipartBoundary = do
    now <- getCurrentTime
    let day0          = UTCTime { utctDay = fromGregorian 0001 01 01
                                , utctDayTime = 0
                                }
        ticks         = floor $ (realToFrac $ diffUTCTime now day0) * 10000000
    return $ "----------------------------" ++ (hex $ show ticks)

-- The bearer authorization header as a String tuple, given an AccessToken.
bearerH    :: AccessToken -> (String, String)
bearerH (AccessToken token _ _ _ _) = makeAuthHeader $ "Bearer " ++ token

-- The basic authorization header as a String tuple, given an OAuth2.
basicH     :: OAuth2      -> (String, String)
basicH (OAuth2 id sec _ _ _ _)      = makeAuthHeader $ "Basic "++B64.encode(id++":"++sec)

-- The pokitdok-haskell user agent header, as a String tuple.
userAgentH :: (String, String)
userAgentH = makeUserAgentHeader $ "haskell-pokitdok/" ++ httpPackageVersion

-- | Pulls the JSON response @String@ from a @ResponseDat@um,
--   and lifts the result to an IO monad.
getJSONIO :: ResponseData -> IO String
getJSONIO = return . getJSON

-- Extracts the JSON response from ResponseData.
getJSON :: ResponseData -> String
getJSON (ResponseData _ body _) = body


-- * Supplementary Client Functions

-- | Takes an @OAuth2@ & an authorization code,
--   and returns an @OAuth2@ with a refreshed @AccessToken@.
activateKeyWithAuthCode :: OAuth2 -> String -> IO OAuth2
activateKeyWithAuthCode auth code = do
    token <- authenticateAuthorizationCode headers auth code
    return $ keyModAccessToken auth (Just token)
    where headers = [basicH auth, userAgentH]

-- | Refreshes an @OAuth2@'s token given whether or not it is expired.
refreshExpired :: OAuth2 -> Bool -> IO OAuth2
refreshExpired auth False = return auth
refreshExpired auth@(OAuth2 _ _ _ _ _ (Just (AccessToken _ (Just _) _ _ _))) _ = do
    token <- authenticateRefreshToken headers auth
    return $ keyModAccessToken auth (Just token)
    where headers = [basicH auth, userAgentH]
refreshExpired auth _ = do
    token <- authenticateClientCredentials headers auth
    return $ keyModAccessToken auth (Just token)
    where headers = [basicH auth, userAgentH]

-- | Checks if the given @AccessToken@ is expired.
isExpired' :: AccessToken -> IO Bool
isExpired' (AccessToken _ _ (Just exp) _ _) = getPOSIXTime >>=
    (\now -> return $ (realToFrac now :: Float) > (fromIntegral exp :: Float) - 5) -- 5 sec timeout
isExpired' _ = return False

-- | Asserts that the given @OAuth2@ is valid to make a call.
assertValid :: OAuth2 -> IO ()
assertValid (OAuth2 _ _ _ _ _ Nothing)  = error "Access token has not been initialized."
assertValid (OAuth2 _ _ _ _ _ (Just t)) = do
    dead <- isExpired' t
    if dead then error "Access token is expired." else return ()