# frozen_string_literal: true

module Deployments
  # Service class for linking merge requests to deployments.
  class LinkMergeRequestsService
    attr_reader :deployment

    # The number of commits per query for which to find merge requests.
    COMMITS_PER_QUERY = 5_000

    def initialize(deployment)
      @deployment = deployment
    end

    def execute
      # Review apps have the environment type set (e.g. to `review`, though the
      # exact value may differ). We don't want to link merge requests to review
      # app deployments, as this is not useful.
      return unless deployment.environment.should_link_to_merge_requests?

      # This service is triggered by a Sidekiq worker, which only runs when a
      # deployment is successful. We add an extra check here in case we ever
      # call this service elsewhere and forget to check the status there.
      #
      # The reason we only want to link successful deployments is as follows:
      # when we link a merge request, we don't link it to future deployments for
      # the same environment. If we were to link an MR to a failed deploy, we
      # wouldn't be able to later on link it to a successful deploy (e.g. after
      # the deploy is retried).
      #
      # In addition, showing failed deploys in the UI of a merge request isn't
      # useful to users, as they can't act upon the information in any
      # meaningful way (i.e. they can't just retry the deploy themselves).
      return unless deployment.success?

      if (prev = deployment.previous_deployment)
        link_merge_requests_for_range(prev.sha, deployment.sha)
      else
        # When no previous deployment is found we fall back to linking all merge
        # requests merged into the deployed branch. This will not always be
        # accurate, but it's better than having no data.
        #
        # We can't use the first commit in the repository as a base to compare
        # to, as this will not scale to large repositories. For example, GitLab
        # itself has over 150 000 commits.
        link_all_merged_merge_requests
      end
    end

    def link_merge_requests_for_range(from, to)
      commits = project
        .repository
        .commits_between(from, to)
        .map(&:id)

      # For some projects the list of commits to deploy may be very large. To
      # ensure we do not end up running SQL queries with thousands of WHERE IN
      # values, we run one query per a certain number of commits.
      #
      # In most cases this translates to only a single query. For very large
      # deployment we may end up running a handful of queries to get and insert
      # the data.
      commits.each_slice(COMMITS_PER_QUERY) do |slice|
        merge_requests =
          project.merge_requests.merged.by_merge_commit_sha(slice)

        deployment.link_merge_requests(merge_requests)

        # The cherry picked commits are tracked via `notes.commit_id`
        # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22209
        #
        # NOTE: cross-joining `merge_requests` table and `notes` table could
        # result in very poor performance because PG planner often uses an
        # inappropriate index.
        # See https://gitlab.com/gitlab-org/gitlab/-/issues/321032.
        mr_ids = project.notes.cherry_picked_merge_requests(slice)
        picked_merge_requests = project.merge_requests.id_in(mr_ids)

        deployment.link_merge_requests(picked_merge_requests)
      end
    end

    def link_all_merged_merge_requests
      merge_requests =
        project.merge_requests.merged.by_target_branch(deployment.ref)

      deployment.link_merge_requests(merge_requests)
    end

    private

    def project
      deployment.project
    end
  end
end
