{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
module Network.Google.OAuth2
    ( getToken
    , OAuth2Client(..)
    , Scope
    , AccessToken
    ) where

import Control.Concurrent
import Control.Exception (onException, throwIO, catch, IOException)
import Control.Monad (join)
import Data.Aeson
import qualified Data.ByteString.Char8 as B
import Data.Monoid ((<>))
import Data.String (fromString)
import Data.Time
import Network.HTTP.Types (renderSimpleQuery, status200)
import Network.HTTP.Req
import Network.Wai
import Network.Wai.Handler.Warp
import System.Directory
import System.Exit
import System.FilePath
import System.IO (hPutStrLn, stderr)
import System.Posix.Files

getToken :: OAuth2Client -> FilePath -> [Scope] -> IO AccessToken
getToken c tokenFile scopes = readToken c tokenFile `catch` download
    where
        download :: IOException -> IO AccessToken
        download = const $ downloadToken c tokenFile scopes

readToken :: OAuth2Client -> FilePath -> IO AccessToken
readToken c tokenFile = do
    t <- read <$> readFile tokenFile
    let dt = 5
        -- Avoid latent error
        e = fromIntegral $ expiresIn t - dt
    now <- getCurrentTime
    mt <- getModificationTime tokenFile
    if now < addUTCTime e mt
       then return $ B.pack $ accessToken t
       else do
           t' <- getNewTokenInfo c (refreshToken t)
           saveTokenInfo tokenFile t'
           return $ B.pack $ accessToken t'

getNewTokenInfo :: OAuth2Client -> RefreshToken -> IO TokenInfo
getNewTokenInfo c rt = do
    let body = ReqBodyUrlEnc $
                   "refresh_token" =: rt <>
                   "client_id" =: clientId c <>
                   "client_secret" =: clientSecret c <>
                   "grant_type" =: ("refresh_token" :: String)
    res <- req POST tokenUrl body jsonResponse mempty
    let t' = responseBody res
    return $ t' { refreshToken = rt }

saveTokenInfo :: FilePath -> TokenInfo -> IO ()
saveTokenInfo tokenFile t = do
    createDirectoryIfMissing True $ takeDirectory tokenFile
    writeFile tokenFile (show t)
    let fm = unionFileModes ownerReadMode ownerWriteMode
    setFileMode tokenFile fm

downloadToken :: OAuth2Client -> FilePath -> [Scope] -> IO AccessToken
downloadToken c tokenFile scopes = do
    code <- getCode c scopes
    t <- exchangeCode c code
    saveTokenInfo tokenFile t
    return $ B.pack $ accessToken t

getCode :: OAuth2Client -> [Scope] -> IO Code
getCode c scopes = do
    m <- newEmptyMVar
    let st = setHost (fromString localhost)
             $ setPort serverPort defaultSettings
    _ <- forkIO $ runSettings st (server m)
            `onException` do
                hPutStrLn stderr $ "Unable to use port " ++ show serverPort
                putMVar m Nothing
    let authUri = "https://accounts.google.com/o/oauth2/v2/auth"
        q = renderSimpleQuery True
                [ ("scope", B.pack $ unwords scopes)
                , ("redirect_uri", B.pack redirectUri)
                , ("response_type", "code")
                , ("client_id", B.pack $ clientId c)
                ]
    putStrLn "Open the following uri in your browser:"
    putStrLn $ B.unpack $ authUri <> q
    mc <- takeMVar m
    case mc of
         Nothing -> die "Unable to get code"
         Just code -> return code

server :: MVar (Maybe Code) -> Application
server m request respond = do
    putMVar m $ B.unpack <$> join (lookup "code" $ queryString request)
    respond $ responseLBS status200
                          [("Content-Type", "text/plain")]
                          "Return your app"

exchangeCode :: OAuth2Client -> Code -> IO TokenInfo
exchangeCode c code = do
    let body = ReqBodyUrlEnc $
                   "code" =: code <>
                   "client_id" =: clientId c <>
                   "client_secret" =: clientSecret c <>
                   "redirect_uri" =: redirectUri <>
                   "grant_type" =: ("authorization_code" :: String)
    res <- req POST tokenUrl body jsonResponse mempty
    return $ responseBody res

tokenUrl :: Url 'Https
tokenUrl = https "accounts.google.com" /: "o" /: "oauth2" /: "token"

serverPort :: Port
serverPort = 8017

localhost :: String
localhost = "127.0.0.1"

redirectUri :: String
redirectUri = concat ["http://", localhost, ":", show serverPort]

data OAuth2Client = OAuth2Client
    { clientId :: String
    , clientSecret :: String
    } deriving (Show, Read)

type AccessToken = B.ByteString
type RefreshToken = String
type Code = String

type Scope = String

data TokenInfo = TokenInfo
    { accessToken :: String
    , refreshToken :: String
    , expiresIn :: Int
    } deriving (Show, Read)

instance FromJSON TokenInfo where
    parseJSON (Object v) = TokenInfo <$> v .: "access_token"
                                     <*> v .:? "refresh_token" .!= ""
                                     <*> v .: "expires_in"
    parseJSON _ = mempty

instance MonadHttp IO where
  handleHttpException = throwIO