{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeFamilies               #-}

-- |
-- Module      : Network.OAuth.Stateful
-- Copyright   : (c) Joseph Abrahamson 2013
-- License     : MIT
--
-- Maintainer  : me@jspha.com
-- Stability   : experimental
-- Portability : non-portable
--

module Network.OAuth.Stateful
(
  -- * An OAuth Monad Transformer
  OAuth, runOAuth,
  OAuthT, runOAuthT, runOAuthT',

  -- * Standard operations

  -- | These operations are similar to those exposed by
  -- "Network.OAuth.Types.Params" or "Network.OAuth.Signing" but use the
  -- OAuth monad state instead of needing manual threading.
  oauth, sign, newParams,

  -- * OAuth State
  withGen, withManager, withCred, getServer, getCredentials, 

  )
  where

import           Control.Applicative
import           Control.Monad.Catch
import           Control.Monad.State
import           Crypto.Random
import           Network.HTTP.Client.Types       (Request)
import           Network.OAuth.MuLens
import qualified Network.OAuth.Signing           as S
import           Network.OAuth.Types.Credentials (Cred, Token, clientCred,
                                                  clientToken, resourceToken)
import           Network.OAuth.Types.Params      (Server (..))
import qualified Network.OAuth.Types.Params      as P

import Network.HTTP.Client.Manager (Manager, ManagerSettings, closeManager,
                                    defaultManagerSettings, newManager)

-- | A simple monad suitable for basic OAuth requests.
newtype OAuthT ty m a =
  OAuthT { unOAuthT :: StateT (OAuthConfig ty) m a }
  deriving ( Functor, Applicative, Monad, MonadIO )

type OAuth ty a = OAuthT ty IO a

instance MonadTrans (OAuthT ty) where
    lift = OAuthT . lift

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

runOAuthT :: (MonadIO m, MonadCatch m) => Cred ty -> Server -> OAuthT ty m a -> m a
runOAuthT = runOAuthT' defaultManagerSettings

runOAuthT' :: (MonadIO m, MonadCatch m) => ManagerSettings -> Cred ty -> Server -> OAuthT ty m a -> m a
runOAuthT' settings creds srv m = do
  pool <- liftIO createEntropyPool
  bracket (liftIO $ newManager settings) (liftIO . closeManager) $ \man ->
    let conf = OAuthConfig man (cprgCreate pool) srv creds
    in  evalStateT (unOAuthT m) conf

-- | Generate default OAuth parameters and use them to sign a request. This
-- is the simplest OAuth method.
oauth :: MonadIO m => Request -> OAuthT ty m Request
oauth req = newParams >>= flip sign req

-- | 'OAuthT' retains a cryptographic random generator state.
withGen :: Monad m => (SystemRNG -> m (a, SystemRNG)) -> OAuthT ty m a
withGen = OAuthT . zoom crng . StateT

-- | 'OAuthT' retains a "Network.HTTP.Client" 'Manager'. The 'Manager' is
-- created at the beginning of an 'OAuthT' thread and destroyed at the end,
-- so it's efficient to pipeline many OAuth requests together.
withManager :: Monad m => (Manager -> m a) -> OAuthT ty m a
withManager f = OAuthT $ zoom manager (get >>= lift . f)

-- | Create a fresh set of parameters.
newParams :: MonadIO m => OAuthT ty m (P.Oa ty)
newParams = do
  px <- withGen (liftIO . P.freshPin)
  c  <- OAuthT $ use credentials
  return P.Oa { P.credentials = c
              , P.workflow    = P.Standard
              , P.pin         = px
              }

-- | Sign a request using a set of parameters, 'P.Oa'.
sign :: Monad m => P.Oa ty -> Request -> OAuthT ty m Request
sign oax req = do
  s <- OAuthT $ use server
  return (S.sign oax s req)

withCred :: Monad m => Cred ty -> OAuthT ty m a -> OAuthT ty' m a
withCred c op = OAuthT $ do
  s <- get
  lift $ evalStateT (unOAuthT op) (s & credentials .~ c)

data OAuthConfig ty =
  OAuthConfig {-# UNPACK #-} !Manager
              {-# UNPACK #-} !SystemRNG
              {-# UNPACK #-} !Server
              !(Cred ty)

getServer :: Monad m => OAuthT ty m Server
getServer = OAuthT (use server)

getCredentials :: Monad m => OAuthT ty m (Cred ty)
getCredentials = OAuthT (use credentials)

manager :: Lens (OAuthConfig ty) (OAuthConfig ty) Manager Manager
manager inj (OAuthConfig m rng sv c) = (\m' -> OAuthConfig m' rng sv c) <$> inj m
{-# INLINE manager #-}

crng :: Lens (OAuthConfig ty) (OAuthConfig ty) SystemRNG SystemRNG
crng inj (OAuthConfig m rng sv c) = (\rng' -> OAuthConfig m rng' sv c) <$> inj rng
{-# INLINE crng #-}

server :: Lens (OAuthConfig ty) (OAuthConfig ty) Server Server
server inj (OAuthConfig m rng sv c) = (\sv' -> OAuthConfig m rng sv' c) <$> inj sv
{-# INLINE server #-}

credentials :: Lens (OAuthConfig ty) (OAuthConfig ty') (Cred ty) (Cred ty')
credentials inj (OAuthConfig m rng sv c) = OAuthConfig m rng sv <$> inj c
{-# INLINE credentials #-}