module Database.PostgreSQL.Query.Functions
       ( -- * Raw query execution
         pgQuery
       , pgQueryWithMasker
       , pgExecute
       , pgExecuteWithMasker
         -- * Transactions
       , pgWithTransaction
       , pgWithSavepoint
       , pgWithTransactionMode
       , pgWithTransactionModeRetry
       , pgWithTransactionSerializable
         -- * Auxiliary
       , pgRepsertRow
       ) where

import Data.Int ( Int64 )
import Database.PostgreSQL.Query.Import
import Database.PostgreSQL.Query.Internal
import Database.PostgreSQL.Query.SqlBuilder
import Database.PostgreSQL.Query.TH
import Database.PostgreSQL.Query.Types
import Database.PostgreSQL.Simple
import Database.PostgreSQL.Simple.Transaction

import qualified Data.Text.Encoding as T

{- | Execute query generated by 'SqlBuilder'. Typical use case:

@
let userName = "Vovka Erohin" :: Text
pgQuery [sqlExp| SELECT id, name FROM users WHERE name = #{userName}|]
@

Or

@
let userName = "Vovka Erohin" :: Text
pgQuery $ Qp "SELECT id, name FROM users WHERE name = ?" [userName]
@

Which is almost the same. In both cases proper value escaping is performed
so you stay protected from sql injections.

-}

pgQuery
  :: (MonadPostgres m, ToSqlBuilder q, FromRow r, HasCallStack)
  => q
  -> m [r]
pgQuery = withFrozenCallStack $ pgQueryWithMasker defaultLogMasker

-- | Execute arbitrary query and return count of affected rows
pgExecute
  :: (MonadPostgres m, ToSqlBuilder q, HasCallStack)
  => q
  -> m Int64
pgExecute = withFrozenCallStack $ pgExecuteWithMasker defaultLogMasker

pgQueryWithMasker
  :: (MonadPostgres m, ToSqlBuilder q, FromRow r, HasCallStack)
  => LogMasker
  -> q
  -> m [r]
pgQueryWithMasker masker q = withFrozenCallStack $ withPGConnection $ \c -> do
    (queryBs, logBs) <- liftBase $ runSqlBuilder c masker $ toSqlBuilder q
    logDebug $ T.decodeUtf8 logBs
    liftBase $ query_ c queryBs

pgExecuteWithMasker
  :: (MonadPostgres m, ToSqlBuilder q, HasCallStack)
  => LogMasker
  -> q
  -> m Int64
pgExecuteWithMasker masker q = withFrozenCallStack $ withPGConnection $ \c -> do
    (queryBs, logBs) <- liftBase $ runSqlBuilder c masker $ toSqlBuilder q
    logDebug $ T.decodeUtf8 logBs
    liftBase $ execute_ c queryBs

-- | Execute all queries inside one transaction. Rollback transaction on exceptions
pgWithTransaction
  :: (HasPostgres m, MonadBaseControl IO m, TransactionSafe m, HasCallStack)
  => (HasCallStack => m a)
  -> m a
pgWithTransaction action = withPGConnection $ \con -> do
    control $ \runInIO -> do
        withTransaction con $ runInIO action

-- | Same as `pgWithTransaction` but executes queries inside savepoint
pgWithSavepoint
  :: (HasPostgres m, MonadBaseControl IO m, TransactionSafe m, HasCallStack)
  => (HasCallStack => m a)
  -> m a
pgWithSavepoint action = withPGConnection $ \con -> do
    control $ \runInIO -> do
        withSavepoint con $ runInIO action

-- | Wrapper for 'withTransactionMode': Execute an action inside a SQL
-- transaction with a given transaction mode.
pgWithTransactionMode
  :: (HasPostgres m, MonadBaseControl IO m, TransactionSafe m, HasCallStack)
  => TransactionMode
  -> (HasCallStack => m a)
  -> m a
pgWithTransactionMode tmode ma = withPGConnection $ \con -> do
    control $ \runInIO -> do
        withTransactionMode tmode con $ runInIO ma

-- | Wrapper for 'withTransactionModeRetry': Like 'pgWithTransactionMode',
-- but also takes a custom callback to determine if a transaction
-- should be retried if an SqlError occurs. If the callback returns
-- True, then the transaction will be retried. If the callback returns
-- False, or an exception other than an SqlError occurs then the
-- transaction will be rolled back and the exception rethrown.
pgWithTransactionModeRetry
  :: (HasPostgres m, MonadBaseControl IO m, TransactionSafe m, HasCallStack)
  => TransactionMode
  -> (SqlError -> Bool)
  -> (HasCallStack => m a)
  -> m a
pgWithTransactionModeRetry tmode epred ma = withPGConnection $ \con -> do
    control $ \runInIO -> do
        withTransactionModeRetry tmode epred con $ runInIO ma

-- | Wrapper for 'withTransactionSerializable': Execute an action
-- inside of a 'Serializable' transaction. If a serialization failure
-- occurs, roll back the transaction and try again. Be warned that
-- this may execute the IO action multiple times.
--
-- A Serializable transaction creates the illusion that your program
-- has exclusive access to the database. This means that, even in a
-- concurrent setting, you can perform queries in sequence without
-- having to worry about what might happen between one statement and
-- the next.
pgWithTransactionSerializable
  :: (HasPostgres m, MonadBaseControl IO m, TransactionSafe m)
  => (HasCallStack => m a)
  -> m a
pgWithTransactionSerializable ma = withPGConnection $ \con -> do
    control $ \runInIO -> do
        withTransactionSerializable con $ runInIO ma



{- | Perform repsert of the same row, first trying "update where" then
"insert" with concatenated fields. Which means that if you run

@
pgRepsertRow "emails" (MR [("user_id", mkValue uid)]) (MR [("email", mkValue email)])
@

Then firstly will be performed

@
UPDATE "emails" SET email = 'foo@bar.com' WHERE "user_id" = 1234
@

And if no one row is affected (which is returned by 'pgExecute'), then

@
INSERT INTO "emails" ("user_id", "email") VALUES (1234, 'foo@bar.com')
@

will be performed

-}

pgRepsertRow
  :: ( MonadPostgres m, MonadLogger m
     , ToMarkedRow wrow, ToMarkedRow urow, HasCallStack)
  => FN                         -- ^ Table name
  -> wrow                       -- ^ where condition
  -> urow                       -- ^ update row
  -> m ()
pgRepsertRow tname wrow urow = do
    let wmr = toMarkedRow wrow
    aff <- pgExecute $ updateTable tname urow
           [sqlExp|WHERE ^{mrToBuilder "AND" wmr}|]
    when (aff == 0) $ do
        let umr = toMarkedRow urow
            imr = wmr <> umr
        _ <- pgExecute $ insertInto tname imr
        return ()