# frozen_string_literal: true
# rubocop:disable Metrics/ModuleLength
module Bulkrax
  module HasMatchers
    extend ActiveSupport::Concern

    included do
      class_attribute :matchers
      self.matchers ||= {}
    end

    class_methods do
      def matcher_class
        Bulkrax::ApplicationMatcher
      end

      def matcher(name, args = {})
        matcher = matcher_class.new(
          to: name,
          parsed: args[:parsed],
          split: args[:split],
          if: args[:if],
          excluded: args[:excluded],
          nested_type: args[:nested_type]
        )
        self.matchers[name] = matcher
      end
    end

    def add_metadata(node_name, node_content, index = nil)
      field_to(node_name).each do |name|
        matcher = self.class.matcher(name, mapping[name].symbolize_keys) if mapping[name] # the field matched to a pre parsed value in application_matcher.rb
        object_name = get_object_name(name) || false # the "key" of an object property. e.g. { object_name: { alpha: 'beta' } }
        multiple = multiple?(name) # the property has multiple values. e.g. 'letters': ['a', 'b', 'c']
        object_multiple = object_name && multiple?(object_name) # the property's value is an array of object(s)

        next unless field_supported?(name) || (object_name && field_supported?(object_name))

        if object_name
          Rails.logger.info("Bulkrax Column automatically matched object #{node_name}, #{node_content}")
          parsed_metadata[object_name] ||= object_multiple ? [{}] : {}
        end

        value = if matcher
                  result = matcher.result(self, node_content)
                  matched_metadata(multiple, name, result, object_multiple)
                elsif multiple
                  Rails.logger.info("Bulkrax Column automatically matched #{node_name}, #{node_content}")
                  multiple_metadata(node_content)
                else
                  Rails.logger.info("Bulkrax Column automatically matched #{node_name}, #{node_content}")
                  single_metadata(node_content)
                end

        object_name.present? ? set_parsed_object_data(object_multiple, object_name, name, index, value) : set_parsed_data(name, value)
      end
    end

    def get_object_name(field)
      mapping&.[](field)&.[]('object')
    end

    def set_parsed_data(name, value)
      return parsed_metadata[name] = value unless multiple?(name)

      parsed_metadata[name] ||= []
      parsed_metadata[name] += Array.wrap(value).flatten
    end

    def set_parsed_object_data(object_multiple, object_name, name, index, value)
      if object_multiple
        index ||= 0
        parsed_metadata[object_name][index] ||= {}
        parsed_metadata[object_name][index][name] ||= []
        if value.is_a?(Array)
          parsed_metadata[object_name][index][name] += value
        else
          parsed_metadata[object_name][index][name] = value
        end
      else
        parsed_metadata[object_name][name] ||= []
        if value.is_a?(Array)
          parsed_metadata[object_name][name] += value
        else
          parsed_metadata[object_name][name] = value
        end
      end
    end

    def single_metadata(content)
      content = content.content if content.is_a?(Nokogiri::XML::NodeSet)
      return unless content
      Array.wrap(content.to_s.strip).join('; ')
    end

    def multiple_metadata(content)
      return unless content

      case content
      when Nokogiri::XML::NodeSet
        content&.content
      when Array
        content
      when Hash
        Array.wrap(content)
      when String
        Array.wrap(content.strip)
      else
        Array.wrap(content)
      end
    end

    def matched_metadata(multiple, name, result, object_multiple)
      if object_multiple
        if mapping[name]['nested_type'] && mapping[name]['nested_type'] == 'Array'
          multiple_metadata(result)
        else
          single_metadata(result)
        end
      elsif multiple
        multiple_metadata(result)
      else
        single_metadata(result)
      end
    end

    def field_supported?(field)
      field = field.gsub('_attributes', '')

      return false if excluded?(field)
      return true if supported_bulkrax_fields.include?(field)

      Bulkrax.object_factory.field_supported?(field: field, model: factory_class)
    end

    def supported_bulkrax_fields
      @supported_bulkrax_fields ||= fields_that_are_always_singular +
                                    fields_that_are_always_multiple
    end

    ##
    # Determine a multiple properties field
    def multiple?(field)
      return true if fields_that_are_always_singular.include?(field.to_s)
      return false if fields_that_are_always_multiple.include?(field.to_s)

      Bulkrax.object_factory.field_multi_value?(field: field, model: factory_class)
    end

    def fields_that_are_always_multiple
      %w[id delete model visibility]
    end

    def fields_that_are_always_singular
      @fields_that_are_always_singular ||= %W[
        file
        remote_files
        rights_statement
        #{related_parents_parsed_mapping}
        #{related_children_parsed_mapping}
      ]
    end

    def schema_form_definitions
      @schema_form_definitions ||= ::SchemaLoader.new.form_definitions_for(factory_class.name.underscore.to_sym)
    end

    # Hyrax field to use for the given import field
    # @param field [String] the importer field name
    # @return [Array] hyrax fields
    def field_to(field)
      fields = mapping&.map do |key, value|
        return unless value

        if value['from'].instance_of?(Array)
          key if value['from'].include?(field) || key == field
        elsif (value['from'] == field) || key == field
          key
        end
      end&.compact

      return [field] if fields.blank?
      return fields
    end

    # Check whether a field is explicitly excluded in the mapping
    def excluded?(field)
      return false if mapping[field].blank?
      mapping[field]['excluded'] || false
    end
  end
end
# rubocop:enable Metrics/ModuleLength