# frozen_string_literal: true

module GQLi
  # Validations
  class Validation
    attr_reader :schema, :root, :errors

    def initialize(schema, root)
      @schema = schema
      @root = root
      @errors = []

      validate
    end

    # Returns wether the query is valid or not
    def valid?
      errors.empty?
    end

    protected

    def validate
      @errors = []

      type_name = root.class.name.split('::').last
      validate_type(type_name)
    end

    private

    def validate_type(type)
      root_type = types.find { |t| t.name.casecmp(type).zero? }
      root.__nodes.each do |node|
        begin
          validate_node(root_type, node)
        rescue StandardError => e
          errors << e
        end
      end

      valid?
    end

    def remove_alias(name)
      return name unless name.include?(':')

      name.split(':')[1].strip
    end

    def types
      schema.types
    end

    def validate_node(parent_type, node)
      validate_directives(node)

      return valid_match_node?(parent_type, node) if node.__name.start_with?('... on')

      node_name = remove_alias(node.__name)

      node_type = parent_type.fetch('fields', []).find { |f| f.name == node_name }
      fail "Node type not found for '#{node_name}'" if node_type.nil?

      validate_params(node_type, node)

      resolved_node_type = type_for(node_type)
      fail "Node type not found for '#{node_name}'" if resolved_node_type.nil?

      validate_nesting_node(resolved_node_type, node)

      node.__nodes.each { |n| validate_node(resolved_node_type, n) }
    end

    def valid_match_node?(parent_type, node)
      return if parent_type.fetch('possibleTypes', []).find { |t| t.name == node.__name.gsub('... on ', '') }
      fail "Match type '#{node.__name.gsub('... on ', '')}' invalid"
    end

    def validate_directives(node)
      return unless node.__params.size >= 1
      node.__params.first.tap do |k, v|
        break unless k.to_s.start_with?('@')

        fail "Directive unknown '#{k}'" unless %i[@include @skip].include?(k)
        fail "Missing arguments for directive '#{k}'" if v.nil? || !v.is_a?(::Hash) || v.empty?
        v.each do |arg, value|
          begin
            fail "Invalid argument '#{arg}' for directive '#{k}'" if arg.to_s != 'if'
            fail "Invalid value for 'if`, must be a boolean" if value != !!value
          rescue StandardError => e
            errors << e
          end
        end
      end
    end

    def validate_params(node_type, node)
      node.__params.reject { |p, _| p.to_s.start_with?('@') }.each do |param, value|
        begin
          arg = node_type.fetch('args', []).find { |a| a.name == param.to_s }
          fail "Invalid argument '#{param}'" if arg.nil?

          arg_type = type_for(arg)
          fail "Argument type not found for '#{param}'" if arg_type.nil?

          validate_value_for_type(arg_type, value, param)
        rescue StandardError => e
          errors << e
        end
      end
    end

    def validate_nesting_node(node_type, node)
      fail "Invalid object for node '#{node.__name}'" unless valid_object_node?(node_type, node)
    end

    def valid_object_node?(node_type, node)
      return false if %w[OBJECT INTERFACE].include?(node_type.kind) && node.__nodes.empty?
      true
    end

    def valid_array_node?(node_type, node)
      return false if %w[OBJECT INTERFACE].include?(node_type.kind) && node.__nodes.empty?
      true
    end

    def value_type_error(is_type, should_be, for_arg)
      should_be = should_be.kind == 'ENUM' ? 'Enum' : should_be.name
      additional_message = '. Wrap the value with `__enum`.' if should_be == 'Enum'

      fail "Value is '#{is_type}', but should be '#{should_be}' for '#{for_arg}'#{additional_message}"
    end

    def validate_value_for_type(arg_type, value, for_arg)
      case value
      when EnumValue
        if arg_type.kind == 'ENUM' && !arg_type.enumValues.map(&:name).include?(value.to_s)
          fail "Invalid value for Enum '#{arg_type.name}' for '#{for_arg}'"
        end
      when ::String
        unless arg_type.name == 'String' || arg_type.name == 'ID'
          value_type_error('String or ID', arg_type, for_arg)
        end
      when ::Integer
        value_type_error('Integer', arg_type, for_arg) unless arg_type.name == 'Int'
      when ::Float
        value_type_error('Float', arg_type, for_arg) unless arg_type.name == 'Float'
      when ::Hash
        validate_hash_value(arg_type, value, for_arg)
      when true, false
        value_type_error('Boolean', arg_type, for_arg) unless arg_type.name == 'Boolean'
      else
        value_type_error(value.class.name, arg_type, for_arg)
      end
    end

    def validate_hash_value(arg_type, value, for_arg)
      value_type_error('Object', arg_type.name, for_arg) unless arg_type.kind == 'INPUT_OBJECT'

      type = types.find { |f| f.name == arg_type.name }
      fail "Type not found for '#{arg_type.name}'" if type.nil?

      value.each do |k, v|
        begin
          input_field = type.fetch('inputFields', []).find { |f| f.name == k.to_s }
          fail "Input field definition not found for '#{k}'" if input_field.nil?

          input_field_type = type_for(input_field)
          fail "Input field type not found for '#{k}'" if input_field_type.nil?

          validate_value_for_type(input_field_type, v, k)
        rescue StandardError => e
          errors << e
        end
      end
    end

    def type_for(field_type)
      type = case field_type.type.kind
             when 'NON_NULL'
               non_null_type(field_type.type.ofType)
             when 'LIST'
               field_type.type.ofType
             when 'OBJECT', 'INTERFACE', 'INPUT_OBJECT'
               field_type.type
             when 'SCALAR'
               field_type.type
             end

      types.find { |t| t.name == type.name }
    end

    def non_null_type(non_null)
      case non_null.kind
      when 'LIST'
        non_null.ofType
      else
        non_null
      end
    end
  end
end
