require "active_model"

module ApiRegulator
  module AttributeDefinitionMixin
    extend ActiveSupport::Concern

    included do
      class_attribute :defined_attributes, default: []
      class_attribute :nested_validators, default: {}
    end

    class_methods do
      def define_attribute_and_validations(param, parent_key = nil)
        # Construct the full key
        full_key = parent_key ? "#{parent_key}.#{param.name}".to_sym : param.name.to_sym

        case param.type
        when :array
          define_array_validations(param, full_key)
        when :object
          define_object_validations(param, full_key)
        else
          define_scalar_validations(param, full_key)
        end
      end

      def define_scalar_validations(param, full_key)
        # Define scalar attributes
        attribute full_key, param.type if param.type
        self.defined_attributes += [full_key]

        param.options.each do |option, value|
          validates full_key, option => value, allow_blank: !param.required?
        end

        # Add type-specific validations
        validate -> { validate_boolean(full_key) } if param.type == :boolean
        validate -> { validate_integer(full_key) } if param.type == :integer
        validate -> { validate_string(full_key) } if param.type == :string
      end


      def define_object_validations(param, full_key)
        # Build nested validator class
        nested_validator_class = build_nested_validator_class(param.children, param.name, self)

        # Add a custom validation for the nested object
        validate -> { validate_nested_object(full_key, nested_validator_class, param) }

        # Store the nested validator
        nested_validators[full_key] = nested_validator_class
      end

      def define_array_validations(param, full_key)
        if param.children.any?
          # Build a nested validator class for array items
          item_validator_class = build_nested_validator_class(param.children, param.name, self)
          validate -> { validate_array_of_objects(full_key, item_validator_class, param) }

          # Store the nested validator
          nested_validators[full_key] = [item_validator_class]
        elsif param.item_type
          # Scalar array with specific item type
          validate -> { validate_array_of_scalars(full_key, param) }
          nested_validators[full_key] = :scalars
        else
          raise "Arrays must have children or an item_type"
        end
      end

      def build_nested_validator_class(children, parent_key, parent_class)
        # Create a unique class name based on the parent key
        class_name = "#{parent_key.to_s.camelize}"

        # Return the existing class if already defined
        if parent_class.const_defined?(class_name, false)
          return parent_class.const_get(class_name)
        end

        # Create the nested validator class
        nested_validator_class = Class.new do
          include ActiveModel::Model
          include ActiveModel::Attributes
          include AttributeDefinitionMixin

          def initialize(attributes = {})
            @raw_attributes = attributes
            allowed_attributes = attributes.slice(*self.class.defined_attributes.map(&:to_sym))
            super(allowed_attributes)
          end

          # Add child attributes and validations
          children.each do |child|
            define_attribute_and_validations(child)
          end
        end

        # Store the nested class under the parent class namespace
        parent_class.const_set(class_name, nested_validator_class)
        nested_validator_class
      end
    end

    # Instance methods
    def validate_array_of_objects(attribute, item_validator_class, param)
      raw_value = @raw_attributes[attribute]

      # Ensure the presence check works only on the array itself, not its contents
      if raw_value.blank?
        errors.add(attribute, "can't be blank") if param.options[:presence]
        return
      end

      unless raw_value.is_a?(Array)
        errors.add(attribute, "must be an array")
        return
      end

      raw_value.each_with_index do |value, index|
        unless value.is_a?(Hash) || value.is_a?(ActionController::Parameters)
          errors.add("#{attribute}[#{index}]", "must be a hash")
          next
        end

        validator = item_validator_class.new(value)
        unless validator.valid?
          validator.errors.each do |error|
            nested_attr = "#{attribute}[#{index}].#{error.attribute}"
            errors.add(nested_attr, error.message)
          end
        end
      end
    end

    def validate_array_of_scalars(attribute, param)
      raw_value = @raw_attributes[attribute]

      if raw_value.nil?
        errors.add(attribute, "can't be blank") if param.options[:presence]
        return
      end

      unless raw_value.is_a?(Array)
        errors.add(attribute, "must be an array")
        return
      end

      inclusion_list = param.options[:inclusion]&.dig(:in)

      raw_value.each_with_index do |value, index|
        # Skip type and inclusion validation if nil is allowed and value is nil
        if value.nil? && (inclusion_list&.include?(nil) || param.options[:allow_nil_items])
          next
        end

        # Type validation for non-nil values
        case param.item_type
        when :string
          unless value.is_a?(String)
            errors.add("#{attribute}[#{index}]", "must be a string")
            next
          end
        when :integer
          unless value.is_a?(Integer)
            errors.add("#{attribute}[#{index}]", "must be an integer")
            next
          end
        when :boolean
          unless [true, false].include?(value)
            errors.add("#{attribute}[#{index}]", "must be a boolean")
            next
          end
        else
          errors.add("#{attribute}[#{index}]", "has an unsupported type")
          next
        end

        # Inclusion validation for non-nil values
        if inclusion_list && !inclusion_list.include?(value)
          errors.add("#{attribute}[#{index}]", "is not included in the list")
        end
      end
    end

    def validate_nested_object(attribute, nested_validator_class, param)
      raw_value = @raw_attributes[attribute]

      # Skip validation if optional and not present
      return if raw_value.nil? && param.options[:optional]

      if raw_value.nil?
        errors.add(attribute, "can't be blank") if param.options[:presence]
        return
      end

      unless raw_value.is_a?(Hash) || raw_value.is_a?(ActionController::Parameters)
        errors.add(attribute, "must be a hash")
        return
      end

      # Initialize the nested validator with the raw value
      validator = nested_validator_class.new(raw_value)

      # If validation fails, propagate errors
      unless validator.valid?
        validator.errors.each do |error|
          nested_attr = "#{attribute}.#{error.attribute}"
          errors.add(nested_attr, error.message)
        end
      end
    end

    def validate_boolean(attribute)
      raw_value = @raw_attributes[attribute]
      unless [true, false, "true", "false", nil].include?(raw_value)
        errors.add(attribute, "must be a boolean (true, false, or nil)")
      end
    end

    def validate_integer(attribute)
      raw_value = @raw_attributes[attribute]
      unless raw_value.to_s =~ /\A[-+]?\d+\z/ || raw_value.nil?
        errors.add(attribute, "must be an integer")
      end
    end

    def validate_string(attribute)
      raw_value = @raw_attributes[attribute]
      unless raw_value.is_a?(String) || raw_value.nil?
        errors.add(attribute, "must be a string")
      end
    end
  end

  class Validator
    include ActiveModel::Model
    include ActiveModel::Attributes
    include AttributeDefinitionMixin

    @validators = {}

    def self.build_all(api_definitions)
      api_definitions.each do |api_definition|
        class_name = "#{api_definition.controller_path}/#{api_definition.action_name}".gsub("/", "_").camelcase
        validator_class = build_class(api_definition.params)
        @validators[[api_definition.controller_path, api_definition.action_name]] = validator_class
        Validator.const_set(class_name, validator_class)
      end
    end

    def self.build_response_validators(api_definitions = ApiRegulator.api_definitions)
      api_definitions.each do |api_definition|
        api_definition.responses.each do |code, params|
          class_name = "#{api_definition.controller_path}/#{api_definition.action_name}/Response#{code}".gsub("/", "_").camelcase

          validator_class = build_class(params.children)
          @validators[[api_definition.controller_path, api_definition.action_name, code]] = validator_class
          Validator.const_set(class_name, validator_class)
        end
      end
    end

    def self.get(controller, action, code = nil)
      @validators[[controller.to_s, action.to_s, code].compact]
    end

    def self.build_class(params)
      Class.new do
        include ActiveModel::Model
        include ActiveModel::Attributes
        include AttributeDefinitionMixin

        def initialize(attributes = {})
          @raw_attributes = attributes.deep_symbolize_keys
          self.class.defined_attributes
          allowed_attributes = attributes.slice(*self.class.defined_attributes.map(&:to_sym))
          super(allowed_attributes)
        end

        params.each do |param|
          define_attribute_and_validations(param)
        end
      end
    end

    def self.validate_response(controller, action, code, body)
      validator_class = get(controller, action, code)

      unless validator_class
        raise "No validator found"
      end

      validator = validator_class.new(body)
      unless validator.valid?
        raise ApiRegulator::ValidationError.new(validator.errors)
      end
    end

    def self.reset_validators
      @validators = {}
    end
  end
end