module Avo::Concerns::HasFieldDiscovery

def associations

def associations
  @associations ||= reflections.reject do |key|
    attachment_associations.key?(key) || tags.key?(key) || rich_texts.key?(key)
  end
end

def attachment_associations

def attachment_associations
  @attachment_associations ||= reflections.select { |_, r| r.options[:class_name] == "ActiveStorage::Attachment" }
end

def base_association_options(reflection)

def base_association_options(reflection)
  {
    as: reflection.macro,
    searchable: true,
    sortable: true
  }
end

def column_in_scope?(column_name)

A column is in scope if it's included in `only` and not in `except`.
Determines if a column is included in the discovery scope.
def column_in_scope?(column_name)
  (!@only || @only.include?(column_name)) && (!@except || !@except.include?(column_name))
end

def column_names_mapping

def column_names_mapping
  @column_names_mapping ||= Avo::Mappings::NAMES_MAPPING.dup
    .merge(Avo.configuration.column_names_mapping || {})
end

def column_types_mapping

def column_types_mapping
  @column_types_mapping ||= Avo::Mappings::FIELDS_MAPPING.dup
    .merge(Avo.configuration.column_types_mapping || {})
end

def create_association_field(association_name, reflection)

def create_association_field(association_name, reflection)
  options = base_association_options(reflection)
  options.merge!(polymorphic_options(reflection)) if reflection.options[:polymorphic]
  field(association_name, **options, **@field_options)
end

def create_attachment_field(association_name, reflection)

def create_attachment_field(association_name, reflection)
  field_name = association_name&.to_s&.delete_suffix("_attachment")&.to_sym || association_name
  field_type = determine_attachment_field_type(reflection)
  field(field_name, as: field_type, **@field_options)
end

def create_field(column_name, field_config)

def create_field(column_name, field_config)
  field_options = {as: field_config.dup.delete(:field).to_sym}.merge(field_config)
  field(column_name, **field_options.symbolize_keys, **@field_options.symbolize_keys)
end

def detect_polymorphic_types(reflection)

def detect_polymorphic_types(reflection)
  ApplicationRecord.descendants.select { |klass| klass.reflections[reflection.plural_name] }
end

def determine_attachment_field_type(reflection)

def determine_attachment_field_type(reflection)
  (
    reflection.is_a?(ActiveRecord::Reflection::HasOneReflection) ||
    reflection.is_a?(ActiveStorage::Reflection::HasOneAttachedReflection)
  ) ? :file : :files
end

def determine_field_config(attribute, column)

def determine_field_config(attribute, column)
  model_enums[attribute.to_s] ||
    self.class.column_names_mapping[attribute] ||
    self.class.column_types_mapping[column.type]
end

def discover_associations(only: nil, except: nil, **field_options)

Discovers and configures associations as fields
def discover_associations(only: nil, except: nil, **field_options)
  setup_discovery_options(only, except, field_options)
  return unless safe_model_class.respond_to?(:reflections)
  discover_attachments
  discover_basic_associations
end

def discover_attachments

def discover_attachments
  attachment_associations.each do |association_name, reflection|
    next unless column_in_scope?(association_name)
    create_attachment_field(association_name, reflection)
  end
end

def discover_basic_associations

def discover_basic_associations
  associations.each do |association_name, reflection|
    next unless column_in_scope?(association_name)
    create_association_field(association_name, reflection)
  end
end

def discover_by_type(associations, as_type)

def discover_by_type(associations, as_type)
  associations.each_key do |association_name|
    next unless column_in_scope?(association_name)
    field association_name, as: as_type, **@field_options.merge(name: yield(association_name))
  end
end

def discover_columns(only: nil, except: nil, **field_options)

Discovers and configures database columns as fields
def discover_columns(only: nil, except: nil, **field_options)
  setup_discovery_options(only, except, field_options)
  return unless safe_model_class.respond_to?(:columns_hash)
  discoverable_columns.each do |column_name, column|
    process_column(column_name, column)
  end
  discover_tags
  discover_rich_texts
end

def discover_rich_texts

def discover_rich_texts
  rich_texts.each_key do |association_name|
    next unless column_in_scope?(association_name)
    field_name = association_name&.to_s&.delete_prefix("rich_text_")&.to_sym || association_name
    field field_name, as: :trix, **@field_options
  end
end

def discover_tags

def discover_tags
  tags.each_key do |association_name|
    next unless column_in_scope?(association_name)
    field(
      tag_field_name(association_name), as: :tags,
      acts_as_taggable_on: tag_field_name(association_name),
      **@field_options
    )
  end
end

def discoverable_columns

def discoverable_columns
  model_db_columns.reject do |column_name, _|
    skip_column?(column_name)
  end
end

def ignore_reflection?(name)

def ignore_reflection?(name)
  %w[blob blobs tags].include?(name.split("_").pop) || name.to_sym == :taggings
end

def model_db_columns

Returns database columns for the model, excluding ignored columns
def model_db_columns
  @model_db_columns ||= safe_model_class.columns_hash.symbolize_keys.except(*COLUMN_NAMES_TO_IGNORE)
end

def model_enums

def model_enums
  @model_enums ||= if safe_model_class.respond_to?(:defined_enums)
    safe_model_class.defined_enums.transform_values do |enum|
      {
        field: :select,
        enum:
      }
    end
  else
    {}
  end.with_indifferent_access
end

def polymorphic_options(reflection)

def polymorphic_options(reflection)
  {polymorphic_as: reflection.name, types: detect_polymorphic_types(reflection)}
end

def process_column(column_name, column)

def process_column(column_name, column)
  field_config = determine_field_config(column_name, column)
  return unless field_config
  create_field(column_name, field_config)
end

def reflections

def reflections
  @reflections ||= safe_model_class.reflections.symbolize_keys.reject do |name, _|
    ignore_reflection?(name.to_s)
  end
end

def rich_text_column?(column_name)

def rich_text_column?(column_name)
  rich_texts.key?(:"rich_text_#{column_name}")
end

def rich_texts

def rich_texts
  @rich_texts ||= reflections.select { |_, r| r.options[:class_name] == "ActionText::RichText" }
end

def safe_model_class

(e.g. in the context of the sidebar)
Fetches the model class, falling back to the items_holder parent record in certain instances
def safe_model_class
  respond_to?(:model_class) ? model_class : @items_holder.parent.model_class
rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
  nil
end

def setup_discovery_options(only, except, field_options)

def setup_discovery_options(only, except, field_options)
  @only = only
  @except = except
  @field_options = field_options
end

def skip_column?(column_name)

def skip_column?(column_name)
  !column_in_scope?(column_name) ||
    reflections.key?(column_name) ||
    rich_text_column?(column_name)
end

def tag_field_name(association_name)

def tag_field_name(association_name)
  association_name&.to_s&.delete_suffix("_taggings")&.pluralize&.to_sym || association_name
end

def tags

def tags
  @tags ||= reflections.select { |_, r| r.options[:as] == :taggable }
end