# frozen_string_literal: true

module Gitlab
  module Ci
    module Reports
      module Security
        class VulnerabilityReportsComparer
          include Gitlab::Utils::StrongMemoize

          attr_reader :base_report, :head_report

          ACCEPTABLE_REPORT_AGE = 1.week

          def initialize(project, base_report, head_report)
            @base_report = base_report
            @head_report = head_report

            @signatures_enabled = project.licensed_feature_available?(:vulnerability_finding_signatures)

            if @signatures_enabled
              @added_findings = []
              @fixed_findings = []
              calculate_changes
            end
          end

          def base_report_created_at
            @base_report.created_at
          end

          def head_report_created_at
            @head_report.created_at
          end

          def base_report_out_of_date
            return false unless @base_report.created_at

            ACCEPTABLE_REPORT_AGE.ago > @base_report.created_at
          end

          def added
            strong_memoize(:added) do
              if @signatures_enabled
                @added_findings
              else
                head_report.findings - base_report.findings
              end
            end
          end

          def fixed
            strong_memoize(:fixed) do
              if @signatures_enabled
                @fixed_findings
              else
                base_report.findings - head_report.findings
              end
            end
          end

          private

          def calculate_changes
            # This is a deconstructed version of the eql? method on
            # Ci::Reports::Security::Finding. It:
            #
            # * precomputes for the head_findings (using FindingMatcher):
            #   * sets of signature shas grouped by priority
            #   * mappings of signature shas to the head finding object
            #
            # These are then used when iterating the base findings to perform
            # fast(er) prioritized, signature-based comparisons between each base finding
            # and the head findings.
            #
            # Both the head_findings and base_findings arrays are iterated once

            base_findings = base_report.findings
            head_findings = head_report.findings

            matcher = FindingMatcher.new(head_findings)

            base_findings.each do |base_finding|
              next if base_finding.requires_manual_resolution?

              matched_head_finding = matcher.find_and_remove_match!(base_finding)

              @fixed_findings << base_finding if matched_head_finding.nil?
            end

            @added_findings = matcher.unmatched_head_findings.values
          end
        end

        class FindingMatcher
          attr_reader :unmatched_head_findings, :head_findings

          include Gitlab::Utils::StrongMemoize

          def initialize(head_findings)
            @head_findings = head_findings
            @unmatched_head_findings = @head_findings.index_by(&:object_id)
          end

          def find_and_remove_match!(base_finding)
            matched_head_finding = find_matched_head_finding_for(base_finding)

            # no signatures matched, so check the normal uuids of the base and head findings
            # for a match
            matched_head_finding = head_signatures_shas[base_finding.uuid] if matched_head_finding.nil?

            @unmatched_head_findings.delete(matched_head_finding.object_id) unless matched_head_finding.nil?

            matched_head_finding
          end

          private

          def find_matched_head_finding_for(base_finding)
            base_signature = sorted_signatures_for(base_finding).find do |signature|
              # at this point a head_finding exists that has a signature with a
              # matching priority, and a matching sha --> lookup the actual finding
              # object from head_signatures_shas
              head_signatures_shas[signature.signature_sha].eql?(base_finding)
            end

            base_signature.present? ? head_signatures_shas[base_signature.signature_sha] : nil
          end

          def sorted_signatures_for(base_finding)
            base_finding.signatures.select { |signature| head_finding_signature?(signature) }
                                   .sort_by { |sig| -sig.priority }
          end

          def head_finding_signature?(signature)
            head_signatures_priorities[signature.priority].include?(signature.signature_sha)
          end

          def head_signatures_priorities
            strong_memoize(:head_signatures_priorities) do
              signatures_priorities = Hash.new { |hash, key| hash[key] = Set.new }

              head_findings.each_with_object(signatures_priorities) do |head_finding, memo|
                head_finding.signatures.each do |signature|
                  memo[signature.priority].add(signature.signature_sha)
                end
              end
            end
          end

          def head_signatures_shas
            strong_memoize(:head_signatures_shas) do
              head_findings.each_with_object({}) do |head_finding, memo|
                head_finding.signatures.each do |signature|
                  memo[signature.signature_sha] = head_finding
                end
                # for the final uuid check when no signatures have matched
                memo[head_finding.uuid] = head_finding
              end
            end
          end
        end
      end
    end
  end
end
