# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/

module HasSearchIndexBackend
  extend ActiveSupport::Concern

  included do
    after_commit  :search_index_update, if: :persisted?
    after_destroy :search_index_destroy
  end

=begin

update search index, if configured - will be executed automatically

  model = Model.find(123)
  model.search_index_update

=end

  def search_index_update
    return true if ignore_search_indexing?(:update)

    # start background job to transfer data to search index
    return true if !SearchIndexBackend.enabled?

    return true if previous_changes.blank?

    SearchIndexJob.perform_later(self.class.to_s, id)
    SearchIndexAssociationsJob.perform_later(self.class.to_s, id)
    true
  end

  def search_index_indexable
    Models.indexable.reject { |local_class| local_class == self.class }
  end

  def search_index_indexable_attributes(index_class)
    result = []
    index_class.new.attributes.each_key do |key|
      attribute_name = key.to_s
      next if attribute_name.blank?

      # due to performance reasons, we only want to process some attributes for specific classes (e.g. tickets)
      next if !index_class.search_index_attribute_relevant?(attribute_name)

      attribute_ref_name = index_class.search_index_attribute_ref_name(attribute_name)
      next if attribute_ref_name.blank?

      association = index_class.reflect_on_association(attribute_ref_name)
      next if association.blank?
      next if association.options[:polymorphic]

      attribute_class = association.klass
      next if attribute_class.blank?
      next if attribute_class != self.class

      result << {
        name:     attribute_name,
        ref_name: attribute_ref_name,
      }
    end
    result
  end

  def search_index_update_delta(index_class:, value:, attribute:)
    raise "Attribute lookup data needs updated_at for delta updates (object: #{self.class}, #{id})" if value['updated_at'].blank?

    data = {
      attribute[:ref_name] => value,
    }
    where = {
      bool: {
        must: [
          {
            term: {
              attribute[:name] => id,
            },
          },
          {
            range: {
              "#{attribute[:ref_name]}.updated_at" => {
                lt: value['updated_at'].strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
              },
            },
          },
        ],
      },
    }
    SearchIndexBackend.update_by_query(index_class.to_s, data, where)
  end

=begin

update search index, if configured - will be executed automatically

  model = Organizations.find(123)
  result = model.search_index_update_associations

returns

  # Updates asscociation data for users and tickets of the organization in this example
  result = true

=end

  def search_index_update_associations
    return if !SearchIndexBackend.enabled?

    new_search_index_value = search_index_attribute_lookup(include_references: false)
    return if new_search_index_value.blank?

    search_index_indexable.each_with_object([]) do |index_class, result|
      search_index_indexable_attributes(index_class).each do |attribute|
        result << search_index_update_delta(index_class: index_class, value: new_search_index_value, attribute: attribute)
      end
    end
  end

=begin

delete search index object, will be executed automatically

  model = Model.find(123)
  model.search_index_destroy

=end

  def search_index_destroy
    return true if ignore_search_indexing?(:destroy)

    SearchIndexBackend.remove(self.class.to_s, id)
    true
  end

=begin

collect data to index and send to backend

  ticket = Ticket.find(123)
  result = ticket.search_index_update_backend

returns

  result = true # false

=end

  def search_index_update_backend
    # fill up with search data
    attributes = search_index_attribute_lookup
    return true if !attributes

    # update backend
    SearchIndexBackend.add(self.class.to_s, attributes)
    true
  end

  def ignore_search_indexing?(_action)
    false
  end

  # methods defined here are going to extend the class, not the instance of it
  class_methods do # rubocop:disable Metrics/BlockLength

=begin

serve method to ignore model attributes in search index

class Model < ApplicationModel
  include HasSearchIndexBackend
  search_index_attributes_ignored :password, :image
end

=end

    def search_index_attributes_ignored(*attributes)
      @search_index_attributes_ignored = attributes
    end

    def search_index_attributes_relevant(*attributes)
      @search_index_attributes_relevant = attributes
    end

=begin

reload search index with full data

  Model.search_index_reload

=end

    def search_index_reload(silent: false, worker: 0)
      tolerance       = 10
      tolerance_count = 0
      query           = reorder(created_at: :desc)
      total           = query.count
      record_count    = 0
      offset          = 0
      batch_size      = 200

      while query.offset(offset).limit(batch_size).count.positive?
        records = query.offset(offset).limit(batch_size)

        Parallel.map(records, { in_processes: worker }) do |record|
          next if record.ignore_search_indexing?(:destroy)

          begin
            record.search_index_update_backend
          rescue => e
            logger.error "Unable to send #{record.class}.find(#{record.id}).search_index_update_backend backend: #{e.inspect}"
            tolerance_count += 1
            sleep 15
            raise "Unable to send #{record.class}.find(#{record.id}).search_index_update_backend backend: #{e.inspect}" if tolerance_count == tolerance
          end
        end

        offset += batch_size

        next if silent

        record_count += records.count
        if (record_count % batch_size).zero? || record_count == total
          print "\r    #{record_count}/#{total}" # rubocop:disable Rails/Output
        end
      end
    end
  end
end