{-# LANGUAGE GADTs #-}
{-# LANGUAGE NamedFieldPuns    #-}
{-# LANGUAGE OverloadedStrings #-}

module Data.Morpheus.Validation.Internal.Value
  ( validateInput )
where

import           Data.Semigroup                 ((<>))
import           Data.Maybe                     (maybe)
import           Data.Foldable                  (traverse_)
import           Data.List                      ( elem )

-- MORPHEUS

import           Data.Morpheus.Error.Utils      ( errorMessage )
import           Data.Morpheus.Error.Variable   ( incompatibleVariableType )
import           Data.Morpheus.Error.Input      ( typeViolation )
import           Data.Morpheus.Types.Internal.AST
                                                ( FieldDefinition(..)
                                                , TypeContent(..)
                                                , TypeDefinition(..)
                                                , ScalarDefinition(..)
                                                , TypeRef(..)
                                                , TypeWrapper(..)
                                                , DataEnumValue(..)
                                                , Value(..)
                                                , ValidValue
                                                , Variable(..)
                                                , Ref(..)
                                                , Message
                                                , Name
                                                , ResolvedValue
                                                , VALID
                                                , VariableContent(..)
                                                , TypeRef(..)
                                                , isWeaker
                                                , isNullableWrapper
                                                , ObjectEntry(..)
                                                , RESOLVED
                                                , InputFieldsDefinition(..)
                                                , Variable(..)
                                                )
import           Data.Morpheus.Types.Internal.AST.OrderedMap
                                                ( unsafeFromValues )
import           Data.Morpheus.Types.Internal.Operation
                                                ( Failure(..) )
import           Data.Morpheus.Types.Internal.Validation
                                                ( InputValidator
                                                , askInputFieldType
                                                , constraintInputUnion
                                                , askInputMember
                                                , selectKnown
                                                , selectWithDefaultValue
                                                , askScopePosition
                                                , withScopeType
                                                , withInputScope
                                                , inputMessagePrefix
                                                , Prop(..)
                                                )

castFailure :: TypeRef -> Maybe Message -> ResolvedValue ->  InputValidator a
castFailure expected message value  = do
  pos <- askScopePosition
  prefix <- inputMessagePrefix
  failure
    $  errorMessage pos
    $ prefix <> typeViolation expected value <> maybe "" (" " <>) message

checkTypeEquality
  :: (Name, [TypeWrapper])
  -> Ref
  -> Variable VALID
  -> InputValidator ValidValue
checkTypeEquality (tyConName, tyWrappers) ref var@Variable { variableValue = ValidVariableValue value, variableType }
  | typeConName variableType == tyConName && not
    (isWeaker (typeWrappers variableType) tyWrappers)
  = pure value
  | otherwise
  = failure $ incompatibleVariableType
      ref
      var
      TypeRef
        { typeConName = tyConName
        , typeWrappers = tyWrappers
        , typeArgs     = Nothing
        }

-- Validate Variable Argument or all Possible input Values
validateInput
  :: [TypeWrapper]
  -> TypeDefinition
  -> ObjectEntry RESOLVED
  -> InputValidator ValidValue
validateInput tyWrappers TypeDefinition { typeContent = tyCont, typeName } =
  withScopeType typeName
  . validateWrapped tyWrappers tyCont
 where
  mismatchError :: [TypeWrapper] -> ResolvedValue -> InputValidator ValidValue
  mismatchError  wrappers = castFailure (TypeRef typeName Nothing wrappers) Nothing
  -- VALIDATION
  validateWrapped
    :: [TypeWrapper]
    -> TypeContent
    -> ObjectEntry RESOLVED
    -> InputValidator ValidValue
  -- Validate Null. value = null ?
  validateWrapped wrappers _  ObjectEntry { entryValue = ResolvedVariable ref variable} =
    checkTypeEquality (typeName, wrappers) ref variable
  validateWrapped wrappers _ ObjectEntry { entryValue = Null}
    | isNullableWrapper wrappers = return Null
    | otherwise                  = mismatchError wrappers Null
  -- Validate LIST
  validateWrapped (TypeMaybe : wrappers) _ value =
    validateWrapped wrappers tyCont value
  validateWrapped (TypeList : wrappers) _ (ObjectEntry key (List list)) =
    List <$> traverse validateElement list
   where
    validateElement = validateWrapped wrappers tyCont . ObjectEntry key
  {-- 2. VALIDATE TYPES, all wrappers are already Processed --}
  {-- VALIDATE OBJECT--}
  validateWrapped [] dt v = validate dt v
   where
    validate
      :: TypeContent -> ObjectEntry RESOLVED -> InputValidator ValidValue
    validate (DataInputObject parentFields) ObjectEntry { entryValue = Object fields} = do
      traverse_ requiredFieldsDefined (unInputFieldsDefinition parentFields)
      Object <$> traverse validateField fields
     where
      requiredFieldsDefined :: FieldDefinition -> InputValidator (ObjectEntry RESOLVED)
      requiredFieldsDefined fieldDef@FieldDefinition { fieldName}
        = selectWithDefaultValue (ObjectEntry fieldName Null) fieldDef fields
      validateField
        :: ObjectEntry RESOLVED -> InputValidator (ObjectEntry VALID)
      validateField entry@ObjectEntry { entryName } = do
          inputField@FieldDefinition{ fieldType = TypeRef { typeConName , typeWrappers }} <- getField
          inputTypeDef <- askInputFieldType inputField
          withInputScope (Prop entryName typeConName) $ ObjectEntry entryName
            <$> validateInput
                  typeWrappers
                  inputTypeDef
                  entry
       where
        getField = selectKnown entry parentFields
    -- VALIDATE INPUT UNION
    -- TODO: enhance input union Validation
    validate (DataInputUnion inputUnion) ObjectEntry { entryValue = Object rawFields} =
      case constraintInputUnion inputUnion rawFields of
        Left message -> castFailure (TypeRef typeName Nothing []) (Just message) (Object rawFields)
        Right (name, Nothing   ) -> return (Object $ unsafeFromValues [ObjectEntry "__typename" (Enum name)])
        Right (name, Just value) -> do
          inputDef <- askInputMember name
          validValue <- validateInput
                              [TypeMaybe]
                              inputDef
                              (ObjectEntry name value)
          return (Object $ unsafeFromValues [ObjectEntry "__typename" (Enum name), ObjectEntry name validValue])
    {-- VALIDATE ENUM --}
    validate (DataEnum tags) ObjectEntry { entryValue } =
      validateEnum (castFailure (TypeRef typeName Nothing []) Nothing) tags entryValue
    {-- VALIDATE SCALAR --}
    validate (DataScalar dataScalar) ObjectEntry { entryValue }  =
      validateScalar dataScalar entryValue (castFailure (TypeRef typeName Nothing []))
    validate _ ObjectEntry { entryValue }  = mismatchError [] entryValue
    {-- 3. THROW ERROR: on invalid values --}
  validateWrapped wrappers _ ObjectEntry { entryValue }  = mismatchError wrappers entryValue

validateScalar
  :: ScalarDefinition
  -> ResolvedValue
  -> (Maybe Message -> ResolvedValue -> InputValidator ValidValue)
  -> InputValidator ValidValue
validateScalar ScalarDefinition { validateValue } value err = do
  scalarValue <- toScalar value
  case validateValue scalarValue of
    Right _            -> return scalarValue
    Left  ""           -> err Nothing value
    Left  message -> err (Just message) value
 where
  toScalar :: ResolvedValue -> InputValidator ValidValue
  toScalar (Scalar x) = pure (Scalar x)
  toScalar scValue    = err Nothing scValue

validateEnum
  :: (ResolvedValue -> InputValidator ValidValue)
  -> [DataEnumValue]
  -> ResolvedValue
  -> InputValidator ValidValue
validateEnum err enumValues value@(Enum enumValue)
  | enumValue `elem` tags = pure (Enum enumValue)
  | otherwise             = err value
  where tags = map enumName enumValues
validateEnum err _ value = err value