# frozen_string_literal: true

# This module enables you to break up your work into batches and run those
# batches as background jobs while keeping all your code in the same file.
# Including this module *implements perform* you should not override it.
# It expects you to implement two methods: #perform_batching and
# #perform_batch.
#
# Optionally you may also implement any combination of Sidekiq::Batch
# callbacks.
#   - #on_complete
#   - #on_success
#   - #on_death
#
# #perform_batching should contain your code for breaking up your work into
# smaller jobs. It handles all the Sidekiq::Batch boilerplate for you. Where
# you would normally call ExampleBatchJob.perform_async you should use
# #queue_batch. If you'd like to custommize the sidekiq batch object, you can
# access it in perform_batching through the `sidekiq_batch` method.
#
# #perform_batch should contain the code that would be in your batch job. Under
# the hood, #queue_batch queues a job which will run #perform_batch.
#
# [Sidekiq::Batch documentation](https://github.com/mperham/sidekiq/wiki/Batches)
# explains batches, their lifecycle, callbacks, etc.
#
# class ExampleJob
#   include Simplekiq::BatchingJob
#
#   def perform_batching(some_id)
#     sidekiq_batch.description = "My custom batch description" # optional
#
#     Record.find(some_id).other_records.in_batches do |other_records|
#       queue_batch(other_records.ids)
#     end
#   end
#
#   def perform_batch(other_record_ids)
#     OtherRecord.where(id: other_record_ids).do_work
#   end
#
#   def on_death(_status, options)
#     same_id_as_before = options["args"].first
#     Record.find(same_id_as_before).death!
#   end

#   def on_complete(_status, options)
#     same_id_as_before = options["args"].first
#     Record.find(same_id_as_before).complete!
#   end

#   def on_success(_status, options)
#     same_id_as_before = options["args"].first
#     Record.find(same_id_as_before).success!
#   end
# end
#
# ExampleJob.perform_async(some_id)
#
# Come home to the impossible flavor of batch creation

module Simplekiq
  module BatchingJob
    include Sidekiq::Worker

    BATCH_CLASS_NAME = "SimplekiqBatch"

    class << self
      def included(klass)
        batch_job_class = Class.new(BaseBatch)
        klass.const_set(BATCH_CLASS_NAME, batch_job_class)

        klass.extend ClassMethods
      end
    end

    module ClassMethods
      def batch_sidekiq_options(options)
        batch_class = const_get(BATCH_CLASS_NAME)
        batch_class.instance_eval do
          sidekiq_options(options)
        end
      end
    end

    def perform(*args)
      self.batches = []

      perform_batching(*args)

      # If we're part of an existing sidekiq batch make this a child batch
      # This is necessary for it work with orchestration; we could add an option
      # to toggle the behavior on and off.
      if batch
        batch.jobs do
          handle_batches(args)
        end
      else
        handle_batches(args)
      end
    end

    protected # TODO: should this be private?

    attr_accessor :batches

    def handle_batches(args)
      if !batches.empty?
        flush_batches(args)
      else
        Simplekiq.run_empty_callbacks(self, args: args)
      end
    end

    def flush_batches(args)
      batch_job_class = self.class.const_get(BATCH_CLASS_NAME)
      sidekiq_batch.description ||= "Simplekiq Batch Jobs for #{self.class.name}, args: #{args}"

      Simplekiq.auto_define_callbacks(sidekiq_batch, args: args, job: self)

      sidekiq_batch.jobs do
        batches.each do |job_args|
          batch_job_class.perform_async(*job_args)
        end
      end
    end

    def queue_batch(*args)
      batches << args
    end

    def batch_description=(description)
      sidekiq_batch.description = description
    end

    private

    def sidekiq_batch
      @sidekiq_batch ||= Sidekiq::Batch.new
    end
  end

  class BaseBatch
    include Sidekiq::Worker

    def perform(*args)
      module_parent_of_class.new.perform_batch(*args)
    end

    private

    def module_parent_of_class
      # Borrowed from https://apidock.com/rails/Module/module_parent_name
      parent_name = self.class.name =~ /::[^:]+\Z/ ? $`.freeze : nil
      parent_name ? Object.const_get(parent_name) : Object
    end
  end
end
