module Gitlab
  module BackgroundMigration
    # Class that migrates events for the new push event payloads setup. All
    # events are copied to a shadow table, and push events will also have a row
    # created in the push_event_payloads table.
    class MigrateEventsToPushEventPayloads
      class Event < ActiveRecord::Base
        self.table_name = 'events'

        serialize :data

        BLANK_REF = ('0' * 40).freeze
        TAG_REF_PREFIX = 'refs/tags/'.freeze
        MAX_INDEX = 69
        PUSHED = 5

        def push_event?
          action == PUSHED && data.present?
        end

        def commit_title
          commit = commits.last

          return nil unless commit && commit[:message]

          index = commit[:message].index("\n")
          message = index ? commit[:message][0..index] : commit[:message]

          message.strip.truncate(70)
        end

        def commit_from_sha
          if create?
            nil
          else
            data[:before]
          end
        end

        def commit_to_sha
          if remove?
            nil
          else
            data[:after]
          end
        end

        def data
          super || {}
        end

        def commits
          data[:commits] || []
        end

        def commit_count
          data[:total_commits_count] || 0
        end

        def ref
          data[:ref]
        end

        def trimmed_ref_name
          if ref_type == :tag
            ref[10..-1]
          else
            ref[11..-1]
          end
        end

        def create?
          data[:before] == BLANK_REF
        end

        def remove?
          data[:after] == BLANK_REF
        end

        def push_action
          if create?
            :created
          elsif remove?
            :removed
          else
            :pushed
          end
        end

        def ref_type
          if ref.start_with?(TAG_REF_PREFIX)
            :tag
          else
            :branch
          end
        end
      end

      class EventForMigration < ActiveRecord::Base
        self.table_name = 'events_for_migration'
      end

      class PushEventPayload < ActiveRecord::Base
        self.table_name = 'push_event_payloads'

        enum action: {
          created: 0,
          removed: 1,
          pushed: 2
        }

        enum ref_type: {
          branch: 0,
          tag: 1
        }
      end

      # start_id - The start ID of the range of events to process
      # end_id - The end ID of the range to process.
      def perform(start_id, end_id)
        return unless migrate?

        find_events(start_id, end_id).each { |event| process_event(event) }
      end

      def process_event(event)
        replicate_event(event)
        create_push_event_payload(event) if event.push_event?
      end

      def replicate_event(event)
        new_attributes = event.attributes
          .with_indifferent_access.except(:title, :data)

        EventForMigration.create!(new_attributes)
      rescue ActiveRecord::InvalidForeignKey
        # A foreign key error means the associated event was removed. In this
        # case we'll just skip migrating the event.
      end

      def create_push_event_payload(event)
        commit_from = pack(event.commit_from_sha)
        commit_to = pack(event.commit_to_sha)

        PushEventPayload.create!(
          event_id: event.id,
          commit_count: event.commit_count,
          ref_type: event.ref_type,
          action: event.push_action,
          commit_from: commit_from,
          commit_to: commit_to,
          ref: event.trimmed_ref_name,
          commit_title: event.commit_title
        )
      rescue ActiveRecord::InvalidForeignKey
        # A foreign key error means the associated event was removed. In this
        # case we'll just skip migrating the event.
      end

      def find_events(start_id, end_id)
        Event
          .where('NOT EXISTS (SELECT true FROM events_for_migration WHERE events_for_migration.id = events.id)')
          .where(id: start_id..end_id)
      end

      def migrate?
        Event.table_exists? && PushEventPayload.table_exists? &&
          EventForMigration.table_exists?
      end

      def pack(value)
        value ? [value].pack('H*') : nil
      end
    end
  end
end
