# frozen_string_literal: true

module ReleaseTools
  class Commits
    include ::SemanticLogger::Loggable

    MAX_COMMITS_TO_CHECK = 100

    def initialize(project, ref: project.default_branch, client: ReleaseTools::GitlabClient)
      @project = project
      @ref = ref
      @client = client
    end

    def merge_base(other_ref)
      @client.merge_base(@project.auto_deploy_path, [@ref, other_ref])&.id
    end

    # Get the latest commit for `ref`
    def latest
      commit_list.first
    end

    delegate :detect, to: :commit_list

    # Find a commit with a passing build on production that also exists on dev.
    # The definition of success changes based on the operation, a predicate block
    # must be provided that receives a commit id and returns if it is a success.
    #
    # @param since_last_auto_deploy [boolean] if true, it will limit the search to
    #   the merge base with the SHA of last auto_deploy package
    # @yieldparam commit_id [string] The commit ID to validate for success
    # @yieldreturn [boolean] whatever the provided commit should be considered successful
    def latest_successful_on_build(since_last_auto_deploy: true)
      limit = find_last_auto_deploy_limit_sha if since_last_auto_deploy

      commit_list.detect do |commit|
        if commit.id == limit
          logger.info('Reached the limit commit', project: @project.auto_deploy_path, limit: limit)

          next true
        end

        next unless yield(commit.id)

        begin
          # Hit the dev API with the specified commit to see if it even exists
          Retriable.with_context(:mirroring) do
            ReleaseTools::GitlabDevClient
              .commit(@project.dev_path, ref: commit.id)
          end

          logger.info(
            'Passing commit found on Build',
            project: @project.auto_deploy_path,
            commit: commit.id
          )
        rescue Gitlab::Error::Error
          logger.debug(
            'Commit passed on Canonical, missing on Build',
            project: @project.auto_deploy_path,
            commit: commit.id
          )

          false
        end
      end
    end

    # @param commit_id [String] is a commit in the commit_list.
    # @return [ObjectifiedHash, nil] Returns the next chronologically newer commit after commit_id.
    #         Returns nil if commit_id doesn't exist in commit_list, or if there is no
    #         commit newer than commit_id.
    def next_commit(commit_id)
      index = commit_list.index { |c| c.id == commit_id }
      return if index.nil? || index.zero?

      # Since the commit_list is in descending chronological order, the previous commit
      # in the list is the next newer commit.
      commit_list[index - 1]
    end

    def find_last_auto_deploy_limit_sha
      # Helm auto-deploy support is incomplete and we are not tracking it
      return unless [Project::GitlabEe, Project::OmnibusGitlab, Project::CNGImage].include?(@project)

      return @find_last_auto_deploy_limit_sha if defined?(@find_last_auto_deploy_limit_sha)

      product_version = ProductVersion.last_auto_deploy
      last_pkg_sha = product_version[@project.metadata_project_name]&.sha
      logger.info('Finding auto_deploy limit SHA', project: @project, last_product_version: product_version.version, last_pkg_sha: last_pkg_sha)
      # TODO (rpereira2): Raise an error here instead of silently returning nil?
      return unless last_pkg_sha

      @find_last_auto_deploy_limit_sha =
        merge_base(last_pkg_sha).tap do |id|
          logger.info('Found auto_deploy limit SHA', limit_sha: id)
        end
    end

    private

    def commit_list
      @commit_list ||= @client.commits(
        @project.auto_deploy_path,
        per_page: MAX_COMMITS_TO_CHECK,
        ref_name: @ref
      )
    end
  end
end
