module Hasura.GraphQL.Resolve.Mutation
  ( convertUpdate
  , convertUpdateByPk
  , convertDelete
  , convertDeleteByPk
  , resolveMutationFields
  , buildEmptyMutResp
  ) where

import           Data.Has
import           Hasura.Prelude

import qualified Control.Monad.Validate              as MV
import qualified Data.Aeson                          as J
import qualified Data.HashMap.Strict                 as Map
import qualified Data.HashMap.Strict.InsOrd          as OMap
import qualified Data.HashMap.Strict.InsOrd.Extended as OMap
import qualified Data.Sequence.NonEmpty              as NESeq
import qualified Data.Text                           as T
import qualified Language.GraphQL.Draft.Syntax       as G

import qualified Hasura.RQL.DML.Delete               as RD
import qualified Hasura.RQL.DML.Returning            as RR
import qualified Hasura.RQL.DML.Update               as RU

import qualified Hasura.RQL.DML.Select               as RS
import qualified Hasura.SQL.DML                      as S

import           Hasura.EncJSON
import           Hasura.GraphQL.Resolve.BoolExp
import           Hasura.GraphQL.Resolve.Context
import           Hasura.GraphQL.Resolve.InputValue
import           Hasura.GraphQL.Resolve.Select       (processTableSelectionSet)
import           Hasura.GraphQL.Validate.Field
import           Hasura.GraphQL.Validate.Types
import           Hasura.RQL.DML.Internal             (currentSession, sessVarFromCurrentSetting)
import           Hasura.RQL.Types
import           Hasura.SQL.Types
import           Hasura.SQL.Value

resolveMutationFields
  :: ( MonadReusability m, MonadError QErr m, MonadReader r m, Has FieldMap r
     , Has OrdByCtx r, Has SQLGenCtx r
     )
  => G.NamedType -> SelSet -> m (RR.MutFldsG UnresolvedVal)
resolveMutationFields ty selSet = fmap (map (first FieldName)) $
  withSelSet selSet $ \fld -> case _fName fld of
    "__typename"    -> return $ RR.MExp $ G.unName $ G.unNamedType ty
    "affected_rows" -> return RR.MCount
    "returning"     -> do
      annFlds <- processTableSelectionSet (_fType fld) $ _fSelSet fld
      annFldsResolved <- traverse
        (traverse (RS.traverseAnnFld convertPGValueToTextValue)) annFlds
      return $ RR.MRet annFldsResolved
    G.Name t        -> throw500 $ "unexpected field in mutation resp : " <> t
  where
    convertPGValueToTextValue = \case
      UVPG annPGVal -> UVSQL <$> txtConverter annPGVal
      UVSessVar colTy sessVar -> pure $ UVSessVar colTy sessVar
      UVSQL sqlExp -> pure $ UVSQL sqlExp
      UVSession    -> pure UVSession

convertRowObj
  :: (MonadReusability m, MonadError QErr m)
  => PGColGNameMap
  -> AnnInpVal
  -> m [(PGCol, UnresolvedVal)]
convertRowObj colGNameMap val =
  flip withObject val $ \_ obj ->
  forM (OMap.toList obj) $ \(k, v) -> do
    prepExpM <- fmap mkParameterizablePGValue <$> asPGColumnValueM v
    pgCol <- pgiColumn <$> resolvePGCol colGNameMap k
    let prepExp = fromMaybe (UVSQL S.SENull) prepExpM
    return (pgCol, prepExp)

type ApplySQLOp =  (PGCol, S.SQLExp) -> S.SQLExp

rhsExpOp :: S.SQLOp -> S.TypeAnn -> ApplySQLOp
rhsExpOp op annTy (col, e) =
  S.mkSQLOpExp op (S.SEIden $ toIden col) annExp
  where
    annExp = S.SETyAnn e annTy

lhsExpOp :: S.SQLOp -> S.TypeAnn -> ApplySQLOp
lhsExpOp op annTy (col, e) =
  S.mkSQLOpExp op annExp $ S.SEIden $ toIden col
  where
    annExp = S.SETyAnn e annTy

convObjWithOp
  :: (MonadReusability m, MonadError QErr m)
  => PGColGNameMap -> ApplySQLOp -> AnnInpVal -> m [(PGCol, UnresolvedVal)]
convObjWithOp colGNameMap opFn val =
  flip withObject val $ \_ obj -> forM (OMap.toList obj) $ \(k, v) -> do
  colVal <- openOpaqueValue =<< asPGColumnValue v
  pgCol <- pgiColumn <$> resolvePGCol colGNameMap k
  -- TODO: why are we using txtEncoder here?
  let encVal = txtEncoder $ pstValue $ _apvValue colVal
      sqlExp = opFn (pgCol, encVal)
  return (pgCol, UVSQL sqlExp)

convDeleteAtPathObj
  :: (MonadReusability m, MonadError QErr m)
  => PGColGNameMap -> AnnInpVal -> m [(PGCol, UnresolvedVal)]
convDeleteAtPathObj colGNameMap val =
  flip withObject val $ \_ obj -> forM (OMap.toList obj) $ \(k, v) -> do
    vals <- traverse (openOpaqueValue <=< asPGColumnValue) =<< asArray v
    pgCol <- pgiColumn <$> resolvePGCol colGNameMap k
    let valExps = map (txtEncoder . pstValue . _apvValue) vals
        annEncVal = S.SETyAnn (S.SEArray valExps) S.textArrTypeAnn
        sqlExp = S.SEOpApp S.jsonbDeleteAtPathOp
                 [S.SEIden $ toIden pgCol, annEncVal]
    return (pgCol, UVSQL sqlExp)

convertUpdateP1
  :: (MonadReusability m, MonadError QErr m)
  => UpdOpCtx -- the update context
  -> (ArgsMap -> m AnnBoolExpUnresolved) -- the bool expression parser
  -> (Field -> m (RR.MutationOutputG UnresolvedVal)) -- the selection set resolver
  -> Field -- the mutation field
  -> m (RU.AnnUpdG UnresolvedVal)
convertUpdateP1 opCtx boolExpParser selectionResolver fld = do
  -- a set expression is same as a row object
  setExpM   <- resolveUpdateOperator "_set" $ convertRowObj colGNameMap
  -- where bool expression to filter column
  whereExp <- boolExpParser args
  -- increment operator on integer columns
  incExpM <- resolveUpdateOperator "_inc" $
    convObjWithOp' $ rhsExpOp S.incOp S.intTypeAnn
  -- append jsonb value
  appendExpM <- resolveUpdateOperator "_append" $
    convObjWithOp' $ rhsExpOp S.jsonbConcatOp S.jsonbTypeAnn
  -- prepend jsonb value
  prependExpM <- resolveUpdateOperator "_prepend" $
    convObjWithOp' $ lhsExpOp S.jsonbConcatOp S.jsonbTypeAnn
  -- delete a key in jsonb object
  deleteKeyExpM <- resolveUpdateOperator "_delete_key" $
    convObjWithOp' $ rhsExpOp S.jsonbDeleteOp S.textTypeAnn
  -- delete an element in jsonb array
  deleteElemExpM <- resolveUpdateOperator "_delete_elem" $
    convObjWithOp' $ rhsExpOp S.jsonbDeleteOp S.intTypeAnn
  -- delete at path in jsonb value
  deleteAtPathExpM <- resolveUpdateOperator "_delete_at_path" $
    convDeleteAtPathObj colGNameMap

  updateItems <- combineUpdateExpressions
                 [ setExpM, incExpM, appendExpM, prependExpM
                 , deleteKeyExpM, deleteElemExpM, deleteAtPathExpM
                 ]

  mutOutput <- selectionResolver fld

  pure $ RU.AnnUpd tn updateItems (unresolvedPermFilter, whereExp) unresolvedPermCheck mutOutput allCols
  where
    convObjWithOp' = convObjWithOp colGNameMap
    allCols = Map.elems colGNameMap
    UpdOpCtx tn _ colGNameMap filterExp checkExpr preSetCols = opCtx
    args = _fArguments fld
    resolvedPreSetItems = Map.toList $ fmap partialSQLExpToUnresolvedVal preSetCols
    unresolvedPermFilter = fmapAnnBoolExp partialSQLExpToUnresolvedVal filterExp
    unresolvedPermCheck = maybe annBoolExpTrue (fmapAnnBoolExp partialSQLExpToUnresolvedVal) checkExpr

    resolveUpdateOperator operator resolveAction =
      (operator,) <$> withArgM args operator resolveAction

    combineUpdateExpressions updateExps = do
      let allOperatorNames = map fst updateExps
          updateItems = mapMaybe (\(op, itemsM) -> (op,) <$> itemsM) updateExps
      -- Atleast any one of operator is expected or preset expressions shouldn't be empty
      if null updateItems && null resolvedPreSetItems then
        throwVE $ "atleast any one of " <> showNames allOperatorNames <> " is expected"
      else do
        let itemsWithOps = concatMap (\(op, items) -> map (second (op,)) items) updateItems
            validateMultiOps col items = do
              when (length items > 1) $ MV.dispute [(col, map fst $ toList items)]
              pure $ snd $ NESeq.head items
            eitherResult = MV.runValidate $ OMap.traverseWithKey validateMultiOps $
                           OMap.groupTuples itemsWithOps
        case eitherResult of
          -- A column shouldn't be present in more than one operator.
          -- If present, then generated UPDATE statement throws unexpected query error
          Left columnsWithMultiOps -> throwVE $
                                      "column found in multiple operators; "
                                      <> T.intercalate ". "
                                      (map (\(col, ops) -> col <<> " in " <> showNames ops)
                                       columnsWithMultiOps)
          Right items -> pure $ resolvedPreSetItems <> OMap.toList items

convertUpdateGeneric
  :: ( MonadReusability m, MonadError QErr m
     , MonadReader r m
     , Has SQLGenCtx r
     )
  => UpdOpCtx -- the update context
  -> (ArgsMap -> m AnnBoolExpUnresolved) -- the bool exp parser
  -> (Field -> m (RR.MutationOutputG UnresolvedVal)) -- the selection set resolver
  -> Field
  -> m RespTx
convertUpdateGeneric opCtx boolExpParser selectionResolver fld = do
  annUpdUnresolved <- convertUpdateP1 opCtx boolExpParser selectionResolver fld
  (annUpdResolved, prepArgs) <- withPrepArgs $ RU.traverseAnnUpd
                                resolveValPrep annUpdUnresolved
  strfyNum <- stringifyNum <$> asks getter
  let whenNonEmptyItems = return $ RU.updateQueryToTx strfyNum
                          (annUpdResolved, prepArgs)
      whenEmptyItems    = return $ return $
                          buildEmptyMutResp $ RU.uqp1Output annUpdResolved
   -- if there are not set items then do not perform
   -- update and return empty mutation response
  bool whenNonEmptyItems whenEmptyItems $ null $ RU.uqp1SetExps annUpdResolved

convertUpdate
  :: ( MonadReusability m, MonadError QErr m
     , MonadReader r m, Has FieldMap r
     , Has OrdByCtx r, Has SQLGenCtx r
     )
  => UpdOpCtx -- the update context
  -> Field -- the mutation field
  -> m RespTx
convertUpdate opCtx =
  convertUpdateGeneric opCtx whereExpressionParser mutationFieldsResolver

convertUpdateByPk
  :: ( MonadReusability m, MonadError QErr m
     , MonadReader r m, Has FieldMap r
     , Has OrdByCtx r, Has SQLGenCtx r
     )
  => UpdOpCtx -- the update context
  -> Field -- the mutation field
  -> m RespTx
convertUpdateByPk opCtx field =
  convertUpdateGeneric opCtx boolExpParser tableSelectionAsMutationOutput field
  where
    boolExpParser args =  withArg args "pk_columns" $ \inpVal -> do
      obj <- asObject inpVal
      pgColValToBoolExp (_uocAllCols opCtx) $ Map.fromList $ OMap.toList obj


convertDeleteGeneric
  :: ( MonadReusability m
     , MonadReader r m
     , Has SQLGenCtx r
     )
  => DelOpCtx -- the delete context
  -> (ArgsMap -> m AnnBoolExpUnresolved) -- the bool exp parser
  -> (Field -> m (RR.MutationOutputG UnresolvedVal)) -- the selection set resolver
  -> Field -- the mutation field
  -> m RespTx
convertDeleteGeneric opCtx boolExpParser selectionResolver fld = do
  whereExp <- boolExpParser $ _fArguments fld
  mutOutput  <- selectionResolver fld
  let unresolvedPermFltr =
        fmapAnnBoolExp partialSQLExpToUnresolvedVal filterExp
      annDelUnresolved = RD.AnnDel tn (unresolvedPermFltr, whereExp)
                         mutOutput allCols
  (annDelResolved, prepArgs) <- withPrepArgs $ RD.traverseAnnDel
                                resolveValPrep annDelUnresolved
  strfyNum <- stringifyNum <$> asks getter
  return $ RD.deleteQueryToTx strfyNum (annDelResolved, prepArgs)
  where
    DelOpCtx tn _ colGNameMap filterExp = opCtx
    allCols = Map.elems colGNameMap

convertDelete
  :: ( MonadReusability m, MonadError QErr m
     , MonadReader r m, Has FieldMap r
     , Has OrdByCtx r, Has SQLGenCtx r
     )
  => DelOpCtx -- the delete context
  -> Field -- the mutation field
  -> m RespTx
convertDelete opCtx =
  convertDeleteGeneric opCtx whereExpressionParser mutationFieldsResolver

convertDeleteByPk
  :: ( MonadReusability m, MonadError QErr m
     , MonadReader r m, Has FieldMap r
     , Has OrdByCtx r, Has SQLGenCtx r
     )
  => DelOpCtx -- the delete context
  -> Field -- the mutation field
  -> m RespTx
convertDeleteByPk opCtx field =
  convertDeleteGeneric opCtx boolExpParser tableSelectionAsMutationOutput field
  where
    boolExpParser =  pgColValToBoolExp (_docAllCols opCtx)

whereExpressionParser
  :: ( MonadReusability m, MonadError QErr m
     , MonadReader r m, Has FieldMap r
     )
  => ArgsMap -> m AnnBoolExpUnresolved
whereExpressionParser args = withArg args "where" parseBoolExp

mutationFieldsResolver
  :: ( MonadReusability m, MonadError QErr m
     , MonadReader r m, Has FieldMap r
     , Has OrdByCtx r, Has SQLGenCtx r
     )
  => Field -> m (RR.MutationOutputG UnresolvedVal)
mutationFieldsResolver field =
  RR.MOutMultirowFields <$> resolveMutationFields (_fType field) (_fSelSet field)

tableSelectionAsMutationOutput
  :: ( MonadReusability m, MonadError QErr m
     , MonadReader r m, Has FieldMap r
     , Has OrdByCtx r, Has SQLGenCtx r
     )
  => Field -> m (RR.MutationOutputG UnresolvedVal)
tableSelectionAsMutationOutput field =
  RR.MOutSinglerowObject <$> processTableSelectionSet (_fType field) (_fSelSet field)

-- | build mutation response for empty objects
buildEmptyMutResp :: RR.MutationOutput -> EncJSON
buildEmptyMutResp = mkTx
  where
    mkTx = \case
      RR.MOutMultirowFields mutFlds -> encJFromJValue $ OMap.fromList $ map (second convMutFld) mutFlds
      RR.MOutSinglerowObject _       -> encJFromJValue $ J.Object mempty
    -- generate empty mutation response
    convMutFld = \case
      RR.MCount -> J.toJSON (0 :: Int)
      RR.MExp e -> J.toJSON e
      RR.MRet _ -> J.toJSON ([] :: [J.Value])

resolveValPrep
  :: (MonadState PrepArgs m)
  => UnresolvedVal -> m S.SQLExp
resolveValPrep = \case
  UVPG annPGVal -> prepare annPGVal
  UVSessVar colTy sessVar -> sessVarFromCurrentSetting colTy sessVar
  UVSQL sqlExp -> pure sqlExp
  UVSession -> pure currentSession