{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE NamedFieldPuns             #-}
{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE TypeFamilies               #-}
{-# LANGUAGE GADTs                      #-}
{-# LANGUAGE RankNTypes                 #-}
{-# LANGUAGE ScopedTypeVariables        #-}

module Data.Morpheus.Types.Internal.Validation
  ( Validator
  , SelectionValidator
  , InputValidator
  , BaseValidator
  , InputSource(..)
  , Context(..)
  , SelectionContext(..)
  , runValidator
  , askSchema
  , askContext
  , askFragments
  , askFieldType
  , askTypeMember
  , selectRequired
  , selectKnown
  , Constraint(..)
  , constraint
  , withScope
  , withScopeType
  , withScopePosition
  , askScopeTypeName
  , selectWithDefaultValue
  , askScopePosition
  , askInputFieldType
  , askInputMember
  , startInput
  , withInputScope
  , inputMessagePrefix
  , checkUnused
  , Prop(..)
  , constraintInputUnion
  )
  where

import           Data.Semigroup                 ( (<>)
                                                , Semigroup(..)
                                                )
import         Control.Monad.Trans.Reader       ( ReaderT(..)
                                                , ask
                                                , withReaderT
                                                )

-- MORPHEUS
import           Data.Morpheus.Types.Internal.Operation
                                                ( Failure(..)
                                                , Selectable
                                                , selectBy
                                                , selectOr
                                                , KeyOf(..)
                                                , member
                                                , size
                                                )
import           Data.Morpheus.Types.Internal.Resolving
                                                ( Eventless )
import           Data.Morpheus.Types.Internal.AST
                                                ( Name
                                                , Position
                                                , Message
                                                , Ref(..)
                                                , TypeRef(..)
                                                , Fragments
                                                , Schema
                                                , FieldDefinition(..)
                                                , FieldsDefinition(..)
                                                , TypeDefinition(..)
                                                , TypeContent(..)
                                                , isInputDataType
                                                , isFieldNullable
                                                , Value(..)
                                                , Object
                                                , entryValue
                                                , __inputname
                                                )
import           Data.Morpheus.Types.Internal.Validation.Validator
                                                ( Validator(..)
                                                , Constraint(..)
                                                , Target(..)
                                                , InputSource(..)
                                                , InputContext(..)
                                                , Context(..)
                                                , Prop(..)
                                                , renderInputPrefix
                                                , Resolution
                                                , SelectionValidator
                                                , InputValidator
                                                , BaseValidator
                                                , SelectionContext(..)
                                                )
import           Data.Morpheus.Types.Internal.Validation.Error
                                                ( MissingRequired(..)
                                                , KindViolation(..)
                                                , Unknown(..)
                                                , InternalError(..)
                                                , Unused(..)
                                                )

getUnused :: (KeyOf b ,Selectable ca a) => ca -> [b] -> [b]
getUnused uses = filter (not . (`member` uses) . keyOf)

failOnUnused :: Unused b => [b] -> Validator ctx ()
failOnUnused x
  | null x = return ()
  | otherwise = do
    (gctx,_) <- Validator ask
    failure $ map (unused gctx) x

checkUnused :: (KeyOf b ,Selectable ca a, Unused b) =>  ca -> [b] -> Validator ctx ()
checkUnused uses = failOnUnused . getUnused uses

constraint
  :: forall (a :: Target) inp ctx. KindViolation a inp
  => Constraint ( a :: Target)
  -> inp
  -> TypeDefinition
  -> Validator ctx (Resolution a)
constraint OBJECT  _   TypeDefinition { typeContent = DataObject { objectFields } , typeName }
  = pure (typeName, objectFields)
constraint INPUT   _   x | isInputDataType x = pure x
constraint target  ctx _  = failure [kindViolation target ctx]

selectRequired
  ::  ( Selectable c value
      , MissingRequired c ctx
      )
  => Ref
  -> c
  -> Validator ctx value
selectRequired selector container
  = do
    (gctx,ctx) <- Validator ask
    selectBy
      [missingRequired gctx ctx selector container]
      (keyOf selector)
      container

selectWithDefaultValue
  ::  ( Selectable values value
      , MissingRequired values ctx
      )
  => value
  -> FieldDefinition
  -> values
  -> Validator ctx value
selectWithDefaultValue
  fallbackValue
  field@FieldDefinition { fieldName }
  values
  = selectOr
    handleNullable
    pure
    fieldName
    values
  where
    ------------------
    handleNullable
      | isFieldNullable field = pure fallbackValue
      | otherwise             = failSelection
    -----------------
    failSelection = do
        (gctx, ctx) <- Validator ask
        failure [missingRequired gctx ctx (Ref fieldName (scopePosition gctx)) values]

selectKnown
  ::  ( Selectable c a
      , Unknown c ctx
      , KeyOf (UnknownSelector c)
      )
  => UnknownSelector c
  -> c
  -> Validator ctx a
selectKnown selector lib
  = do
    (gctx, ctx) <- Validator ask
    selectBy
      (unknown gctx ctx lib selector)
      (keyOf selector)
      lib

askFieldType
  :: FieldDefinition
  -> SelectionValidator TypeDefinition
askFieldType field@FieldDefinition{ fieldType = TypeRef { typeConName }  }
  = do
    schema <- askSchema
    selectBy
        [internalError field]
        typeConName
        schema

askTypeMember
  :: Name
  -> SelectionValidator (Name, FieldsDefinition)
askTypeMember
  name
  = askSchema
      >>= selectOr notFound pure name
      >>= constraintOBJECT
    where
      notFound = do
          scopeType <- askScopeTypeName
          failure $
              "Type \"" <> name
              <> "\" referenced by union \"" <> scopeType
              <> "\" can't found in Schema."
      --------------------------------------
      constraintOBJECT TypeDefinition { typeName , typeContent } = con typeContent
        where
          con DataObject { objectFields } = pure (typeName, objectFields)
          con _ = do
            scopeType <- askScopeTypeName
            failure $
                "Type \"" <> typeName
                  <> "\" referenced by union \"" <> scopeType
                  <> "\" must be an OBJECT."

askInputFieldType
  :: FieldDefinition
  -> InputValidator TypeDefinition
askInputFieldType field@FieldDefinition{ fieldName , fieldType = TypeRef { typeConName }  }
  = askSchema
    >>= selectBy
        [internalError field]
        typeConName
    >>= constraintINPUT
 where
  constraintINPUT x
    | isInputDataType x = pure x
    | otherwise         = failure $
        "Type \"" <> typeName x
        <> "\" referenced by field \"" <> fieldName
        <> "\" must be an input type."

askInputMember
  :: Name
  -> InputValidator TypeDefinition
askInputMember
  name
  = askSchema
      >>= selectOr notFound pure name
      >>= constraintINPUT_OBJECT
    where
      typeInfo tName
        = "Type \"" <> tName <> "\" referenced by inputUnion "
      notFound = do
          scopeType <- askScopeTypeName
          failure $ typeInfo name <> scopeType <> "\" can't found in Schema."
      --------------------------------------
      constraintINPUT_OBJECT tyDef@TypeDefinition { typeName , typeContent } = con typeContent
        where
          con DataInputObject { } = pure tyDef
          con _ = do
            scopeType <- askScopeTypeName
            failure $ typeInfo typeName <> "\"" <> scopeType <> "\" must be an INPUT_OBJECT."

startInput :: InputSource -> InputValidator a -> Validator ctx a
startInput inputSource
  = setContext
  $ const InputContext
    { inputSource
    , inputPath = []
    }

withInputScope :: Prop -> InputValidator a -> InputValidator a
withInputScope prop = setContext update
  where
    update ctx@InputContext { inputPath = old }
      = ctx { inputPath = old <> [prop] }

runValidator :: Validator ctx a -> Context -> ctx -> Eventless a
runValidator (Validator x) globalCTX ctx = runReaderT x (globalCTX,ctx)

askContext :: Validator ctx ctx
askContext = snd <$> Validator ask

askSchema :: Validator ctx Schema
askSchema = schema . fst <$> Validator ask

askFragments :: Validator ctx Fragments
askFragments = fragments . fst <$> Validator ask

askScopeTypeName :: Validator ctx  Name
askScopeTypeName = scopeTypeName . fst <$> Validator ask

askScopePosition :: Validator ctx Position
askScopePosition = scopePosition . fst <$> Validator ask

setContext
  :: (c' -> c)
  -> Validator c a
  -> Validator c' a
setContext f = Validator . withReaderT ( \(x,y) -> (x,f y)) . _runValidator

setGlobalContext
  :: (Context -> Context)
  -> Validator c a
  -> Validator c a
setGlobalContext f = Validator . withReaderT ( \(x,y) -> (f x,y)) . _runValidator

withScope :: Name -> Ref -> Validator ctx a -> Validator ctx a
withScope scopeTypeName (Ref scopeSelectionName scopePosition) = setGlobalContext update
     where
       update ctx = ctx { scopeTypeName , scopePosition , scopeSelectionName}

withScopePosition :: Position -> Validator ctx a -> Validator ctx a
withScopePosition scopePosition = setGlobalContext update
    where
      update ctx = ctx { scopePosition  }

withScopeType :: Name -> Validator ctx a -> Validator ctx a
withScopeType scopeTypeName = setGlobalContext update
    where
      update ctx = ctx { scopeTypeName  }

inputMessagePrefix :: InputValidator Message
inputMessagePrefix = renderInputPrefix <$> askContext


constraintInputUnion
  :: forall stage. [(Name, Bool)]
  -> Object stage
  -> Either Message (Name, Maybe (Value stage))
constraintInputUnion tags hm = do
  (enum :: Value stage) <- entryValue <$> selectBy
      ("valid input union should contain \"" <> __inputname <> "\" and actual value")
      __inputname
      hm
  tyName <- isPosibeInputUnion tags enum
  case size hm of
    1 -> pure (tyName, Nothing)
    2 -> do
      value <- entryValue <$> selectBy
          ("value for Union \""<> tyName <> "\" was not Provided.")
          tyName
          hm
      pure (tyName , Just value)
    _ -> failure ("input union can have only one variant." :: Message)

isPosibeInputUnion :: [(Name, Bool)] -> Value stage -> Either Message Name
isPosibeInputUnion tags (Enum name) = case lookup name tags of
  Nothing -> failure (name <> " is not posible union type" :: Message)
  _       -> pure name
isPosibeInputUnion _ _ = failure $ "\""<> __inputname <> "\" must be Enum"