{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-}

module Data.Morpheus.Validation.Internal.Value
  ( validateInputByTypeRef,
    validateInputByType,
    ValidateWithDefault,
  )
where

import Control.Monad.Except (throwError)
import Data.Morpheus.Error.Input (typeViolation)
import Data.Morpheus.Error.Variable (incompatibleVariableType)
import Data.Morpheus.Internal.Utils
  ( singleton,
  )
import Data.Morpheus.Types.Internal.AST
  ( CONST,
    DataEnumValue (..),
    FieldDefinition (..),
    FieldName,
    FieldsDefinition,
    GQLError,
    IN,
    Object,
    ObjectEntry (..),
    Ref (..),
    ScalarDefinition (..),
    ScalarValue (..),
    TRUE,
    TypeContent (..),
    TypeDefinition (..),
    TypeName,
    TypeRef (..),
    TypeWrapper (..),
    Typed (..),
    UnionMember (..),
    UnionTypeDefinition,
    VALID,
    ValidValue,
    Value (..),
    Variable (..),
    VariableContent (..),
    atPositions,
    isNullable,
    isSubtype,
    mkMaybeType,
    mkTypeRef,
    msg,
    packName,
    toCategory,
    typed,
    unitFieldName,
    unitTypeName,
    untyped,
  )
import Data.Morpheus.Types.Internal.Validation
  ( askType,
    askTypeMember,
    constraintInputUnion,
    selectKnown,
    selectWithDefaultValue,
  )
import Data.Morpheus.Types.Internal.Validation.Scope (setType)
import Data.Morpheus.Types.Internal.Validation.Validator
import Relude

violation ::
  Maybe GQLError ->
  Value s ->
  InputValidator schemaS ctx a
violation message value = do
  Scope
    { position,
      currentTypeName,
      currentTypeWrappers
    } <-
    asksScope id
  prefix <- inputMessagePrefix
  throwError
    $ ( prefix
          <> typeViolation
            (TypeRef currentTypeName currentTypeWrappers)
            value
          <> maybe "" (" " <>) message
      )
    `atPositions` position

checkTypeCompatibility ::
  TypeRef ->
  Ref FieldName ->
  Variable VALID ->
  InputValidator schemaS ctx ValidValue
checkTypeCompatibility valueType ref var@Variable {variableValue = ValidVariableValue value, variableType}
  | variableType `isSubtype` valueType = pure value
  | otherwise = throwError $ incompatibleVariableType ref var valueType

validateInputByTypeRef ::
  (ValidateWithDefault c schemaS s) =>
  Typed IN schemaS TypeRef ->
  Value s ->
  Validator schemaS (InputContext c) (Value VALID)
validateInputByTypeRef
  ref
  value = do
    inputTypeDef <- askType ref
    validateInputByType
      (untyped typeWrappers ref)
      inputTypeDef
      value

validateValueByField ::
  (ValidateWithDefault c schemaS s) =>
  FieldDefinition IN schemaS ->
  Value s ->
  Validator schemaS (InputContext c) (Value VALID)
validateValueByField field =
  inField field
    . validateInputByTypeRef
      (typed fieldType field)

-- Validate input Values
validateInputByType ::
  (ValidateWithDefault ctx schemaS valueS) =>
  TypeWrapper ->
  TypeDefinition IN schemaS ->
  Value valueS ->
  InputValidator schemaS ctx ValidValue
validateInputByType tyWrappers typeDef =
  withScope (setType typeDef tyWrappers) . validateWrapped tyWrappers typeDef

-- VALIDATION
validateWrapped ::
  (ValidateWithDefault ctx schemaS valueS) =>
  TypeWrapper ->
  TypeDefinition IN schemaS ->
  Value valueS ->
  InputValidator schemaS ctx ValidValue
-- Validate Null. value = null ?
validateWrapped wrappers _ (ResolvedVariable ref variable) = do
  typeName <- asksScope currentTypeName
  checkTypeCompatibility (TypeRef typeName wrappers) ref variable
validateWrapped wrappers _ Null
  | isNullable wrappers = pure Null
  | otherwise = violation Nothing Null
-- Validate LIST

validateWrapped (TypeList wrappers _) tyCont (List list) =
  List <$> traverse (validateInputByType wrappers tyCont) list
validateWrapped (TypeList wrappers _) tyCont singleElem =
  List . pure <$> validateInputByType wrappers tyCont singleElem
{-- 2. VALIDATE TYPES, all wrappers are already Processed --}
{-- VALIDATE OBJECT--}
validateWrapped BaseType {} TypeDefinition {typeContent} entryValue =
  validateUnwrapped typeContent entryValue

validateUnwrapped ::
  (ValidateWithDefault ctx schemaS valueS) =>
  TypeContent TRUE IN schemaS ->
  Value valueS ->
  InputValidator schemaS ctx ValidValue
validateUnwrapped (DataInputObject parentFields) (Object fields) =
  Object <$> validateInputObject parentFields fields
validateUnwrapped (DataInputUnion inputUnion) (Object rawFields) =
  validateInputUnion inputUnion rawFields
validateUnwrapped (DataEnum tags) value =
  validateEnum tags value
validateUnwrapped (DataScalar dataScalar) value =
  validateScalar dataScalar value
validateUnwrapped _ value = violation Nothing value

-- INPUT UNION
validateInputUnion ::
  (ValidateWithDefault ctx schemaS s) =>
  UnionTypeDefinition IN schemaS ->
  Object s ->
  InputValidator schemaS ctx (Value VALID)
validateInputUnion inputUnion rawFields =
  case constraintInputUnion inputUnion rawFields of
    Left message -> violation (Just $ msg message) (Object rawFields)
    Right (name, value) -> validateInputUnionMember name value

validateInputUnionMember ::
  (ValidateWithDefault ctx schemaS valueS) =>
  UnionMember IN schemaS ->
  Value valueS ->
  InputValidator schemaS ctx (Value VALID)
validateInputUnionMember member value = do
  inputDef <- askDef
  mkInputUnionValue member <$> validateInputByType mkMaybeType inputDef value
  where
    askDef
      | nullary member = askType (Typed $ mkTypeRef unitTypeName)
      | otherwise = toCategory <$> askTypeMember member

mkInputUnionValue :: UnionMember IN s' -> Value s -> Value s
mkInputUnionValue
  UnionMember
    { memberName,
      nullary
    } = Object . singleton key . ObjectEntry key . packNullary
    where
      key = coerce memberName
      packNullary
        | nullary = Object . singleton unitFieldName . ObjectEntry unitFieldName
        | otherwise = id

-- INPUT Object
validateInputObject ::
  (ValidateWithDefault ctx schemaS valueS) =>
  FieldsDefinition IN schemaS ->
  Object valueS ->
  InputValidator schemaS ctx (Object VALID)
validateInputObject fieldsDef object =
  traverse_ (`selectKnown` fieldsDef) object
    *> traverse (validateWithDefault object) fieldsDef

class ValidateWithDefault c schemaS s where
  validateWithDefault ::
    Object s ->
    FieldDefinition IN schemaS ->
    Validator schemaS (InputContext c) (ObjectEntry VALID)

instance ValidateWithDefault c VALID s where
  validateWithDefault object fieldDef@FieldDefinition {fieldName} =
    ObjectEntry fieldName
      <$> selectWithDefaultValue
        pure
        (validateValueByField fieldDef . entryValue)
        fieldDef
        object

instance ValidateWithDefault c CONST s where
  validateWithDefault object fieldDef@FieldDefinition {fieldName} =
    ObjectEntry fieldName
      <$> selectWithDefaultValue
        (validateValueByField fieldDef)
        (validateValueByField fieldDef . entryValue)
        fieldDef
        object

-- Leaf Validations
validateScalar ::
  ScalarDefinition ->
  Value s ->
  InputValidator schemaS ctx ValidValue
validateScalar ScalarDefinition {validateValue} value = do
  typeName <- asksScope currentTypeName
  scalarValue <- toScalar typeName value
  case validateValue scalarValue of
    Right _ -> pure scalarValue
    Left "" -> violation Nothing value
    Left message -> violation (Just $ msg message) value
  where
    toScalar :: TypeName -> Value s -> InputValidator schemaS ctx ValidValue
    toScalar typeName (Scalar x) | isValidDefault typeName x = pure (Scalar x)
    toScalar _ _ = violation Nothing value

isValidDefault :: TypeName -> ScalarValue -> Bool
isValidDefault "Boolean" = isBoolean
isValidDefault "String" = isString
isValidDefault "Float" = oneOf [isFloat, isInt]
isValidDefault "Int" = isInt
isValidDefault "ID" = oneOf [isInt, isFloat, isString]
isValidDefault _ = const True

oneOf :: [a -> Bool] -> a -> Bool
oneOf ls v = any (v &) ls

isBoolean :: ScalarValue -> Bool
isBoolean Boolean {} = True
isBoolean _ = False

isString :: ScalarValue -> Bool
isString String {} = True
isString _ = False

isFloat :: ScalarValue -> Bool
isFloat Float {} = True
isFloat _ = False

isInt :: ScalarValue -> Bool
isInt Int {} = True
isInt _ = False

isVariableValue :: InputValidator schemaS c Bool
isVariableValue =
  \case
    SourceVariable {isDefaultValue} -> not isDefaultValue
    _ -> False
    <$> inputValueSource

validateEnum ::
  [DataEnumValue s] ->
  Value valueS ->
  InputValidator schemaS c ValidValue
validateEnum enumValues value@(Scalar (String enumValue))
  | packName enumValue `elem` tags = do
      isFromVariable <- isVariableValue
      if isFromVariable
        then pure (Enum (packName enumValue))
        else violation Nothing value
  where
    tags = fmap enumName enumValues
validateEnum enumValues value@(Enum enumValue)
  | enumValue `elem` tags = pure (Enum enumValue)
  | otherwise = violation Nothing value
  where
    tags = fmap enumName enumValues
validateEnum _ value = violation Nothing value
