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

if Object.const_defined?(:RSpec)
  # Ok, we're testing, we know we're going to stub `Gitlab`, so we just ignore
else
  require 'gitlab'

  if Gitlab.singleton_class.method_defined?(:com?)
    abort 'lib/gitlab.rb is loaded, and this means we can no longer load the client and we cannot proceed'
  end
end

require 'time'

class PreMergeChecks
  DEFAULT_API_ENDPOINT                                  = "https://gitlab.com/api/v4"
  MERGE_TRAIN_REF_REGEX                                 = %r{\Arefs/merge-requests/\d+/train\z}
  PIPELINE_FRESHNESS_DEFAULT_THRESHOLD_IN_HOURS         = 8
  PIPELINE_FRESHNESS_STABLE_BRANCHES_THRESHOLD_IN_HOURS = 72
  PREDICTIVE_PIPELINE_IDENTIFIER                        = 'predictive'
  REQUIRED_TIER_IDENTIFIER                              = 'tier:3'
  STABLE_BRANCH_SUFFIX                                  = '-stable-ee'
  TIER_IDENTIFIER_REGEX                                 = /tier:\d/

  PreMergeChecksFailedError = Class.new(StandardError)
  PreMergeChecksStatus = Struct.new(:exitstatus, :message) do
    def success?
      exitstatus == 0
    end
  end

  def initialize(
    api_endpoint: ENV.fetch('CI_API_V4_URL', DEFAULT_API_ENDPOINT),
    project_id: ENV['CI_PROJECT_ID'],
    merge_request_iid: ENV['CI_MERGE_REQUEST_IID'],
    target_branch: ENV['CI_MERGE_REQUEST_TARGET_BRANCH_NAME'])
    @api_endpoint        = api_endpoint
    @project_id          = project_id
    @merge_request_iid   = merge_request_iid.to_i
    @target_branch       = target_branch
  end

  def execute
    check_required_ids!

    # Find the first non merge-train pipeline in project with id `project_id`
    latest_pipeline_id = api_client.merge_request_pipelines(project_id, merge_request_iid).auto_paginate do |pipeline|
      next if pipeline.ref.match?(MERGE_TRAIN_REF_REGEX)
      # We ensure we are comparing strings (not integers and strings)
      next if pipeline.project_id.to_s != project_id.to_s

      break pipeline.id
    end

    unless latest_pipeline_id
      fail_check!("Expected to have a latest pipeline that ran in project ##{project_id} but got none!")
    end

    latest_pipeline = api_client.pipeline(project_id, latest_pipeline_id)

    check_pipeline_for_merged_results!(latest_pipeline)
    check_pipeline_success!(latest_pipeline)
    check_pipeline_identifier!(latest_pipeline)
    check_pipeline_freshness!(latest_pipeline)

    PreMergeChecksStatus.new(0, "All good for merge! 🚀")
  rescue PreMergeChecksFailedError => ex
    PreMergeChecksStatus.new(1, ex.message)
  end

  private

  attr_reader :api_endpoint, :project_id, :merge_request_iid, :target_branch

  def api_client
    @api_client ||= begin
      Gitlab.configure do |config|
        config.endpoint = api_endpoint
        config.private_token = ENV.fetch('GITLAB_API_PRIVATE_TOKEN', '')
      end
      Gitlab.client
    end
  end

  def check_required_ids!
    fail_check!("Missing project_id!") unless project_id
    fail_check!("Missing merge_request_iid!") if merge_request_iid == 0
    fail_check!("Missing target_branch!") unless target_branch
  end

  def check_pipeline_for_merged_results!(pipeline)
    return if pipeline.ref == "refs/merge-requests/#{merge_request_iid}/merge"

    fail_check! <<~TEXT
      Expected to have a Merged Results pipeline but got #{pipeline.ref}!

      Please start a new pipeline.
    TEXT
  end

  def check_pipeline_success!(pipeline)
    return if pipeline.status == 'success'

    fail_check! <<~TEXT
      Expected latest pipeline (#{pipeline.web_url}) to be successful! Pipeline status was "#{pipeline.status}".

      Please start a new pipeline.
    TEXT
  end

  def check_pipeline_freshness!(pipeline)
    hours_ago = ((Time.now - Time.parse(pipeline.created_at)) / 3600).ceil(2)
    threshold =
      if target_branch_is_stable_branch?
        PIPELINE_FRESHNESS_STABLE_BRANCHES_THRESHOLD_IN_HOURS
      else
        PIPELINE_FRESHNESS_DEFAULT_THRESHOLD_IN_HOURS
      end

    return if hours_ago < threshold

    fail_check! <<~TEXT
      Expected latest pipeline (#{pipeline.web_url}) to be created within the last #{threshold} hours (it was created #{hours_ago} hours ago)!

      Please start a new pipeline.
    TEXT
  end

  def check_pipeline_identifier!(pipeline)
    if pipeline.name.match?(TIER_IDENTIFIER_REGEX) && !pipeline.name.include?(REQUIRED_TIER_IDENTIFIER) # rubocop:disable Rails/NegateInclude -- Not executed in Rails context
      fail_check! <<~MSG
        Expected latest pipeline (#{pipeline.web_url}) to be a tier-3 pipeline! Pipeline name was "#{pipeline.name}".

        Please ensure the MR has all the required approvals, start a new pipeline and put the MR back on the Merge Train.
      MSG
    end

    return unless pipeline.name.include?(PREDICTIVE_PIPELINE_IDENTIFIER)

    fail_check! <<~MSG
      Expected latest pipeline (#{pipeline.web_url}) not to be a predictive pipeline! Pipeline name was "#{pipeline.name}".

      Please ensure the MR has all the required approvals, start a new pipeline and put the MR back on the Merge Train.
    MSG
  end

  def target_branch_is_stable_branch?
    target_branch.end_with?(STABLE_BRANCH_SUFFIX)
  end

  def fail_check!(text)
    raise PreMergeChecksFailedError, text
  end
end

if $PROGRAM_NAME == __FILE__
  require 'optparse'
  options = {}

  OptionParser.new do |opts|
    opts.on("-p", "--project_id [string]", String, "Project ID") do |value|
      options[:project_id] = value
    end

    opts.on("-m", "--merge_request_iid [string]", String, "Merge request IID") do |value|
      options[:merge_request_iid] = value
    end

    opts.on("-t", "--target-branch [string]", String, "Target branch name") do |value|
      options[:target_branch] = value
    end

    opts.on("-h", "--help") do
      puts "Usage: #{File.basename(__FILE__)} [--project_id <PROJECT_ID>] " \
        "[--merge_request_iid <MERGE_REQUEST_IID>] [--target-branch <TARGET_BRANCH>]"
      puts
      puts "Examples:"
      puts
      puts "#{File.basename(__FILE__)} --project_id \"gitlab-org/gitlab\" " \
        "--merge_request_iid \"1\" --target-branch \"master\""

      exit
    end
  end.parse!

  colors_to_codes = {
    red: 31,
    green: 32
  }.freeze

  pre_merge_checks_status = PreMergeChecks.new(**options).execute

  if pre_merge_checks_status.success?
    puts "\e[#{colors_to_codes[:green]}m#{pre_merge_checks_status.message}\e[0m"
  else
    puts "\e[#{colors_to_codes[:red]}m#{pre_merge_checks_status.message}\e[0m"
  end

  exit(pre_merge_checks_status.exitstatus)
end
