#!/usr/bin/env ruby

# frozen_string_literal: true

require 'active_support/core_ext/object/to_query'
require 'optparse'
require 'open3'
require 'rainbow/refinement'
require 'tty-prompt'
using Rainbow

class Backport
  SECURITY_REPO_URLS = {
    ssh: 'git@gitlab.com:gitlab-org/security/gitlab.git',
    http: 'https://gitlab.com/gitlab-org/security/gitlab.git'
  }.freeze

  DEFAULT_OPTIONS = {
    base: {
      version: nil, branch: nil, sha: nil, merge_request: false, stable_branch_suffix: 'stable'
    },
    security: {
      branch_prefix: 'security',
      new_merge_request_url: 'https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/new',
      merge_request_template: 'Security Fix'
    },
    bugfix: {
      branch_prefix: 'backport',
      new_merge_request_url: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/new',
      merge_request_template: 'Stable Branch'
    }
  }.freeze

  BACKPORT_TYPE_CHOICES = [
    { name: 'Security Fix', value: :security }.freeze,
    { name: 'Bug Fix', value: :bugfix }.freeze
  ].freeze

  def initialize
    @prompt = TTY::Prompt.new(help_color: :cyan)
    @options = build_options
  end

  attr_reader :prompt

  def create!
    correct_repository_check!

    return dry_run if dry_run?

    pick_commits
    push_commits
  end

  private

  def correct_repository_check!
    if security_backport? && !pushing_to_security_remote?
      abort('⛔️ Can only push security backports to the security repository ⛔️'.red)
    elsif !security_backport? && pushing_to_security_remote?
      abort('⛔️ Bugfixes should be pushed to the canonical repository ⛔️'.red)
    end
  end

  def pushing_to_security_remote?
    @options[:remote] == security_remote_name
  end

  def build_options
    options = DEFAULT_OPTIONS[:base].dup
    parse_options(options)

    options[:sha] ||= git_head_sha
    options[:branch] ||= git_current_branch
    options[:remote] ||= select_git_remote
    options[:version] ||= select_version

    options.merge(DEFAULT_OPTIONS[confirmed_backport_type!])
  end

  def confirmed_backport_type!
    backport_type = prompt.select('⚠️ What type of fix are you backporting? ⚠️'.red, BACKPORT_TYPE_CHOICES)

    @security_backport = backport_type == :security

    backport_type
  end

  def security_backport?
    @security_backport
  end

  def add_security_remote
    puts "⚠️ You do not have the security remote configured ⚠️".red

    return attempt_to_add_security_remote if prompt.yes?('Would you like me to add the remote for you now?')

    print_security_remote_config_instructions_and_exit
  end

  def attempt_to_add_security_remote
    security_repo_url = prompt.select('Which remote format do you normally use?', git_remote_style_choices)
    stdin, stdout, stderr, wait_thr = Open3.popen3("git remote add security #{security_repo_url}")

    if wait_thr.value.success? # rubocop:disable Cop/LineBreakAroundConditionalBlock -- https://gitlab.com/gitlab-org/ruby/gems/gitlab-styles/-/merge_requests/258
      @security_remote_name = 'security'
      puts '✅ Successfully created `security` remote'.white
    else
      puts "⛔️ Could not create `security` remote ⛔️".red
      puts ('-' * 80).red
      puts "\n#{stderr.read}".red
      puts ('-' * 80).red

      print_security_remote_config_instructions_and_exit
    end
  ensure
    stdin.close
    stdout.close
    stderr.close
  end

  def print_security_remote_config_instructions_and_exit
    puts "Please add the gitlab security remote to git with:".white
    puts "git remote add security #{SECURITY_REPO_URLS[:ssh]}".cyan
    puts "or".white
    puts "git remote add security #{SECURITY_REPO_URLS[:http]}".cyan
    puts "and then try again".white
    exit 1
  end

  def parse_options(options)
    parser = OptionParser.new do |opts|
      opts.banner = <<~BANNER
        Usage: #{$PROGRAM_NAME} [options]

        This tool requires confirmation for the backport type and will prompt
        for the remote and version unless specified.

      BANNER

      opts.on('-v', '--version 10.0', 'Version to target (opens prompt if not passed)') do |version|
        options[:version] = version&.tr('.', '-')
      end

      opts.on('-r', '--remote dev', "Git remote name of repository (opens prompt if not passed)") do |remote|
        options[:remote] = remote
      end

      opts.on('-b', '--branch branch-name', 'Original branch name (optional, defaults to current branch)') do |branch|
        options[:branch] = branch
      end

      opts.on('-s', '--sha abcd', 'SHA or SHA range to cherry pick (optional, defaults to HEAD SHA)') do |sha|
        options[:sha] = sha
      end

      opts.on('--mr', '--merge-request', 'Create a Merge Request targeting the stable branch') do
        options[:merge_request] = true
      end

      opts.on('-d', '--dry-run', 'Display the Git commands this script will run without calling them') do
        options[:try] = true
      end

      opts.on('-h', '--help', 'Displays Help') do
        puts opts

        exit
      end
    end

    parser.parse!
  end

  def git_head_sha
    `git rev-parse HEAD`.strip
  end

  def git_current_branch
    `git rev-parse --abbrev-ref HEAD`.strip
  end

  def select_git_remote
    prompt.select("Which remote do you want to push to?", remote_choices)
  end

  def git_remotes
    @git_remotes ||= `git remote -v`.strip.split("\n").each_with_object({}) do |line, output|
      name, url, _type = line.split(/\s+/)
      output[name] = url
    end
  end

  def security_remote_name
    return @security_remote_name if defined?(@security_remote_name)

    @security_remote_name = git_remotes.find { |_k, url| SECURITY_REPO_URLS.value?(url) }&.first
    add_security_remote if @security_remote_name.nil?
    @security_remote_name
  end

  # Sorts git remotes so the security remote is first in the list
  def remote_choices
    [security_remote_name] + (git_remotes.keys - [security_remote_name])
  end

  def git_remote_style_choices
    SECURITY_REPO_URLS.map { |style, url| { name: "#{style}: #{url}", value: url } }
  end

  def select_version
    prompt.select('Which version are you targeting?', version_choices)
  end

  def current_version
    version_filepath = File.join(File.dirname($PROGRAM_NAME), '../VERSION')
    full_version = File.read(version_filepath)
    major, minor, _rest = full_version.split('.')
    [major.to_i, minor.to_i]
  end

  def version_choices
    major, minor = current_version

    Array.new(3) do
      minor -= 1

      if minor < 0
        major -= 1
        minor = 11
      end

      { name: "#{major}.#{minor}", value: "#{major}-#{minor}" }
    end
  end

  def dry_run?
    @options[:try] == true
  end

  def dry_run
    puts "\nGit commands:".blue
    puts git_commands.join("\n")
    puts "\nMerge request URL:".blue
    puts new_merge_request_url
  end

  def pick_commits
    cmd = git_pick_commands.join(' && ')
    stdin, stdout, stderr, wait_thr = Open3.popen3(cmd)

    puts stdout.read.green
    puts stderr.read.red

    unless wait_thr.value.success? # rubocop:disable Cop/LineBreakAroundConditionalBlock -- https://gitlab.com/gitlab-org/ruby/gems/gitlab-styles/-/merge_requests/258
      puts <<~MSG
        It looks like cherry pick failed!
        Open a new terminal and fix the conflicts.
        Once fixed run `git cherry-pick --continue`

        After you are done, return here and continue. (Press n to cancel)

        Ready to continue? (Y/n)
      MSG

      unless ['', 'Y', 'y'].include?(gets.chomp)
        puts "\nRemaining git commands:".blue
        puts 'git cherry-pick --continue'
        puts git_push_commands.join("\n")
        exit 1
      end
    end
  ensure
    stdin.close
    stdout.close
    stderr.close
  end

  def push_commits
    cmd = git_push_commands.join(' && ')
    stdin, stdout, stderr, wait_thr = Open3.popen3(cmd)

    puts stdout.read.green
    puts stderr.read.red

    puts new_merge_request_url.blue if wait_thr.value.success? && !@options[:merge_request]
  ensure
    stdin.close
    stdout.close
    stderr.close
  end

  def git_pick_commands
    [
      fetch_stable_branch,
      create_backport_branch,
      cherry_pick_commit
    ]
  end

  def git_push_commands
    [
      push_to_remote,
      checkout_original_branch
    ]
  end

  def git_commands
    git_pick_commands + git_push_commands
  end

  def fetch_stable_branch
    "git fetch #{@options[:remote]} #{stable_branch}"
  end

  def create_backport_branch
    "git checkout -B #{source_branch} #{@options[:remote]}/#{stable_branch} --no-track"
  end

  def cherry_pick_commit
    "git cherry-pick #{@options[:sha]}"
  end

  def push_to_remote
    [
      "git push #{@options[:remote]} #{source_branch} --no-verify",
      *merge_request_push_options
    ].join(' ')
  end

  def checkout_original_branch
    "git checkout #{@options[:branch]}"
  end

  def gitlab_params
    {
      issuable_template: @options[:merge_request_template],
      merge_request: {
        source_branch: source_branch,
        target_branch: stable_branch
      }
    }
  end

  def merge_request_push_options
    return [] unless @options[:merge_request]

    [
      "-o mr.create",
      "-o mr.target='#{stable_branch}'",
      "-o mr.description='Please apply `#{@options[:merge_request_template]}` template.'",
      "-o mr.milestone='#{@options[:version].tr('-', '.')}'"
    ]
  end

  def source_branch
    branch = "#{@options[:branch]}-#{@options[:version]}"
    branch = "#{@options[:branch_prefix]}-#{branch}" unless branch.start_with?("#{@options[:branch_prefix]}-")
    branch
  end

  def stable_branch
    "#{@options[:version]}-#{@options[:stable_branch_suffix]}-ee"
  end

  def new_merge_request_url
    "#{@options[:new_merge_request_url]}?#{gitlab_params.to_query}"
  end
end

Backport.new.create!
