module Jekyll::Algolia::Indexer

def self.index(index_name)

index_name - String name of the index

Public: Returns an Algolia Index object from an index name
def self.index(index_name)
  ::Algolia::Index.new(index_name)
end

def self.index?(index_name)

instead, which will fail if the index does not exist
Note: there is no API endpoint to do that, so we try to get the settings

index_name - Name of the index

Public: Check if an index exists
def self.index?(index_name)
  index(index_name).get_settings
  return true
rescue StandardError
  return false
end

def self.init

User Agent and give an easy access to the main index
This call will instanciate the Algolia API client, set the custom

Public: Init the module
def self.init
  ::Algolia.init(
    application_id: Configurator.application_id,
    api_key: Configurator.api_key
  )
  set_user_agent
end

def self.local_object_ids(records)

records - Array of all local records

Public: Returns an array of the local objectIDs
def self.local_object_ids(records)
  records.map { |record| record[:objectID] }.compact.sort
end

def self.remote_object_ids(index)

processed, but makes debugging easier when comparing arrays is needed.
The returned array is sorted. It won't have any impact on the way it is

index - Algolia Index to target

Public: Returns an array of all the objectIDs in the index
def self.remote_object_ids(index)
  list = []
  Logger.verbose(
    "I:Inspecting existing records in index #{index.name}..."
  )
  begin
    index.browse(attributesToRetrieve: 'objectID') do |hit|
      list << hit['objectID']
    end
  rescue StandardError
    # The index might not exist if it's the first time we use the plugin
    # so we'll consider that it means there are no records there
    return []
  end
  list.sort
end

def self.run(records)

records - Records to push

Public: Push all records to Algolia and configure the index
def self.run(records)
  init
  # Indexing zero record is surely a misconfiguration
  if records.length.zero?
    files_to_exclude = Configurator.algolia('files_to_exclude').join(', ')
    Logger.known_message(
      'no_records_found',
      'files_to_exclude' => files_to_exclude,
      'nodes_to_index' => Configurator.algolia('nodes_to_index')
    )
    exit 1
  end
  index_name = Configurator.index_name
  index = index(index_name)
  # Update settings
  update_settings(index)
  # Getting list of objectID in remote and locally
  remote_ids = remote_object_ids(index)
  local_ids = local_object_ids(records)
  # Getting list of what to add and what to delete
  old_records_ids = remote_ids - local_ids
  new_records_ids = local_ids - remote_ids
  new_records = records.select do |record|
    new_records_ids.include?(record[:objectID])
  end
  update_records(index_name, old_records_ids, new_records)
  Logger.log('I:✔ Indexing complete')
end

def self.set_user_agent

are explicit in defining it to help debug from the dashboard.
each integration version is pinned to a specific API client version, we
every API client should follow the "Algolia for YYY" pattern. Even if
Every integrations should follow the "YYY Integration" pattern, and

Public: Set the User-Agent to send to the API
def self.set_user_agent
  user_agent = [
    "Jekyll Integration (#{VERSION})",
    "Algolia for Ruby (#{::Algolia::VERSION})",
    "Jekyll (#{::Jekyll::VERSION})",
    "Ruby (#{RUBY_VERSION})"
  ].join('; ')
  ::Algolia.set_extra_header('User-Agent', user_agent)
end

def self.update_records(index_name, old_records_ids, new_records)

Does nothing in dry run mode
update
Note: All operations will be done in one batch, assuring an atomic

new_records - Records to add to the index
old_records_ids - Ids of records to delete from the index
index_name - The Algolia index

Public: Update records of the index
def self.update_records(index_name, old_records_ids, new_records)
  # Stop if nothing to change
  if old_records_ids.empty? && new_records.empty?
    Logger.log('I:Nothing to index. Your content is already up to date.')
    return
  end
  Logger.log("I:Updating records in index #{index_name}...")
  Logger.log("I:Records to delete: #{old_records_ids.length}")
  Logger.log("I:Records to add:    #{new_records.length}")
  return if Configurator.dry_run?
  operations = new_records.map do |new_record|
    { action: 'addObject', indexName: index_name, body: new_record }
  end
  old_records_ids.each do |object_id|
    operations << {
      action: 'deleteObject', indexName: index_name,
      body: { objectID: object_id }
    }
  end
  # Run the batches in slices if they are too large
  batch_size = Configurator.algolia('indexing_batch_size')
  operations.each_slice(batch_size) do |slice|
    begin
      ::Algolia.batch!(slice)
    rescue StandardError => error
      records = slice.map do |record|
        record[:body]
      end
      ErrorHandler.stop(error, records: records)
    end
  end
end

def self.update_settings(index)

users to configure them through their dashboard.
are defined in _config.yml. Otherwise, they are left untouched, allowing
Settings will only be updated in the first push, and if custom settings
Does nothing in dry run mode

index - The Algolia Index

Public: Update settings of the index
def self.update_settings(index)
  has_custom_settings = !Configurator.algolia('settings').nil?
  index_exists = index?(index.name)
  # No need to update the settings if the index is already configured and
  # the user did not specify custom settings
  return if index_exists && !has_custom_settings
  Logger.verbose('I:Updating settings')
  return if Configurator.dry_run?
  settings = Configurator.settings
  begin
    index.set_settings!(settings)
  rescue StandardError => error
    ErrorHandler.stop(error, settings: settings)
  end
end