# frozen_string_literal: true

require 'json'
require 'chronic'

class ParamValidation
  # Given a hash of data and a validation hash, check all the validations, raising an Error on the first invalid key
  # @raise [ValidationError] if one or more of the validations fail.
  def initialize(data, validations)
    errors = []
    validations.each do |key, validators|
      val = key === :root ? data : (data[key] || data[key.to_s] || data[key.to_sym])
      next if validators[:required].nil? && val.nil?

      validators.each do |name, arg|
        validator = @@validators[name]
        msg = validations[key][:message]
        next unless validator

        is_valid = @@validators[name].call(val, arg, data)
        msg_proc = @@messages[name]
        msg ||= @@messages[name].call(key: key, data: data, val: val, arg: arg) if msg_proc
        errors.push(msg: msg, data: { key: key, val: val, name: name, msg: msg }) unless is_valid
      end
    end
    if errors.length == 1
      raise ValidationError.new(errors[0][:msg], errors[0][:data])
    elsif errors.length > 1
      msg = errors.collect { |e| e[:msg] }.join('\n')
      raise ValidationError.new(msg, errors.collect { |e| e[:data] })
    end
  end

  def self.messages
    @@messages
  end

  def self.set_message(name, &block)
    @@messages[name] = block
  end

  def self.validators
    @@validators
  end

  def self.add_validator(name, &block)
    @@validators[name] = block
  end

  def self.structure_validators
    @@structure_validators
  end

  def self.add_structure_validator(name, &block)
    @@structure_validators[name] = block
  end

  # In each Proc
  #  - val is the value we are actually validating from the data passed in
  #  - arg is the argument passed into the validator (eg for {required: true}, it is `true`)
  #  - data is the entire set of data
  @@validators = {
    required: ->(val, _arg, _data) { !val.nil? },
    absent: ->(val, _arg, _data) { val.nil? },
    not_blank: ->(val, _arg, _data) { val.is_a?(String) && !val.empty? },
    not_included_in: lambda { |val, arg, _data|
                       begin
                                                !arg.include?(val)
                       rescue StandardError
                         false
                                              end
                     },
    included_in: lambda { |val, arg, _data|
                   begin
                                            arg.include?(val)
                   rescue StandardError
                     false
                                          end
                 },
    format: lambda { |val, arg, _data|
              begin
                                       val =~ arg
              rescue StandardError
                false
                                     end
            },
    is_integer: ->(val, _arg, _data) { val.is_a?(Integer) || val =~ /\A[+-]?\d+\Z/ },
    is_float: lambda { |val, _arg, _data|
                val.is_a?(Float) || (begin
                                                              !!Float(val)
                                     rescue StandardError
                                       false
                                                            end)
              },
    min_length: lambda { |val, arg, _data|
                  begin
                                           val.length >= arg
                  rescue StandardError
                    false
                                         end
                },
    max_length: lambda { |val, arg, _data|
                  begin
                                           val.length <= arg
                  rescue StandardError
                    false
                                         end
                },
    length_range: lambda { |val, arg, _data|
                    begin
                                             arg.cover?(val.length)
                    rescue StandardError
                      false
                                           end
                  },
    length_equals: ->(val, arg, _data) { val.length == arg },
    is_reference: ->(val, _arg, _data) { (val.is_a?(Integer) && val >= 0) || val =~ /\A\d+\Z/ || val == '' },
    equals: ->(val, arg, _data) { val == arg },
    min: lambda { |val, arg, _data|
           begin
                                    val >= arg
           rescue StandardError
             false
                                  end
         },
    max: lambda { |val, arg, _data|
           begin
                                    val <= arg
           rescue StandardError
             false
                                  end
         },
    is_array: ->(val, _arg, _data) { val.is_a?(Array) },
    is_hash: ->(val, _arg, _data) { val.is_a?(Hash) },
    is_json: ->(val, _arg, _data) { ParamValidation.is_valid_json?(val) },
    in_range: lambda { |val, arg, _data|
                begin
                                         arg.cover?(val)
                rescue StandardError
                  false
                                       end
              },
    is_a: ->(val, arg, _data) { arg.is_a?(Enumerable) ? arg.any? { |i| val.is_a?(i) } : val.is_a?(arg) },
    can_be_date: ->(val, _arg, _data) { val.is_a?(Date) || val.is_a?(DateTime) || Chronic.parse(val) },
    array_of_hashes: ->(_val, arg, data) { data.is_a?(Array) && data.map { |pair| ParamValidation.new(pair.to_h, arg) }.all? }
  }

  @@messages = {
    required: ->(h) { "#{h[:key]} is required" },
    absent: ->(h) { "#{h[:key]} must not be present" },
    not_blank: ->(h) { "#{h[:key]} must not be blank" },
    not_included_in: ->(h) { "#{h[:key]} must not be included in #{h[:arg].join(', ')}" },
    included_in: ->(h) { "#{h[:key]} must be one of #{h[:arg].join(', ')}" },
    format: ->(h) { "#{h[:key]} doesn't have the right format" },
    is_integer: ->(h) { "#{h[:key]} should be an integer" },
    is_float: ->(h) { "#{h[:key]} should be a float" },
    min_length: ->(h) { "#{h[:key]} has a minimum length of #{h[:arg]}" },
    max_length: ->(h) { "#{h[:key]} has a maximum length of #{h[:arg]}" },
    length_range: ->(h) { "#{h[:key]} should have a length within #{h[:arg]}" },
    length_equals: ->(h) { "#{h[:key]} should have a length of #{h[:arg]}" },
    is_reference: ->(h) { "#{h[:key]} should be an integer or blank" },
    equals: ->(h) { "#{h[:key]} should equal #{h[:arg]}" },
    min: ->(h) { "#{h[:key]} must be at least #{h[:arg]}" },
    max: ->(h) { "#{h[:key]} cannot be more than #{h[:arg]}" },
    in_range: ->(h) { "#{h[:key]} should be within #{h[:arg]}" },
    is_json: ->(h) { "#{h[:key]} should be valid JSON" },
    is_hash: ->(h) { "#{h[:key]} should be a hash" },
    is_a: ->(h) { "#{h[:key]} should be of the type(s): #{h[:arg].is_a?(Enumerable) ? h[:arg].join(', ') : h[:arg]}" },
    can_be_date: ->(h) { "#{h[:key]} should be a datetime or be parsable as one" },
    array_of_hashes: ->(_h) { 'Please pass in an array of hashes' }
  }

  # small utility for testing json validity
  def self.is_valid_json?(str)
    JSON.parse(str)
    true
  rescue StandardError => e
    false
  end

  # Special error class that holds all the error data for reference
  class ValidationError < TypeError
    attr_reader :data

    # @param [String] msg message for the validation error(s). Multiple error
    #   messages are split by new lines (\n)
    # @param [Hash, Array<Hash>] data information about the validation failure
    #   or failures. If one failure, a single failure hash is returned, if multiple, an array is returned.
    #   Each failure hash has the following:
    #     * :key - the [Symbol] of the key in the hash where verification failed
    #     * :val - the value of pair in the hash selected by :key
    #     * :name - the [Symbol] for the verification which failed
    #     * :msg - the [String] for the msg for the verifications which failed
    def initialize(msg, data)
      @data = data
      super(msg)
    end
  end
end
