# frozen_string_literal: true

module ReleaseTools
  module Security
    class ImplementationIssue
      include ::SemanticLogger::Loggable

      # Number of merge requests that has to be associated to every Security Issue
      MERGE_REQUESTS_SIZE = 4

      # Label indicating issue doesn't require full backports
      BACKPORT_EXCEPTION_LABEL = 'reduced backports'

      # Internal ID of GitLab Release Bot
      GITLAB_RELEASE_BOT_ID = 2_324_599

      # Format of stable branches on GitLab repos
      STABLE_BRANCH_REGEX = /^(\d+-\d+-stable(-ee)?)$/

      # Collection of security projects that are allowed to early merge
      PROJECTS_ALLOWED_TO_EARLY_MERGE = [
        Project::GitlabEe.security_id,
        Project::OmnibusGitlab.security_id,
        Project::CNGImage.security_id
      ].freeze

      DEFAULT_BRANCHES = %w[main master].freeze # rubocop:disable ReleaseTools/DefaultBranchLiteral

      # The status of merge requests to be considered
      OPENED = 'opened'

      MERGED = 'merged'

      ALLOWED_DEFAULT_MR_STATE = [OPENED, MERGED].freeze

      attr_reader :project_id, :iid, :web_url, :reference, :merge_requests, :issue

      delegate :labels, to: :@issue
      delegate :assignees, to: :@issue
      delegate :author, to: :@issue
      delegate :title, to: :@issue

      # this allows usage of GitLabClient methods that use `project_path`
      alias project project_id

      def initialize(issue, merge_requests)
        @issue = issue
        @project_id = issue.project_id
        @iid = issue.iid
        @web_url = issue.web_url
        @reference = issue.references.full

        @merge_requests = merge_requests
      end

      def ready_to_be_processed?
        validate

        pending_reasons.empty?
      end

      def pending_reasons
        return [] unless @reasons

        @reasons
      end

      def merge_requests_targeting_default_branch
        @merge_requests_targeting_default_branch ||=
          merge_requests
            .select { |merge_request| DEFAULT_BRANCHES.include?(merge_request.target_branch) }
      end

      # Only returns the first MR targeting the default branch
      def merge_request_targeting_default_branch
        @merge_request_targeting_default_branch ||= merge_requests_targeting_default_branch.first
      end

      def default_merge_request_handled?
        return merge_request_targeting_default_branch.state == MERGED if merge_request_targeting_default_branch.present?

        @issue.labels.include?(BACKPORT_EXCEPTION_LABEL)
      end

      def mwps_set_on_default_merge_request?
        merge_request_targeting_default_branch.present? &&
          merge_request_targeting_default_branch.state == OPENED &&
          merge_request_targeting_default_branch.merge_when_pipeline_succeeds
      end

      def default_merge_request_deployed?
        return false unless merge_request_targeting_default_branch

        # Check if the API returns the default branch MR when filtering by environment
        mrs = GitlabClient.merge_requests(
          merge_request_targeting_default_branch.project_id,
          {
            iids: [merge_request_targeting_default_branch.iid],
            environment: 'gprd'
          }
        )

        # Check if the first_deployed_to_production_at attribute is a reliable way to check if an MR has been deployed
        # to gprd.
        logger.info(
          'Security default branch MR deployed to gprd?',
          environment_filter: !mrs.empty?,
          first_deployed_to_production_at: merge_request_targeting_default_branch.first_deployed_to_production_at.present?,
          deployment_timestamp: merge_request_targeting_default_branch.first_deployed_to_production_at,
          mr_url: merge_request_targeting_default_branch.web_url
        )

        !mrs.empty?
      end

      def backports
        @backports ||= merge_requests
          .select { |merge_request| merge_request.target_branch.match?(STABLE_BRANCH_REGEX) }
      end

      def backports_merged?
        !backports.empty? && backports.all? { |mr| mr.state == MERGED }
      end

      def processed?
        merge_requests.all? { |mr| mr.state == MERGED }
      end

      def allowed_to_early_merge?
        merge_request_targeting_default_branch&.state == OPENED &&
          PROJECTS_ALLOWED_TO_EARLY_MERGE.include?(merge_request_targeting_default_branch.project_id)
      end

      def pending_merge_requests
        if PROJECTS_ALLOWED_TO_EARLY_MERGE.include?(project_id)
          backports
        else
          opened_merge_requests
        end
      end

      def security_target?
        @issue.labels.include?(ReleaseTools::Security::IssueCrawler::SECURITY_TARGET_LABEL)
      end

      def cves_issue
        issue = RelatedIssuesFinder.new(self).cves_issue

        return unless issue

        CvesIssue.new(issue)
      end

      def canonical_issue
        RelatedIssuesFinder.new(self).canonical_issue
      end

      def opened_merge_requests_targeting_default_branch
        merge_requests_targeting_default_branch.select { |merge_request| merge_request.state == OPENED }
      end

      private

      def validate
        return if defined?(@reasons)

        @reasons = []

        validate_merge_requests_present
        validate_missing_backports
        validate_default_mr_status
        validate_backport_mr_status
        validate_merge_requests_assigned_to_the_bot
        validate_merge_requests
      end

      def reject(reason)
        logger.warn("Rejecting implementation issue due to #{reason}", url: web_url)

        @reasons << reason
      end

      def validate_merge_requests_present
        return unless merge_requests.empty?

        reject(
          <<~EOF.squish
            There are no MRs linked to this issue. Follow the steps in the issue description to
            create the required MRs.
          EOF
        )
      end

      def validate_missing_backports
        return unless
          merge_requests.length < MERGE_REQUESTS_SIZE &&
          @issue.labels.exclude?(BACKPORT_EXCEPTION_LABEL)

        reject(
          <<~EOF.squish
            Every security issue is expected to have 4 MRs; 3 backports targeting the versions in the
            [next scheduled patch release](https://gitlab.com/gitlab-org/gitlab/-/issues/?label_name%5B%5D=upcoming%20security%20release)
            and 1 MR targeting master. If this issue legitimately does not need to have all 4 MRs,
            please add the ~"reduced backports" label to this issue
          EOF
        )
      end

      def validate_merge_requests_assigned_to_the_bot
        unassigned_mrs = merge_requests.reject do |merge_request|
          merge_request
            .assignees
            .map { |assignee| assignee.to_h.transform_keys(&:to_sym)[:id] }.include?(GITLAB_RELEASE_BOT_ID)
        end

        return if unassigned_mrs.empty?

        reject(
          <<~EOF.squish
            The following merge requests need to be assigned to the `@gitlab-release-tools-bot`: #{unassigned_mrs.map(&:web_url).join(', ')}
          EOF
        )
      end

      def validate_default_mr_status
        return unless merge_request_targeting_default_branch

        default_branch_state = merge_request_targeting_default_branch.state
        return if ALLOWED_DEFAULT_MR_STATE.include?(default_branch_state)

        reject(
          <<~EOF.squish
            The [MR targeting the default branch](#{merge_request_targeting_default_branch.web_url}) needs
            to be in one of the following states: #{ALLOWED_DEFAULT_MR_STATE.join(', ')}
          EOF
        )
      end

      def validate_backport_mr_status
        return if backports.all? { |mr| mr.state == OPENED }

        reject(
          <<~EOF.squish
            The following backport MRs need to be in the 'opened' state:
            #{backports.reject { |mr| mr.state == OPENED }.map(&:web_url).join(', ')}
          EOF
        )
      end

      def validate_merge_requests
        invalid_mrs = ReleaseTools::Security::MergeRequestsValidator
          .new(ReleaseTools::Security::Client.new)
          .execute(merge_requests: opened_merge_requests)
          .last

        return if invalid_mrs.empty?

        reject(
          <<~EOF.squish
            The following merge requests are not ready. A comment has been posted in each MR with more details:
            #{invalid_mrs.map(&:web_url).join(', ')}
          EOF
        )
      end

      def opened_merge_requests
        merge_requests
            .select { |merge_request| merge_request.state == OPENED }
      end
    end
  end
end
