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

# This script is designed to manage database migrations for GitLab.
# It allows users to selectively run or revert migrations that have been changed in the current Git branch.
# This is especially useful for database reviewers and maintainers
# The script uses the 'fzf' command-line fuzzy finder for interactive selection of migration files.
#
# Examples:
#
# 1. Running migrations:
#    $ ./scripts/database/migrate.rb
#    This will show a list of changed migration files and allow you to select which ones to apply.
#
# 2. Reverting migrations:
#    $ ./scripts/database/migrate.rb -t down
#    This will show a list of changed migration files and allow you to select which ones to revert.
#
# 3. Dry run mode:
#    $ ./scripts/database/migrate.rb -n
#    This will show what commands would be executed without actually running them.
#
# 4. Debug mode:
#    $ ruby scripts/database/migrate.rb --debug
#    This will run the script with additional debug output for troubleshooting.
# 5. Custom base branch:
#    $ BASE_REF=origin/master ruby scripts/database/migrate.rb
#    This will run the script with origin/master as the base branch for migrations retrieval
#
# The script checks for changed migration files in both 'db/migrate' and 'db/post_migrate' directories,
# and executes the selected migrations for both the main and secondary databases.

require 'optparse'

class Migrate
  SCRIPT_NAME = File.basename($PROGRAM_NAME)
  MIGRATIONS_DIR = 'db/migrate'
  POST_DEPLOY_MIGRATIONS_DIR = 'db/post_migrate'
  BRANCH_NAME = ENV.fetch('BASE_REF', 'master')

  def require_commands!(*commands)
    missing_commands = commands.reject { |command| system("command", "-v", command, out: File::NULL) }

    abort("This script requires #{missing_commands.join(', ')} to be installed.") unless missing_commands.empty?
  end

  def parse_options
    options = {
      task: :up,
      dry_run: false
    }
    OptionParser.new do |opts|
      opts.banner = "Usage: #{SCRIPT_NAME} [options]"

      opts.on('--debug', 'Enable debug mode') do |v|
        options[:debug] = v
      end
      opts.on('-n', '--dry-run', 'Show commands without executing them') do |v|
        options[:dry_run] = v
      end
      opts.on('-t', '--task [up|down]', [:up, :down], 'Set task - "up" to migrate forward, "down" to rollback') do |v|
        options[:task] = v
      end
    end.parse!

    options
  end

  def prompt(list, prompt:, multi: false, reverse: false)
    arr = list.join("\n")

    fzf_args = [].tap do |args|
      args << '--layout="reverse"'
      args << '--height=30%'
      args << '--multi' if multi
      args << '--tac' if reverse
    end

    output = IO.popen("echo \"#{arr}\" | fzf #{fzf_args.join(' ')} --prompt=\"#{prompt}\"", &:readlines)
    return [] unless output

    selection = output.join.strip

    return selection unless multi

    selection.split("\n")
  end

  def get_changed_files(branch_name:)
    set = Set.new

    [MIGRATIONS_DIR, POST_DEPLOY_MIGRATIONS_DIR].each do |dir|
      set += `git diff --name-only --diff-filter=d $(git merge-base #{branch_name} HEAD)..HEAD #{dir}`
        .split("\n").to_set
      set += `git diff --diff-filter=d --merge-base --name-only #{branch_name} #{dir}`.split("\n")
    end

    set
  end

  def database_migration_tasks(direction, rails_help_output = nil)
    rails_help_output ||= `bin/rails --help`
    rails_help_output
      .lines
      .grep(/db:migrate:#{direction}:/)
      .map(&:strip)
      .map { |line| line.split(' ').first }
  end

  def migrations_for_files(files, direction)
    files.map do |file|
      version = file.match(/^\d+/)[0].to_i
      tasks = database_migration_tasks(direction).join(' ')
      "bin/rails #{tasks} VERSION=#{version}"
    end
  end

  # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity -- we can skip it for this script
  def execute
    options = parse_options
    puts "Options: #{options.inspect}" if options[:debug]

    files = get_changed_files(branch_name: BRANCH_NAME)
    puts "Files: #{files.inspect}" if options[:debug]

    base_files = files.map { |path| File.basename(path) }
    puts "Base files: #{base_files.inspect}" if options[:debug]

    if base_files.empty?
      puts 'No migration files found'
      exit 1
    end

    selected_files = prompt(base_files, prompt: 'Select migrations (press tab to select multiple)> ', multi: true)
    puts "Selected files: #{selected_files.inspect}" if options[:debug]

    if selected_files.empty?
      puts 'No files selected'
      exit 1
    end

    sorted = selected_files.sort_by { |f| f.match(/^\d+/)[0].to_i }
    puts "Sorted: #{sorted.inspect}" if options[:debug]

    migrations = case options[:task]
                 when :up
                   migrations_for_files(sorted, "up")
                 when :down
                   migrations_for_files(sorted.reverse, "down")
                 else
                   puts 'Invalid task. Use --task=[up|down]'
                   exit 1
                 end

    migrations.each do |cmd|
      puts "$ #{cmd}"

      if options[:dry_run]
        puts "[dry-run] Skipping execution"
      else
        raise "Migration failed: #{cmd}" unless system(cmd)
      end
    end
  end
  # rubocop:enable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
end

if File.identical?(__FILE__, $PROGRAM_NAME)
  migrator = Migrate.new
  migrator.require_commands!('fzf')
  migrator.execute
end
