{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE MultiWayIf          #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies        #-}
{-# LANGUAGE TypeOperators       #-}

-- |
-- Module      : Data.DynamoDb
-- License     : BSD-style
--
-- Maintainer  : palkovsky.ondrej@gmail.com
-- Stability   : experimental
-- Portability : portable
--
-- Type-safe library for accessing DynamoDB database.
--
module Database.DynamoDB (
    -- * Introduction
    -- $intro

    -- * Proxies
    -- $proxy

    -- * Lens support
    -- $lens

    -- * Conversion support
    -- $conversion

    -- * Data types
    DynamoException(..)
  , Consistency(..)
  , Direction(..)
  , Column
    -- * Attribute path combinators
    , (<.>), (<!>), (<!:>)
    -- * Fetching items
  , getItem
  , getItemBatch
    -- * Query options
  , QueryOpts
  , queryOpts
  , qConsistentRead, qStartKey, qDirection, qFilterCondition, qHashKey, qRangeCondition, qLimit
    -- * Performing query
  , query
  , querySimple
  , queryCond
  , querySource
  , querySourceChunks
  , querySourceByKey
  , queryOverIndex
    -- * Scan options
  , ScanOpts
  , scanOpts
  , sFilterCondition, sConsistentRead, sLimit, sParallel, sStartKey
    -- * Performing scan
  , scan
  , scanSource
  , scanSourceChunks
  , scanCond
    -- * Helper conduits
  , leftJoin
  , innerJoin
    -- * Data entry
  , putItem
  , putItemBatch
  , insertItem
    -- * Data modification
  , updateItemByKey
  , updateItemByKey_
  , updateItemCond_
    -- * Deleting data
  , deleteItemByKey
  , deleteItemCondByKey
  , deleteItemBatchByKey
    -- * Delete table
  , deleteTable
    -- * Utility functions
  , tableKey
    -- * Typeclasses
  , DynamoTable
  , DynamoIndex
  , PrimaryKey
  , ContainsTableKey
  , CanQuery
  , TableScan
) where

import           Control.Lens                        ((%~), (.~), (^.))
import           Control.Monad                       (void)
import           Control.Monad.Catch                 (throwM)
import           Data.Bool                           (bool)
import           Data.Function                       ((&))
import           Data.Proxy
import           Data.Semigroup                      ((<>))
import           Network.AWS
import qualified Network.AWS.DynamoDB.DeleteItem     as D
import qualified Network.AWS.DynamoDB.GetItem        as D
import qualified Network.AWS.DynamoDB.PutItem        as D
import qualified Network.AWS.DynamoDB.UpdateItem     as D
import qualified Network.AWS.DynamoDB.DeleteTable    as D
import qualified Network.AWS.DynamoDB.Types    as D

import           Database.DynamoDB.Class
import           Database.DynamoDB.Filter
import           Database.DynamoDB.Internal
import           Database.DynamoDB.Types
import           Database.DynamoDB.Update
import           Database.DynamoDB.BatchRequest
import           Database.DynamoDB.QueryRequest


dDeleteItem :: DynamoTable a r => Proxy a -> PrimaryKey a r -> D.DeleteItem
dDeleteItem p pkey = D.deleteItem (tableName p) & D.diKey .~ dKeyToAttr p pkey

dGetItem :: DynamoTable a r => Proxy a -> PrimaryKey a r -> D.GetItem
dGetItem p pkey = D.getItem (tableName p) & D.giKey .~ dKeyToAttr p pkey

-- | Write item into the database; overwrite any previously existing item with the same primary key.
putItem :: (MonadAWS m, DynamoTable a r) => a -> m ()
putItem item = void $ send (dPutItem item)

-- | Write item into the database only if it doesn't already exist.
insertItem  :: forall a r m. (MonadAWS m, DynamoTable a r) => a -> m ()
insertItem item = do
  let keyfields = primaryFields (Proxy :: Proxy a)
      -- Create condition attribute_not_exist(hash_key)
      pkeyMissing = (AttrMissing . nameGenPath . pure . IntraName) $ head keyfields
      (expr, attnames, attvals) = dumpCondition pkeyMissing
      cmd = dPutItem item & D.piExpressionAttributeNames .~ attnames
                          & D.piConditionExpression .~ Just expr
                          & bool (D.piExpressionAttributeValues .~ attvals) id (null attvals) -- HACK; https://github.com/brendanhay/amazonka/issues/332
  void $ send cmd


-- | Read item from the database; primary key is either a hash key or (hash,range) tuple depending on the table.
getItem :: forall m a r. (MonadAWS m, DynamoTable a r) => Consistency -> Proxy a -> PrimaryKey a r -> m (Maybe a)
getItem consistency p key = do
  let cmd = dGetItem p key & D.giConsistentRead . consistencyL .~ consistency
  rs <- send cmd
  let result = rs ^. D.girsItem
  if | null result -> return Nothing
     | otherwise ->
          case dGsDecode result of
              Right res -> return (Just res)
              Left err -> throwM (DynamoException $ "Cannot decode item: " <> err)

-- | Delete item from the database by specifying the primary key.
deleteItemByKey :: forall m a r. (MonadAWS m, DynamoTable a r) => Proxy a -> PrimaryKey a r -> m ()
deleteItemByKey p pkey = void $ send (dDeleteItem p pkey)

-- | Delete item from the database by specifying the primary key and a condition.
-- Throws AWS exception if the condition does not succeed.
deleteItemCondByKey :: forall m a r.
    (MonadAWS m, DynamoTable a r) => Proxy a -> PrimaryKey a r -> FilterCondition a -> m ()
deleteItemCondByKey p pkey cond =
    let (expr, attnames, attvals) = dumpCondition cond
        cmd = dDeleteItem p pkey & D.diExpressionAttributeNames .~ attnames
                                 & bool (D.diExpressionAttributeValues .~ attvals) id (null attvals) -- HACK; https://github.com/brendanhay/amazonka/issues/332
                                 & D.diConditionExpression .~ Just expr
    in void (send cmd)

-- | Generate update item object; automatically adds condition for existence of primary
-- key, so that only existing objects are modified
dUpdateItem :: forall a r. DynamoTable a r
          => Proxy a -> PrimaryKey a r -> Action a -> Maybe (FilterCondition a) ->  Maybe D.UpdateItem
dUpdateItem p pkey actions mcond =
    genAction <$> dumpActions actions
  where
    keyfields = primaryFields p
        -- Create condition attribute_exists(hash_key)
    pkeyExists = (AttrExists . nameGenPath . pure . IntraName) (head keyfields)

    genAction actparams =
        D.updateItem (tableName p) & D.uiKey .~ dKeyToAttr p pkey
                                   & addActions actparams
                                   & addCondition (Just pkeyExists <> mcond)

    addActions (expr, attnames, attvals) =
          (D.uiUpdateExpression .~ Just expr)
            . (D.uiExpressionAttributeNames %~ (<> attnames))
            . bool (D.uiExpressionAttributeValues %~ (<> attvals)) id (null attvals)
    addCondition (Just cond) =
        let (expr, attnames, attvals) = dumpCondition cond
        in  (D.uiConditionExpression .~ Just expr)
            . (D.uiExpressionAttributeNames %~ (<> attnames))
            . bool (D.uiExpressionAttributeValues %~ (<> attvals)) id (null attvals) -- HACK; https://github.com/brendanhay/amazonka/issues/332
    addCondition Nothing = id -- Cannot happen anyway


-- | Update item in a table.
--
-- > updateItem (Proxy :: Proxy Test) (12, "2") (colCount +=. 100)
updateItemByKey_ :: forall a m r.
      (MonadAWS m, DynamoTable a r) => Proxy a -> PrimaryKey a r -> Action a -> m ()
updateItemByKey_ p pkey actions
  | Just cmd <- dUpdateItem p pkey actions Nothing = void $ send cmd
  | otherwise = return ()

-- | Update item in a database, return an updated version of the item.
updateItemByKey :: forall a m r.
      (MonadAWS m, DynamoTable a r) => Proxy a -> PrimaryKey a r -> Action a -> m a
updateItemByKey p pkey actions
  | Just cmd <- dUpdateItem p pkey actions Nothing = do
        rs <- send (cmd & D.uiReturnValues .~ Just D.AllNew)
        case dGsDecode (rs ^. D.uirsAttributes) of
            Right res -> return res
            Left err -> throwM (DynamoException $ "Cannot decode item: " <> err)
  | otherwise = do
      rs <- getItem Strongly p pkey
      case rs of
          Just res -> return res
          Nothing -> throwM (DynamoException "Cannot decode item.")

-- | Update item in a table while specifying a condition.
updateItemCond_ :: forall a m r. (MonadAWS m, DynamoTable a r)
    => Proxy a -> PrimaryKey a r -> FilterCondition a -> Action a -> m ()
updateItemCond_ p pkey cond actions
  | Just cmd <- dUpdateItem p pkey actions (Just cond) = void $ send cmd
  | otherwise = return ()

-- | Delete a table from DynamoDB.
deleteTable :: (MonadAWS m, DynamoTable a r) => Proxy a -> m ()
deleteTable p = void $ send (D.deleteTable (tableName p))

-- | Extract primary key from a record.
--
-- You can use this on both main table or on index tables if they contain the primary key from
-- the main table. Table key is always projected to indexes anyway, so just define it in
-- every index.
tableKey :: forall a parent key. ContainsTableKey a parent key => a -> key
tableKey = dTableKey

-- $intro
--
-- This library is operated in the following way:
--
-- * Create instances for your custom types using "Database.DynamoDB.Types"
-- * Create ordinary datatypes with records
-- * Use functions from "Database.DynamoDB.TH" to derive appropriate instances
-- * Optionally call generated migration function to automatically create
--   tables and indices
-- * Call functions from this module to access the database
--
-- The library does its best to ensure that only correct DynamoDB
-- operations are allowed. There are some limitations of DynamoDB
-- regarding access to empty values, but the library takes care
-- of this reasonably well.
--
-- Example of use
--
-- You may need to set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment
-- variables.
--
-- @
-- data Test = Test {
--     category :: T.Text
--   , messageid :: T.Text
--   , subject :: T.Text
-- } deriving (Show)
-- mkTableDefs "migrate" (tableConfig "" (''Test, WithRange) [] [])
-- @
--
-- This code creates appropriate instances for the table and the columns. It creates
-- global variables `colCategory`, `colMessageid` and `colSubject` that can be used
-- in filtering conditions or update queries.
--
-- @
-- main = do
--    lgr <- newLogger Info stdout
--    env <- newEnv NorthVirginia Discover
--    -- Override, use DynamoDD on localhost
--    let dynamo = setEndpoint False "localhost" 8000 dynamoDB
--    let newenv = env & configure dynamo
--                     & set envLogger lgr
--    runResourceT $ runAWS newenv $ do
--        -- Create tables and indexes
--        migrate mempty Nothing
--        -- Save data to database
--        putItem (Test "news" "1-2-3-4" "New subject")
--        -- Fetch data given primary key
--        item <- getItem Eventually tTest ("news", "1-2-3-4")
--        liftIO $ print item       -- (item :: Maybe Test)
--        -- Scan data using filter condition, return 10 results
--        items <- scanCond tTest (subject' ==. "New subejct") 10
--        print items         -- (items :: [Test])
-- @
--
-- See examples/ and test/ directories for more detail examples.

-- $proxy
--
-- In order to avoid ambiguity errors, most API calls need a 'Proxy' argument
-- to find out on which table or index to operate. These proxies are automatically
-- generated as a name of type prepended with "t" for tables and "i" for indexes.
--
-- A proxy for table Test will have name tTest, for index TestIndex the name will
-- be iTestIndex.

-- $lens
--
-- If the field names in the table record start with an underscore, the lens
-- get automatically generated for accessing the fields. The lens are polymorphic,
-- you can use them to access the fields of both main table and all the indexes.
--
-- @
-- data Test = Test {
--     _messageid :: T.Text
--   , _category :: T.Text
--   , _subject :: T.Text
-- } deriving (Show)
-- data TestIndex = TestIndex {
--     i_category :: T.Text
--   , i_messageid :: T.Text
-- }
-- mkTableDefs "migrate" (tableConfig "" (''Test, WithRange) [(''TestIndex, NoRange)] [])
--
-- doWithTest :: Test -> ...
-- doWithTest item = (item ^. category) ...
--
-- doWithItemIdx :: TestIndex -> ..
-- getCategoryIdx item = (item ^. category) ...
-- @

-- $conversion
--
-- Given a type 'Test' and an index type 'TestIndex',
-- a function 'toTest' is created that converts from 'TestIndex' to 'Test'.
-- Such function is created only if 'TestIndex' projects
-- all fields from 'Test'.