{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE NamedFieldPuns    #-}
{-# LANGUAGE OverloadedStrings #-}

module Data.Morpheus.Execution.Subscription.Apollo
  ( SubAction(..)
  , apolloFormat
  , acceptApolloSubProtocol
  , toApolloResponse
  ) where

import           Data.Aeson                 (FromJSON (..), ToJSON (..), Value (..), eitherDecode, encode, pairs,
                                             withObject, (.:), (.:?), (.=))
import           Data.ByteString.Lazy.Char8 (ByteString)
import           Data.Morpheus.Types        (GQLRequest (..))
import           Data.Morpheus.Types.IO     (GQLResponse)
import           Data.Semigroup             ((<>))
import           Data.Text                  (Text)
import           GHC.Generics               (Generic)
import           Network.WebSockets         (AcceptRequest (..), RequestHead, getRequestSubprotocols)

type ApolloID = Text

data ApolloSubscription payload =
  ApolloSubscription
    { apolloId      :: Maybe ApolloID
    , apolloType    :: Text
    , apolloPayload :: Maybe payload
    }
  deriving (Show, Generic)

instance FromJSON a => FromJSON (ApolloSubscription a) where
  parseJSON = withObject "ApolloSubscription" objectParser
    where
      objectParser o =
        ApolloSubscription <$> o .:? "id" <*> o .: "type" <*> o .:? "payload"

data RequestPayload =
  RequestPayload
    { payloadOperationName :: Maybe Text
    , payloadQuery         :: Maybe Text
    , payloadVariables     :: Maybe Value
    }
  deriving (Show, Generic)

instance FromJSON RequestPayload where
  parseJSON = withObject "ApolloPayload" objectParser
    where
      objectParser o =
        RequestPayload <$> o .:? "operationName" <*> o .:? "query" <*>
        o .:? "variables"

instance ToJSON a => ToJSON (ApolloSubscription a) where
  toEncoding (ApolloSubscription id' type' payload') =
    pairs $ "id" .= id' <> "type" .= type' <> "payload" .= payload'

acceptApolloSubProtocol :: RequestHead -> AcceptRequest
acceptApolloSubProtocol reqHead =
  apolloProtocol (getRequestSubprotocols reqHead)
  where
    apolloProtocol ["graphql-subscriptions"] =
      AcceptRequest (Just "graphql-subscriptions") []
    apolloProtocol ["graphql-ws"] = AcceptRequest (Just "graphql-ws") []
    apolloProtocol _ = AcceptRequest Nothing []

toApolloResponse :: ApolloID -> GQLResponse -> ByteString
toApolloResponse sid val =
  encode $ ApolloSubscription (Just sid) "data" (Just val)

data SubAction
  = RemoveSub ApolloID
  | AddSub ApolloID GQLRequest
  | SubError String

apolloFormat :: ByteString -> SubAction
apolloFormat = toWsAPI . eitherDecode
  where
    toWsAPI :: Either String (ApolloSubscription RequestPayload) -> SubAction
    toWsAPI (Right ApolloSubscription { apolloType = "start"
                                      , apolloId = Just sessionId
                                      , apolloPayload = Just RequestPayload { payloadQuery = Just query
                                                                            , payloadOperationName = operationName
                                                                            , payloadVariables = variables
                                                                            }
                                      }) =
      AddSub sessionId (GQLRequest {query, operationName, variables})
    toWsAPI (Right ApolloSubscription { apolloType = "stop"
                                      , apolloId = Just sessionId
                                      }) = RemoveSub sessionId
    toWsAPI (Right x) = SubError (show x)
    toWsAPI (Left x) = SubError x