module ActiveRecord::Turntable
  module ActiveRecordExt
    module LockingOptimistic
      if Util.ar_version_equals_or_later?("5.1.6")
        ::ActiveRecord::Locking::Optimistic.class_eval <<-EOD
          private

          def _update_row(attribute_names, attempted_action = "update")
            return super unless locking_enabled?

            begin
              locking_column = self.class.locking_column
              previous_lock_value = read_attribute_before_type_cast(locking_column)
              attribute_names << locking_column

              self[locking_column] += 1

              constraints = {
                self.class.primary_key => id_in_database,
                locking_column => previous_lock_value
              }
              if self.class.sharding_condition_needed?
                constraints[self.class.turntable_shard_key] = self[self.class.turntable_shard_key]
              end

              if Util.ar52_or_later?
                affected_rows = self.class._update_record(
                  attributes_with_values(attribute_names),
                  constraints,
                )
              else
                affected_rows = self.class.unscoped._update_record(
                  arel_attributes_with_values(attribute_names),
                  constraints,
                )
              end

              if affected_rows != 1
                raise ActiveRecord::StaleObjectError.new(self, attempted_action)
              end

              affected_rows

            # If something went wrong, revert the locking_column value.
            rescue Exception
              self[locking_column] = previous_lock_value.to_i
              raise
            end
          end
        EOD
      elsif Util.ar51?
        ::ActiveRecord::Locking::Optimistic.class_eval <<-EOD
          private
          # @note Override to add sharding condition on optimistic locking
          def _update_record(attribute_names = self.attribute_names)
            return super unless locking_enabled?
            return 0 if attribute_names.empty?

            begin
              klass = self.class

              lock_col = self.class.locking_column

              previous_lock_value = read_attribute_before_type_cast(lock_col)

              increment_lock

              attribute_names.push(lock_col)

              relation = self.class.unscoped

              condition_scope = relation.where(
                self.class.primary_key => id,
                lock_col => previous_lock_value
              )
              if klass.turntable_enabled? and klass.primary_key != klass.turntable_shard_key.to_s
                condition_scope = condition_scope.where(
                  klass.turntable_shard_key => self.send(klass.turntable_shard_key)
                )
              end

              affected_rows = condition_scope.update_all(
                attributes_for_update(attribute_names).map do |name|
                  [name, _read_attribute(name)]
                end.to_h
              )

              unless affected_rows == 1
                raise ActiveRecord::StaleObjectError.new(self, "update")
              end

              affected_rows

            # If something went wrong, revert the locking_column value.
            rescue Exception
              send(lock_col + "=", previous_lock_value.to_i)
              raise
            end
          end
        EOD
      elsif Util.earlier_than_ar51?
        ::ActiveRecord::Locking::Optimistic.class_eval <<-EOD
          private
          # @note Override to add sharding condition on optimistic locking
          def _update_record(attribute_names = self.attribute_names) #:nodoc:
            return super unless locking_enabled?
            return 0 if attribute_names.empty?

            klass = self.class
            lock_col = self.class.locking_column
            previous_lock_value = send(lock_col).to_i
            increment_lock

            attribute_names += [lock_col]
            attribute_names.uniq!

            begin
              relation = self.class.unscoped

              condition_scope = relation.where(
                self.class.primary_key => id,
                lock_col => previous_lock_value,
              )
              if klass.turntable_enabled? and klass.primary_key != klass.turntable_shard_key.to_s
                condition_scope = condition_scope.where(
                  klass.turntable_shard_key => self.send(klass.turntable_shard_key)
                )
              end

              affected_rows = condition_scope.update_all(
                attributes_for_update(attribute_names).map do |name|
                  [name, _read_attribute(name)]
                end.to_h
              )

              unless affected_rows == 1
                raise ActiveRecord::StaleObjectError.new(self, "update")
              end

              affected_rows

              # If something went wrong, revert the version.
            rescue Exception
              send(lock_col + '=', previous_lock_value)
              raise
            end
          end
        EOD
      end
    end
  end
end
