#!/usr/bin/env ruby
# frozen_string_literal: true

####
# Prints a report which helps reconcile occurrences of the `QueryLimiting.disable(ISSUE_LINK)`
# allowlist block against the corresponding open issues.
#
# If everything is consistent, the script should ideally not report any issues or code lines,
# other than possibly remaining "calls with no issue iid" which use variables/etc.
#
# - See https://gitlab.com/gitlab-org/gitlab/-/issues/325640
# - See https://gitlab.com/groups/gitlab-org/-/epics/5670

# We need to take some precautions when using the `gitlab` gem in this project.
#
# See https://docs.gitlab.com/ee/development/pipelines/internals.html#using-the-gitlab-ruby-gem-in-the-canonical-project.
require 'gitlab'
require 'optparse'
require 'rubygems'

class QueryLimitingReport
  GITLAB_PROJECT_ID = 278964 # gitlab-org/gitlab project
  ISSUES_SEARCH_LABEL = 'querylimiting-disable'
  CODE_LINES_SEARCH_STRING = 'QueryLimiting.disable'
  PAGINATION_LIMIT = 500

  DEFAULT_OPTIONS = {
    api_token: ENV['API_TOKEN']
  }.freeze

  def initialize(options)
    @options = options

    Gitlab.configure do |config|
      config.endpoint = 'https://gitlab.com/api/v4'
      config.private_token = options.fetch(:api_token)
    end
  end

  def execute
    # PLAN:
    # Read all issues matching criteria and extract array of issue iids
    # Find all code references and extract issue iids
    # Print list of all issues without code references
    # Print list of all code references issue iids that don't have search label
    # Print list of all code references with no issue iids (i.e. dynamic or variable argument)

    total_issues = find_issues_by_label(ISSUES_SEARCH_LABEL)
    issues = total_issues.select { |issue| issue[:state] == 'opened' }
    code_lines = find_code_lines

    code_lines_grouped = code_lines.group_by { |code_line| code_line[:has_issue_iid] }
    code_lines_without_issue_iid = code_lines_grouped[false]
    code_lines_with_issue_iid = code_lines_grouped[true]

    all_issue_iids_in_code_lines = code_lines_with_issue_iid.map { |line| line[:issue_iid] }

    issues_without_code_references = issues.reject do |issue|
      all_issue_iids_in_code_lines.include?(issue[:iid])
    end

    all_issue_iids = issues.map { |issue| issue[:iid] }
    code_lines_with_missing_issues = code_lines_with_issue_iid.reject do |code_line|
      all_issue_iids.include?(code_line[:issue_iid])
    end

    puts "\n\n\nREPORT:"

    puts "\n\nFound #{total_issues.length} total issues with '#{ISSUES_SEARCH_LABEL}' search label, #{issues.length} are still opened..."
    puts "\n\nFound #{code_lines.length} total occurrences of '#{CODE_LINES_SEARCH_STRING}' in code..."

    puts "\n" + ('-' * 80)

    puts "\n\nIssues without any '#{CODE_LINES_SEARCH_STRING}' code references (#{issues_without_code_references.length} total):"
    pp issues_without_code_references

    puts "\n" + ('-' * 80)

    puts "\n\n'#{CODE_LINES_SEARCH_STRING}' calls with references to an issue which doesn't have '#{ISSUES_SEARCH_LABEL}' search label (#{code_lines_with_missing_issues.length} total):"
    pp code_lines_with_missing_issues

    puts "\n" + ('-' * 80)

    puts "\n\n'#{CODE_LINES_SEARCH_STRING}' calls with no issue iid (#{code_lines_without_issue_iid&.length || 0} total):"
    pp code_lines_without_issue_iid
  end

  private

  attr_reader :options

  def find_issues_by_label(label)
    issues = []

    puts("Finding issues by label #{label}...")
    paginated_issues = Gitlab.issues(GITLAB_PROJECT_ID, 'labels' => label)
    paginated_issues.paginate_with_limit(PAGINATION_LIMIT) do |item|
      item_hash = item.to_hash

      issue_iid = item_hash.fetch('iid')
      issue = {
        iid: issue_iid,
        state: item_hash.fetch('state'),
        title: item_hash.fetch('title'),
        issue_url: "https://gitlab.com/gitlab-org/gitlab/issues/#{issue_iid}"
      }

      issues << issue
    end

    issues
  end

  def find_code_lines
    code_lines = []

    puts("Finding code lines...")
    paginated_blobs = Gitlab.search_in_project(GITLAB_PROJECT_ID, 'blobs', CODE_LINES_SEARCH_STRING)
    paginated_blobs.paginate_with_limit(PAGINATION_LIMIT) do |item|
      item_hash = item.to_hash

      filename = item_hash.fetch('filename')
      next unless /\.rb\Z/.match?(filename)

      file_contents = Gitlab.file_contents(GITLAB_PROJECT_ID, filename)
      file_lines = file_contents.split("\n")

      file_lines.each_index do |index|
        line = file_lines[index]
        next unless /#{CODE_LINES_SEARCH_STRING}/o.match?(line)

        issue_iid = line.slice(%r{issues/(\d+)\D}, 1)
        line_number = index + 1
        code_line = {
          file_location: "#{filename}:#{line_number}",
          filename: filename,
          line_number: line_number,
          line: line,
          issue_iid: issue_iid.to_i,
          has_issue_iid: !issue_iid.nil?
        }
        code_lines << code_line
      end
    end

    code_lines.sort_by! { |line| "#{line[:filename]}-#{line[:line_number].to_s.rjust(4, '0')}" }
    code_lines.map do |line|
      line.delete(:filename)
      line.delete(:line_number)
      line
    end
  end
end

if $PROGRAM_NAME == __FILE__
  options = QueryLimitingReport::DEFAULT_OPTIONS.dup

  OptionParser.new do |opts|
    opts.on("-t", "--api-token API_TOKEN", String, "A value API token with the `read_api` scope. Can be set as an env variable 'API_TOKEN'.") do |value|
      options[:api_token] = value
    end

    opts.on("-h", "--help", "Prints this help") do
      puts opts
      exit
    end
  end.parse!

  QueryLimitingReport.new(options).execute
end
