{-# LANGUAGE GeneralizedNewtypeDeriving #-}

-- |
-- Module      : Network.OAuth.Simple
-- Copyright   : (c) Joseph Abrahamson 2013
-- License     : MIT
--
-- Maintainer  : me@jspha.com
-- Stability   : experimental
-- Portability : non-portable
--
-- Simplified Monadic interface for managing @http-client@ and
-- @oauthenticated@ state. Re-exposes all of the functionality from
-- "Network.OAuth" and "Network.OAuth.ThreeLegged".
--
module Network.OAuth.Simple (

  -- * A monad for authenticated requests
  --
  -- | "Network.OAuth.Simple" re-exports the "Network.OAuth" and
  -- "Network.Oauth.ThreeLegged" interfaces using the obvious 'StateT' and 'ReaderT'
  -- wrappers for tracking configuration, credentials, and random generator state.
  -- Managing 'C.Manager' state is out of scope for this module, but since 'OAuthT'
  -- is a monad transformer, it's easy enough to add another layer with the needed
  -- state.

  oauth, runOAuthSimple,

  -- ** More sophisticated interface
  runOAuth, runOAuthT, OAuthT (..), OAuth,

  -- * Configuration management
  upgradeCred, upgrade,

  -- * Configuration re-exports

  -- ** OAuth Credentials
  O.Token (..), O.Cred, O.Client, O.Temporary, O.Permanent,

  -- *** Creating Credentials
  O.clientCred, O.temporaryCred, O.permanentCred,
  O.fromUrlEncoded,

  -- ** OAuth Configuration
  O.Server (..), O.defaultServer,
  O.ParameterMethod (..), O.SignatureMethod (..), O.Version (..),

  -- ** Three-Legged Authorization

  -- *** Configuration types
  O.ThreeLegged (..), O.parseThreeLegged, O.Callback (..),
  O.Verifier,

  -- *** Actions
  requestTemporaryToken, buildAuthorizationUrl, requestPermanentToken,

  -- *** Example System
  requestTokenProtocol, TokenRequestFailure (..)

  ) where

import           Control.Applicative
import qualified Control.Monad.Catch             as E
import           Control.Monad.Reader
import           Control.Monad.State
import           Control.Monad.Trans.Either
import qualified Crypto.Random                   as R
import qualified Data.ByteString.Lazy            as SL
import qualified Network.HTTP.Client             as C
import qualified Network.OAuth                   as O
import qualified Network.OAuth.ThreeLegged       as O
import qualified Network.OAuth.Types.Credentials as Cred
import           Network.URI                     (URI)

data OaConfig ty =
  OaConfig { cred        :: O.Cred ty
           , server      :: O.Server
           , threeLegged :: O.ThreeLegged
           }

-- | Perform authenticated requests using a shared 'C.Manager' and
-- a particular set of 'O.Cred's.
newtype OAuthT ty m a =
  OAuthT { unOAuthT :: ReaderT (OaConfig ty) (StateT R.SystemRNG m) a }
  deriving ( Functor, Applicative, Monad
           , MonadReader (OaConfig ty)
           , MonadState R.SystemRNG
           , E.MonadCatch
           , E.MonadThrow
           , MonadIO
           )
instance MonadTrans (OAuthT ty) where lift = OAuthT . lift . lift

-- | 'OAuthT' wrapped over 'IO'.
type OAuth ty = OAuthT ty IO

-- | Run's an 'OAuthT' using a fresh 'R.EntropyPool'.
runOAuthT
  :: (MonadIO m) =>
     OAuthT ty m a -> O.Cred ty -> O.Server -> O.ThreeLegged ->
     m a
runOAuthT oat cr srv tl = do
  entropy <- liftIO R.createEntropyPool
  evalStateT (runReaderT (unOAuthT oat) (OaConfig cr srv tl)) (R.cprgCreate entropy)

runOAuth :: OAuth ty a -> O.Cred ty -> O.Server -> O.ThreeLegged -> IO a
runOAuth = runOAuthT

-- | The simplest way to execute a set of authenticated requests. Produces
-- invalid 'ThreeLegged' requests---use 'runOAuth' to provide 'O.Server' and
-- 'O.ThreeLegged' configuration information.
runOAuthSimple :: OAuth ty a -> O.Cred ty -> IO a
runOAuthSimple oat cr = runOAuth oat cr O.defaultServer tl where
  Just tl = O.parseThreeLegged "http://example.com"
                               "http://example.com"
                               "http://example.com"
                               O.OutOfBand

upgradeCred :: (Cred.ResourceToken ty', Monad m) => O.Token ty' -> OAuthT ty m (O.Cred ty')
upgradeCred tok = liftM (Cred.upgradeCred tok . cred) ask

-- | Given a 'Cred.ResourceToken' of some kind, run an inner 'OAuthT' session
-- with the same configuration but new credentials.
upgrade :: (Cred.ResourceToken ty', Monad m) => O.Token ty' -> OAuthT ty' m a -> OAuthT ty m a
upgrade tok oat = do
  gen  <- state R.cprgFork
  conf <- ask
  let conf' = conf { cred = Cred.upgradeCred tok (cred conf) }
  lift $ evalStateT (runReaderT (unOAuthT oat) conf') gen

liftBasic :: MonadIO m => (R.SystemRNG -> OaConfig ty -> IO (a, R.SystemRNG)) -> OAuthT ty m a
liftBasic f = do
  gen  <- get
  conf <- ask
  (a, gen') <- liftIO (f gen conf)
  put gen'
  return a

-- | Sign a request using fresh credentials.
oauth :: MonadIO m => C.Request -> OAuthT ty m C.Request
oauth req = liftBasic $ \gen conf -> O.oauth (cred conf) (server conf) req gen

-- Three-Legged Authorization
--------------------------------------------------------------------------------

requestTemporaryToken
  :: MonadIO m => C.Manager ->
     OAuthT O.Client m (C.Response (Either SL.ByteString (O.Token O.Temporary)))
requestTemporaryToken man =
  liftBasic $ \gen conf ->
    O.requestTemporaryToken (cred conf)
                            (server conf)
                            (threeLegged conf)
                            man
                            gen

buildAuthorizationUrl :: Monad m => OAuthT O.Temporary m URI
buildAuthorizationUrl = do
  conf <- ask
  return $ O.buildAuthorizationUrl (cred conf) (threeLegged conf)

requestPermanentToken
  :: MonadIO m => C.Manager -> O.Verifier ->
     OAuthT O.Temporary m (C.Response (Either SL.ByteString (O.Token O.Permanent)))
requestPermanentToken man ver =
  liftBasic $ \gen conf ->
    O.requestPermanentToken (cred conf)
                            (server conf)
                            ver
                            (threeLegged conf)
                            man
                            gen

data TokenRequestFailure =
    OnTemporaryRequest C.HttpException
  | BadTemporaryToken SL.ByteString
  | OnPermanentRequest C.HttpException
  | BadPermanentToken SL.ByteString
  deriving ( Show )

-- | Run a full Three-legged authorization protocol using the simple interface
-- of this module. This is similar to the 'O.requestTokenProtocol' in
-- "Network.OAuth.ThreeLegged", but offers better error handling due in part to
-- the easier management of configuration state.
requestTokenProtocol
  :: (Functor m, MonadIO m, E.MonadCatch m) =>
     C.Manager -> (URI -> m O.Verifier) ->
     OAuthT O.Client m (Either TokenRequestFailure (O.Cred O.Permanent))
requestTokenProtocol man getVerifier = runEitherT $ do
  -- Most of the code here is very simple, except that it does a LOT of
  -- exception lifting. Try to ignore the EitherT noise on the left side
  -- of each line.
  tempResp <- liftE OnTemporaryRequest $ E.try (requestTemporaryToken man)
  ttok     <- upE BadTemporaryToken $ C.responseBody tempResp
  upgradeE ttok $ do
    verifier <- lift $ buildAuthorizationUrl >>= lift . getVerifier
    permResp <- liftE OnPermanentRequest $ E.try (requestPermanentToken man verifier)
    ptok     <- upE BadPermanentToken $ C.responseBody permResp 
    lift $ upgradeCred ptok
  where
    -- These functions explain most of the EitherT noise. They're largely
    -- useful for lifting default EitherT responses up into the error type
    -- we want.
    mapE     :: Functor m => (e -> f) -> EitherT e m b -> EitherT f m b
    mapE f = bimapEitherT f id
    liftE    :: Functor m => (e -> f) -> m (Either e b) -> EitherT f m b
    liftE f = mapE f . EitherT
    upE      :: (Monad m, Functor m) => (e -> f) -> Either e b -> EitherT f m b
    upE f = liftE f . return
    -- This is just 'upgrade' played out in the EitherT monad.
    upgradeE :: (Monad m, Cred.ResourceToken ty') =>
                Cred.Token ty'
                -> EitherT e (OAuthT ty' m) a -> EitherT e (OAuthT ty m) a
    upgradeE tok = EitherT . upgrade tok . runEitherT