# frozen_string_literal: true

module ReleaseTools
  module Security
    # Validating of multiple security merge requests in batches
    class MergeRequestsValidator
      include ::SemanticLogger::Loggable
      include ParallelMethods

      ERROR_TEMPLATE = <<~TEMPLATE.strip
        @%<author_username>s

        This security merge request does not meet our requirements for
        security merge requests. Please take the following steps to ensure
        this merge request can be merged:

        1. Resolve all the errors listed below
        2. Assign the merge request back to @%<bot_username>s as an Assignee
        3. Make sure your security implementation issue has the ~"security-target" label
           applied or your issue will not be linked to the security tracking issue.
           See https://gitlab.com/gitlab-org/release/docs/-/blob/master/general/security/engineer.md#process
           for the full process.

        ## Errors

        The following errors were detected:

        %<errors>s

        #{Messaging::COMMENT_FOOTNOTE}
      TEMPLATE

      # @param [ReleaseTools::Security::Client|ReleaseTools::Security::DevClient] client
      def initialize(client)
        @client = client
        @valid = []
        @invalid = []
      end

      # Validates all security merge requests, returning those that were valid.
      #
      # The valid and invalid merge requests are returned so that other code can
      # use these MRs, for example by merging them.
      #
      # The return value is an Array of Arrays, in the following format:
      #
      #     [
      #       [valid_merge_request1, valid_merge_request2, ...],
      #       [invalid_merge_request1, invalid_merge_request2, ...]
      #     ]
      #
      def execute(merge_requests: [])
        validate_merge_requests(merge_requests)

        [@valid, @invalid]
      end

      # @param [Gitlab::ObjectifiedHash] basic_mr
      def validate_merge_request(basic_mr)
        logger.trace(__method__, merge_request: basic_mr.web_url)

        # Merge requests retrieved using the MR list API do not include all data
        # we need, such as pipeline details. To work around this we must perform
        # an additional request for every merge request to get this data.
        mr = @client.merge_request(basic_mr.project_id, basic_mr.iid)
        validator = MergeRequestValidator.new(mr, @client)

        validator.validate

        if validator.errors.any?
          reassign_with_errors(mr, validator.errors)
          cancel_mwps(mr)

          [false, mr]
        else
          [true, mr]
        end
      end

      # @param [Gitlab::ObjectifiedHash] mr
      # @param [Array<String>] errors
      def reassign_with_errors(mr, errors)
        logger.trace(__method__, merge_request: mr.web_url, errors: errors.count)

        return if SharedStatus.dry_run?

        project_id = mr.project_id
        iid = mr.iid
        sec_release_tracking_issue_search_link = 'https://gitlab.com/gitlab-org/gitlab/-/issues?label_name%5B%5D=upcoming%20security%20release'

        @client.create_merge_request_note(
          project_id,
          iid,
          format(
            ERROR_TEMPLATE,
            author_username: mr.author.username,
            bot_username: Client::RELEASE_TOOLS_BOT_USERNAME,
            sec_release_tracking_issue_search_link: sec_release_tracking_issue_search_link,
            errors: errors.join("\n\n")
          )
        )

        @client.update_merge_request(project_id, iid, assignee_id: mr.author.id)
      end

      private

      def cancel_mwps(merge_request)
        return unless merge_request.merge_when_pipeline_succeeds
        return if SharedStatus.dry_run?

        logger.info('Canceling MWPS', merge_request: merge_request.web_url)

        Retriable.with_context(:api) do
          @client.cancel_merge_when_pipeline_succeeds(merge_request)
        end
      rescue ::Gitlab::Error::MethodNotAllowed, ::Gitlab::Error::NotAcceptable
        # 405 MethodNotAllowed is returned when the MR is already merged or closed
        # 406 NotAcceptable is returned when MWPS is not set
        # We can ignore these errors and continue in these cases
        logger.info('MWPS is not set or MR is already merged or closed. Skipping.')

        nil
      end

      def validate_merge_requests(merge_requests)
        parallel_map(merge_requests) do |merge_request|
          is_valid, mr = validate_merge_request(merge_request)

          if is_valid
            @valid << mr
          else
            @invalid << mr
          end
        end
      end
    end
  end
end
