{-# LANGUAGE ConstraintKinds     #-}
{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE NamedFieldPuns      #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications    #-}

module Data.Morpheus.Execution.Server.Resolve
  ( statelessResolver
  , byteStringIO
  , streamResolver
  , statefulResolver
  , RootResCon
  , fullSchema
  , coreResolver
  )
where

import           Data.Aeson                     ( encode )
import           Data.Aeson.Internal            ( formatError
                                                , ifromJSON
                                                )
import           Data.Aeson.Parser              ( eitherDecodeWith
                                                , jsonNoDup
                                                )
import qualified Data.ByteString.Lazy.Char8    as L
import           Data.Functor.Identity          ( Identity(..) )
import           Data.Proxy                     ( Proxy(..) )

-- MORPHEUS
import           Data.Morpheus.Error.Utils      ( badRequestError )
import           Data.Morpheus.Execution.Server.Encode
                                                ( EncodeCon
                                                , encodeMutation
                                                , encodeQuery
                                                , encodeSubscription
                                                )
import           Data.Morpheus.Execution.Server.Introspect
                                                ( IntroCon
                                                , ObjectFields(..)
                                                )
import           Data.Morpheus.Execution.Subscription.ClientRegister
                                                ( GQLState
                                                , publishUpdates
                                                )
import           Data.Morpheus.Parsing.Request.Parser
                                                ( parseGQL )
--import           Data.Morpheus.Schema.Schema                         (Root)
import           Data.Morpheus.Schema.SchemaAPI ( defaultTypes
                                                , hiddenRootFields
                                                , schemaAPI
                                                )
import           Data.Morpheus.Types.GQLType    ( GQLType(CUSTOM) )
import           Data.Morpheus.Types.Internal.AST
                                                ( Operation(..)
                                                , ValidOperation
                                                , DataFingerprint(..)
                                                , DataTyCon(..)
                                                , DataTypeLib(..)
                                                , MUTATION
                                                , OperationType(..)
                                                , QUERY
                                                , SUBSCRIPTION
                                                , initTypeLib
                                                , Value
                                                )
import           Data.Morpheus.Types.Internal.Resolving
                                                ( GQLRootResolver(..)
                                                , Resolver(..)
                                                , toResponseRes
                                                , GQLChannel(..)
                                                , ResponseEvent(..)
                                                , ResponseStream
                                                , Validation
                                                , cleanEvents
                                                , ResultT(..)
                                                , unpackEvents
                                                , Failure(..)
                                                , resolveUpdates
                                                )
import           Data.Morpheus.Types.IO         ( GQLRequest(..)
                                                , GQLResponse(..)
                                                , renderResponse
                                                )
import           Data.Morpheus.Validation.Internal.Utils
                                                ( VALIDATION_MODE(..) )
import           Data.Morpheus.Validation.Query.Validation
                                                ( validateRequest )
import           Data.Typeable                  ( Typeable )


type EventCon event
  = (Eq (StreamChannel event), Typeable event, GQLChannel event)

type IntrospectConstraint m event query mutation subscription
  = ( IntroCon (query (Resolver QUERY event m))
    , IntroCon (mutation (Resolver MUTATION event m))
    , IntroCon (subscription (Resolver SUBSCRIPTION event m))
    )

type RootResCon m event query mutation subscription
  = ( EventCon event
    , Typeable m
    , IntrospectConstraint m event query mutation subscription
    , EncodeCon QUERY event m (query (Resolver QUERY event m))
    , EncodeCon MUTATION event m (mutation (Resolver MUTATION event m))
    , EncodeCon
        SUBSCRIPTION
        event
        m
        (subscription (Resolver SUBSCRIPTION event m))
    )

decodeNoDup :: Failure String m => L.ByteString -> m GQLRequest
decodeNoDup str = case eitherDecodeWith jsonNoDup ifromJSON str of
  Left  (path, x) -> failure $ formatError path x
  Right value     -> pure value


byteStringIO
  :: Monad m => (GQLRequest -> m GQLResponse) -> L.ByteString -> m L.ByteString
byteStringIO resolver request = case decodeNoDup request of
  Left  aesonError' -> return $ badRequestError aesonError'
  Right req         -> encode <$> resolver req

statelessResolver
  :: (Monad m, RootResCon m event query mut sub)
  => GQLRootResolver m event query mut sub
  -> GQLRequest
  -> m GQLResponse
statelessResolver root req =
  renderResponse <$> runResultT (coreResolver root req)

streamResolver
  :: forall event m query mut sub
   . (Monad m, RootResCon m event query mut sub)
  => GQLRootResolver m event query mut sub
  -> GQLRequest
  -> ResponseStream event m GQLResponse
streamResolver root req =
  ResultT $ pure . renderResponse <$> runResultT (coreResolver root req)

coreResolver
  :: forall event m query mut sub
   . (Monad m, RootResCon m event query mut sub)
  => GQLRootResolver m event query mut sub
  -> GQLRequest
  -> ResponseStream event m Value
coreResolver root@GQLRootResolver { queryResolver, mutationResolver, subscriptionResolver } request
  = validRequest >>= execOperator
 where
  validRequest
    :: Monad m => ResponseStream event m (DataTypeLib, ValidOperation)
  validRequest = cleanEvents $ ResultT $ pure $ do
    schema <- fullSchema $ Identity root
    query  <- parseGQL request >>= validateRequest schema FULL_VALIDATION
    pure (schema, query)
  ----------------------------------------------------------
  execOperator (schema, operation@Operation { operationType = Query }) =
    toResponseRes (encodeQuery (schemaAPI schema) queryResolver operation)
  execOperator (_, operation@Operation { operationType = Mutation }) =
    toResponseRes (encodeMutation mutationResolver operation)
  execOperator (_, operation@Operation { operationType = Subscription }) =
    response
   where
    response =
      toResponseRes (encodeSubscription subscriptionResolver operation)

statefulResolver
  :: EventCon event
  => GQLState IO event
  -> (GQLRequest -> ResponseStream event IO Value)
  -> L.ByteString
  -> IO L.ByteString
statefulResolver state streamApi requestText = do
  res <- runResultT (decodeNoDup requestText >>= streamApi)
  mapM_ execute (unpackEvents res)
  pure $ encode $ renderResponse res
 where
  execute (Publish updates) = publishUpdates state updates
  execute Subscribe{}       = pure ()


fullSchema
  :: forall proxy m event query mutation subscription
   . (IntrospectConstraint m event query mutation subscription)
  => proxy (GQLRootResolver m event query mutation subscription)
  -> Validation DataTypeLib
fullSchema _ = querySchema >>= mutationSchema >>= subscriptionSchema
 where
  querySchema = resolveUpdates
    (initTypeLib (operatorType (hiddenRootFields ++ fields) "Query"))
    (defaultTypes : types)
   where
    (fields, types) = objectFields
      (Proxy @(CUSTOM (query (Resolver QUERY event m))))
      (Proxy @(query (Resolver QUERY event m)))
  ------------------------------
  mutationSchema lib = resolveUpdates
    (lib { mutation = maybeOperator fields "Mutation" })
    types
   where
    (fields, types) = objectFields
      (Proxy @(CUSTOM (mutation (Resolver MUTATION event m))))
      (Proxy @(mutation (Resolver MUTATION event m)))
  ------------------------------
  subscriptionSchema lib = resolveUpdates
    (lib { subscription = maybeOperator fields "Subscription" })
    types
   where
    (fields, types) = objectFields
      (Proxy @(CUSTOM (subscription (Resolver SUBSCRIPTION event m))))
      (Proxy @(subscription (Resolver SUBSCRIPTION event m)))
   -- maybeOperator :: [a] -> Text -> Maybe (Text, DataTyCon[a])
  maybeOperator []     = const Nothing
  maybeOperator fields = Just . operatorType fields
  -- operatorType :: [a] -> Text -> (Text, DataTyCon[a])
  operatorType typeData typeName =
    ( typeName
    , DataTyCon { typeData
                , typeName
                , typeFingerprint = SystemFingerprint typeName
                , typeMeta        = Nothing
                }
    )