{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}

module Opaleye.Trans.Exception
    ( OpaleyeT (..)
    , runOpaleyeT

    , -- * Transactions
      Transaction
    , transaction
    , run

    , -- * Queries
      query
    , queryFirst

    , -- * Inserts
      insert
    , insertMany
    , insertReturning
    , insertReturningFirst
    , insertManyReturning

    , -- * Updates
      update
    , updateReturning
    , updateReturningFirst

    , -- * Deletes
      delete

    , -- * Exceptions
      withExceptOpaleye
    , withExceptTrans

    , -- * Utilities
      withError
    , withoutError
    , liftError
    , withTrans
    , maybeError

    , -- * Reexports
      liftIO
    , MonadIO
    , ask
    , Int64

    , throwError
    , catchError
    ) where

import           Control.Exception               (catch, throw)
import           Control.Monad.Except            (ExceptT (..), MonadError,
                                                  catchError, runExceptT,
                                                  throwError, withExceptT)
import           Control.Monad.IO.Class          (MonadIO, liftIO)
import           Control.Monad.Reader            (MonadReader (..))
import           Control.Monad.Trans             (MonadTrans (..))
import           Control.Monad.Catch             (MonadCatch, MonadThrow)

import           Data.Profunctor.Product.Default (Default)

import           Database.PostgreSQL.Simple      (Connection)
import qualified Database.PostgreSQL.Simple      as PSQL

import           GHC.Int                         (Int64)

import           Opaleye

import qualified Opaleye.Trans                   as T


newtype OpaleyeT e m a = OpaleyeT { unOpaleyeT :: ExceptT e (T.OpaleyeT m) a }
    deriving ( Functor, Applicative, Monad, MonadIO, MonadReader Connection
             , MonadError e, MonadCatch, MonadThrow
             )


instance MonadTrans (OpaleyeT e) where
    lift = OpaleyeT . lift . lift


withOpaleye :: Monad m => T.OpaleyeT m a -> OpaleyeT e m a
withOpaleye = OpaleyeT . lift


-- | Given a 'Connection', run an 'OpaleyeT'
runOpaleyeT :: PSQL.Connection -> OpaleyeT e m a -> m (Either e a)
runOpaleyeT c = T.runOpaleyeT c . runExceptT . unOpaleyeT


withExceptOpaleye :: Functor m => (e -> e') -> OpaleyeT e m a -> OpaleyeT e' m a
withExceptOpaleye f = OpaleyeT . withExceptT f . unOpaleyeT


-- | Just like 'T.Transaction' only with exception handling
newtype Transaction e a = Transaction { unTransaction :: ExceptT e T.Transaction a }
    deriving (Functor, Applicative, Monad, MonadReader Connection, MonadError e)


withExceptTrans :: (e -> e') -> Transaction e a -> Transaction e' a
withExceptTrans f = Transaction . withExceptT f . unTransaction


withError :: Monad m => T.OpaleyeT m (Either e a) -> OpaleyeT e m a
withError f = withOpaleye f >>= either throwError return


withoutError :: Monad m => OpaleyeT e m a -> T.OpaleyeT m (Either e a)
withoutError = runExceptT . unOpaleyeT


liftError :: Monad m
          => (T.Transaction (Either e a) -> T.OpaleyeT m (Either r b))
          -> Transaction e a -> OpaleyeT r m b
liftError f = withError . f . runExceptT . unTransaction


-- | Run a postgresql transaction in the 'OpaleyeT' monad
transaction :: MonadIO m => Transaction e a -> OpaleyeT e m a
transaction = liftError T.transaction


-- | Execute a query without a literal transaction
run :: MonadIO m => Transaction e a -> OpaleyeT e m a
run = liftError T.run


withTrans :: T.Transaction a -> Transaction e a
withTrans = Transaction . lift


-- | Execute a 'Query'. See 'runQuery'.
query :: Default QueryRunner a b => Query a -> Transaction e [b]
query = withTrans . T.query


maybeError :: T.Transaction (Maybe b) -> e -> Transaction e b
maybeError f e = withTrans f >>= maybe (throwError e) return


-- | Retrieve the first result from a 'Query'. Similar to @listToMaybe <$> runQuery@.
queryFirst :: Default QueryRunner a b => e -> Query a -> Transaction e b
queryFirst e q = maybeError (T.queryFirst q) e


-- | Insert into a 'Table'. See 'runInsert'.
insert :: Table w r -> w -> Transaction e Int64
insert t = withTrans . T.insert t


-- | Insert many records into a 'Table'. See 'runInsertMany'.
insertMany :: Table w r -> [w] -> Transaction e Int64
insertMany t = withTrans . T.insertMany t


-- | Insert a record into a 'Table' with a return value. See 'runInsertReturning'.
insertReturning :: Default QueryRunner a b => Table w r -> (r -> a) -> w -> Transaction e [b]
insertReturning t ret = withTrans . T.insertReturning t ret


-- | Insert a record into a 'Table' with a return value. Retrieve only the first result.
-- Similar to @'listToMaybe' '<$>' 'insertReturning'@
insertReturningFirst :: Default QueryRunner a b => e -> Table w r -> (r -> a) -> w -> Transaction e b
insertReturningFirst e t ret w = maybeError (T.insertReturningFirst t ret w) e


-- | Insert many records into a 'Table' with a return value for each record.
--
-- Maybe not worth defining. This almost certainly does the wrong thing.
insertManyReturning :: Default QueryRunner a b => Table w r -> [w] -> (r -> a) -> Transaction e [b]
insertManyReturning t ws = withTrans . T.insertManyReturning t ws


-- | Update items in a 'Table' where the predicate is true. See 'runUpdate'.
update :: Table w r -> (r -> w) -> (r -> Column PGBool) -> Transaction e Int64
update t r2w = withTrans . T.update t r2w


-- | Update items in a 'Table' with a return value. See 'runUpdateReturning'.
updateReturning :: Default QueryRunner a b
                => Table w r
                -> (r -> w)
                -> (r -> Column PGBool)
                -> (r -> a)
                -> Transaction e [b]
updateReturning t r2w p = withTrans . T.updateReturning t r2w p


-- | Update items in a 'Table' with a return value. Similar to @'listToMaybe' '<$>' 'updateReturning'@.
updateReturningFirst :: Default QueryRunner a b
                     => e
                     -> Table w r
                     -> (r -> w)
                     -> (r -> Column PGBool)
                     -> (r -> a)
                     -> Transaction e b
updateReturningFirst e t r2w p r2r = maybeError (T.updateReturningFirst t r2w p r2r) e


-- | Delete items in a 'Table' that satisfy some boolean predicate. See 'runDelete'.
delete :: Table a b -> (b -> Column PGBool) -> Transaction e Int64
delete t = withTrans . T.delete t