-- |
-- This module contains everything required 
-- to use \"hasql\" with Postgres.
-- For information on how it should be used consult the \"hasql\" docs.
-- 
-- Please note that there is a few limitations inflicted by Postgres,
-- encoding which in the type system would seriously burden the API,
-- so it was decided to make it the user's responsibility 
-- to make sure that certain conditions are satisfied during the runtime.
-- Particularly this concerns the 'Bknd.CxValue' instances of 
-- @Maybe@, @[]@ and @Vector@.
-- For details consult the docs on those instances.
-- 
module Hasql.Postgres 
(
  Postgres,
  Connector.Settings(..),
  CxError(..),
  TxError(..),
  Unknown(..),
)
where

import Hasql.Postgres.Prelude
import qualified Database.PostgreSQL.LibPQ as PQ
import qualified Hasql.Backend as Bknd
import qualified Hasql.Postgres.Connector as Connector
import qualified Hasql.Postgres.Statement as Statement
import qualified Hasql.Postgres.PTI as PTI
import qualified Hasql.Postgres.Mapping as Mapping
import qualified Hasql.Postgres.Session.Transaction as Transaction
import qualified Hasql.Postgres.Session.Execution as Execution
import qualified Hasql.Postgres.Session.ResultProcessing as ResultProcessing
import qualified Language.Haskell.TH as TH
import qualified Data.Text.Encoding as Text
import qualified Data.Vector as Vector
import qualified Data.Aeson as J
import qualified ListT


-- |
-- A connection to PostgreSQL.
data Postgres = 
  Postgres {
    connection :: !PQ.Connection,
    executionEnv :: !Execution.Env,
    transactionEnv :: !Transaction.Env,
    mappingEnv :: !Mapping.Environment
  }

data CxError =
  -- | 
  -- Impossible to connect. 
  -- A clarification might be given in the attached byte string.
  CantConnect (Maybe ByteString) |
  -- | 
  -- Server is running an unsupported version of Postgres.
  -- The parameter is the version in such a format,
  -- where a value @80105@ identifies a version @8.1.5@.
  UnsupportedVersion Int
  deriving (Show, Eq)


instance Bknd.Cx Postgres where
  type CxSettings Postgres =
    Connector.Settings
  type CxError Postgres =
    CxError
  acquireCx settings =
    runEitherT $ do
      c <- EitherT $ fmap (mapLeft connectorErrorMapping) $ Connector.open settings
      lift $ do
        e <- Execution.newEnv c
        Postgres <$> pure c <*> pure e <*> Transaction.newEnv e <*> getIntegerDatetimes c
    where
      getIntegerDatetimes c =
        fmap decodeValue $ PQ.parameterStatus c "integer_datetimes"
        where
          decodeValue = 
            \case
              Just "on" -> True
              _ -> False
      connectorErrorMapping =
        \case
          Connector.BadStatus x -> CantConnect x
          Connector.UnsupportedVersion x -> UnsupportedVersion x
  releaseCx =
    PQ.finish . connection


-- * Transactions
-------------------------

data TxError =
  -- |
  -- Received no response from the database.
  NoResult !(Maybe ByteString) |
  -- | 
  -- An error reported by the DB. Code, message, details, hint.
  -- 
  -- * The SQLSTATE code for the error. The SQLSTATE code identifies the type of error that has occurred; it can be used by front-end applications to perform specific operations (such as error handling) in response to a particular database error. For a list of the possible SQLSTATE codes, see Appendix A. This field is not localizable, and is always present.
  -- * The primary human-readable error message (typically one line). Always present.
  -- * Detail: an optional secondary error message carrying more detail about the problem. Might run to multiple lines.
  -- * Hint: an optional suggestion what to do about the problem. This is intended to differ from detail in that it offers advice (potentially inappropriate) rather than hard facts. Might run to multiple lines.
  ErroneousResult !ByteString !ByteString !(Maybe ByteString) !(Maybe ByteString) |
  -- |
  -- The database returned an unexpected result.
  -- Indicates an improper statement or a schema mismatch.
  UnexpectedResult !Text |
  -- |
  -- An attempt to perform an action, 
  -- which requires a transaction context, without one.
  -- 
  -- Currently it's only raised when trying to stream
  -- without establishing a transaction.
  NotInTransaction
  deriving (Show, Eq)


instance Bknd.CxTx Postgres where
  type TxError Postgres =
    TxError
  runTx p mode =
    runEitherT . runMaybeT . flip runReaderT p . inTransaction mode . interpretTx 


type Interpreter a =
  ReaderT Postgres (MaybeT (EitherT TxError IO)) a

liftExecution :: Execution.M a -> Interpreter a
liftExecution m =
  do
    r <- ReaderT $ \p -> liftIO $ Execution.run (executionEnv p) m
    either throwResultProcessingError return r

liftTransaction :: Transaction.M a -> Interpreter a
liftTransaction m =
  do
    r <- ReaderT $ \p -> liftIO $ Transaction.run (transactionEnv p) m
    either throwTransactionError return r
  where
    throwTransactionError =
      \case
        Transaction.NotInTransaction -> lift $ lift $ left $ NotInTransaction
        Transaction.ResultProcessingError a -> throwResultProcessingError a

throwResultProcessingError :: ResultProcessing.Error -> Interpreter a
throwResultProcessingError =
  \case
    ResultProcessing.NoResult a -> lift $ lift $ left $ NoResult a
    ResultProcessing.ErroneousResult a b c d -> lift $ lift $ left $ ErroneousResult a b c d
    ResultProcessing.UnexpectedResult a -> lift $ lift $ left $ UnexpectedResult a
    ResultProcessing.TransactionConflict -> lift $ mzero

convertStatement :: Bknd.Stmt Postgres -> Interpreter Statement.Statement
convertStatement s =
  asks $ \p -> 
    let
      liftParam (StmtParam o f) = 
        (,) o ((,) <$> f (mappingEnv p) <*> pure PQ.Binary)
      in 
        Statement.Statement
          (Statement.UnicodeTemplate (Bknd.stmtTemplate s))
          (toList $ fmap liftParam $ Bknd.stmtParams s)
          (Bknd.stmtPreparable s)

interpretTx :: Bknd.Tx Postgres a -> Interpreter a
interpretTx =
  iterTM $ \case
    Bknd.UnitTx stmt next -> do
      stmt' <- convertStatement stmt
      liftExecution $ Execution.unitResult =<< Execution.statement stmt'
      next
    Bknd.CountTx stmt next -> do
      stmt' <- convertStatement stmt
      r <- liftExecution $ Execution.countResult =<< Execution.statement stmt'
      next $ r
    Bknd.VectorTx stmt next -> do
      stmt' <- convertStatement stmt
      r <- liftExecution $ Execution.vectorResult =<< Execution.statement stmt'
      r' <- 
        asks $ \p -> 
          (fmap . fmap) (ResultValue (mappingEnv p)) $ r
      next r'
    Bknd.StreamTx batching stmt next -> do
      stmt' <- convertStatement stmt
      r <- liftTransaction $ Transaction.streamWithCursor batching stmt'
      r' <- 
        asks $ \p -> 
          (fmap . fmap) (ResultValue (mappingEnv p)) $
          hoist (lift . flip runReaderT p . liftTransaction) $
          r
      next r'

inTransaction :: Bknd.TxMode -> Interpreter a -> Interpreter a
inTransaction mode m =
  do
    liftTransaction $ beginTransaction
    result <- ReaderT $ \p -> lift $ lift $ runEitherT $ runMaybeT $ flip runReaderT p $ m
    case result of
      Left e -> do
        liftTransaction $ finishTransaction False
        lift $ lift $ left $ e
      Right Nothing -> do
        liftTransaction $ finishTransaction False
        mzero
      Right (Just r) -> do
        liftTransaction $ finishTransaction True
        return r
  where
    (,) beginTransaction finishTransaction =
      case mode of
        Nothing -> 
          (,) (return ()) 
              (const (return ()))
        Just (isolation, Nothing) -> 
          (,) (Transaction.beginTransaction (convertIsolation isolation, False))
              (Transaction.finishTransaction)
        Just (isolation, Just commit) ->
          (,) (Transaction.beginTransaction (convertIsolation isolation, True))
              (\commit' -> Transaction.finishTransaction (commit && commit'))
      where
        convertIsolation =
          \case
            Bknd.Serializable    -> Statement.Serializable
            Bknd.RepeatableReads -> Statement.RepeatableRead
            Bknd.ReadCommitted   -> Statement.ReadCommitted
            Bknd.ReadUncommitted -> Statement.ReadCommitted


-- * Mappings
-------------------------
-- Not using TH to generate instances
-- to be able to document them.
-------------------------

data instance Bknd.ResultValue Postgres =
  ResultValue !Mapping.Environment !(Maybe ByteString)

data instance Bknd.StmtParam Postgres =
  StmtParam !PQ.Oid !(Mapping.Environment -> Maybe ByteString)


{-# INLINE encodeValueUsingMapping #-}
encodeValueUsingMapping :: Mapping.Mapping a => a -> Bknd.StmtParam Postgres
encodeValueUsingMapping x = 
  StmtParam
    (PTI.oidPQ $ Mapping.oid x)
    (flip Mapping.encode x)

{-# INLINE decodeValueUsingMapping #-}
decodeValueUsingMapping :: Mapping.Mapping a => Bknd.ResultValue Postgres -> Either Text a
decodeValueUsingMapping (ResultValue e x) = 
  Mapping.decode e x

-- | 
-- Maps to the same type as the underlying value, 
-- encoding 'Nothing' as /NULL/.
-- 
-- /LIMITATION/
-- 
-- Multilevel 'Maybe's are not supported.
-- E.g., a value @Just Nothing@ of type @(Maybe (Maybe a))@ 
-- will be encoded the same way as @Nothing@.
instance Mapping.Mapping a => Bknd.CxValue Postgres (Maybe a) where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to Postgres arrays. 
-- 
-- /LIMITATION 1/
-- 
-- In multidimensional lists all rows of a dimension must have the same length.
-- 
-- E.g., the following is a corrupt value:
-- 
-- > [[1,2], [3]]
-- 
-- The following is a valid one:
-- 
-- > [[1,2], [3,4], [5,6]]
-- 
-- /LIMITATION 2/
-- 
-- 'Maybe' cannot be used to wrap an intermediate level in a multidimensional list.
-- 
-- E.g., the following is a corrupt type:
-- 
-- > [Maybe [a]]
-- 
-- However, both the first level list and the value are allowed to be wrapped in 'Maybe'.
-- So the following is a valid type:
-- 
-- > Maybe [[[Maybe a]]]
-- 
-- /NOTICE/
-- 
-- Also, please note that since 'String' is just an alias to @['Char']@,
-- it will be mapped to an array of characters. 
-- So if you want to map to a textual type use 'Text' instead.
-- 
instance (Mapping.Mapping a, Mapping.ArrayMapping a) => Bknd.CxValue Postgres [a] where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to Postgres' arrays.
-- 
-- Same rules as for the list instance apply. 
-- Consult its docs for details.
instance (Mapping.Mapping a, Mapping.ArrayMapping a) => Bknd.CxValue Postgres (Vector a) where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @int8@.
instance Bknd.CxValue Postgres Int where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @int2@.
instance Bknd.CxValue Postgres Int8 where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @int2@.
instance Bknd.CxValue Postgres Int16 where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @int4@.
instance Bknd.CxValue Postgres Int32 where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @int8@.
instance Bknd.CxValue Postgres Int64 where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @int8@.
instance Bknd.CxValue Postgres Word where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @int2@.
instance Bknd.CxValue Postgres Word8 where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @int2@.
instance Bknd.CxValue Postgres Word16 where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @int4@.
instance Bknd.CxValue Postgres Word32 where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @int8@.
instance Bknd.CxValue Postgres Word64 where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @float4@.
instance Bknd.CxValue Postgres Float where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @float8@.
instance Bknd.CxValue Postgres Double where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @numeric@.
instance Bknd.CxValue Postgres Scientific where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @date@.
instance Bknd.CxValue Postgres Day where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @time@.
instance Bknd.CxValue Postgres TimeOfDay where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @timetz@.
-- 
-- Unlike with @timestamptz@, 
-- Postgres does store the timezone information for @timetz@.
-- However the \"time\" library does not contain any composite type,
-- that fits the task, so we use a pair of 'TimeOfDay' and 'TimeZone'
-- to represent a value on the Haskell's side.
instance Bknd.CxValue Postgres (TimeOfDay, TimeZone) where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @timestamp@.
instance Bknd.CxValue Postgres LocalTime where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @timestamptz@.
-- 
-- /NOTICE/
-- 
-- Postgres does not store the timezone information of @timestamptz@.
-- Instead it stores a UTC value and performs silent conversions
-- to the currently set timezone, when dealt with in the text format.
-- However this library bypasses the silent conversions
-- and communicates with Postgres using the UTC values directly.
instance Bknd.CxValue Postgres UTCTime where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @interval@.
instance Bknd.CxValue Postgres DiffTime where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @char@.
-- Note that it supports UTF-8 values.
instance Bknd.CxValue Postgres Char where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @text@.
instance Bknd.CxValue Postgres Text where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @text@.
instance Bknd.CxValue Postgres LazyText where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @bytea@.
instance Bknd.CxValue Postgres ByteString where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @bytea@.
instance Bknd.CxValue Postgres LazyByteString where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @bool@.
instance Bknd.CxValue Postgres Bool where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @uuid@.
instance Bknd.CxValue Postgres UUID where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping

-- |
-- Maps to @json@.
-- 
-- Only works for PostgreSQL versions >= 9.2.
instance Bknd.CxValue Postgres J.Value where
  encodeValue = encodeValueUsingMapping
  decodeValue = decodeValueUsingMapping


-- ** Custom types
-------------------------

-- |
-- A wrapper around a 'ByteString',
-- which identifies the value with the PostgreSQL's \"unknown\" type,
-- thus leaving the choice of the type to Postgres.
-- The bytestring needs to be encoded according to the Postgres binary format
-- of the type it expects.
-- 
-- Essentially this is a low-level hook into the phases of encoding and decoding
-- of values with custom codecs.
-- <http://hackage.haskell.org/package/postgresql-binary The "postgresql-binary" library> 
-- is your toolchain when dealing with this type.
newtype Unknown = 
  Unknown ByteString

-- |
-- Maps to @unknown@.
instance Bknd.CxValue Postgres Unknown where
  encodeValue (Unknown x) = 
    StmtParam (PTI.oidPQ (PTI.ptiOID (PTI.unknown))) (const $ Just x)
  decodeValue (ResultValue _ x) = 
    maybe (Left "Decoding a NULL to Unknown") (Right . Unknown) x