# frozen_string_literal: true

require 'active_support'
require 'active_support/core_ext'
require 'active_support/core_ext/string'
require 'gitlab/housekeeper/logger'
require 'gitlab/housekeeper/keep'
require 'gitlab/housekeeper/gitlab_client'
require 'gitlab/housekeeper/git'
require 'gitlab/housekeeper/change'
require 'gitlab/housekeeper/substitutor'
require 'gitlab/housekeeper/filter_identifiers'
require 'amazing_print'
require 'digest'

module Gitlab
  module Housekeeper
    class Runner
      def initialize(
        max_mrs: 1,
        dry_run: false,
        keeps: nil,
        filter_identifiers: [],
        push_when_approved: false,
        target_branch: 'master')
        @max_mrs = max_mrs
        @dry_run = dry_run
        @logger = Logger.new($stdout)
        @target_branch = target_branch
        @push_when_approved = push_when_approved
        require_keeps

        @keeps = if keeps
                   keeps.map { |k| k.is_a?(String) ? k.constantize : k }
                 else
                   all_keeps
                 end

        @filter_identifiers = ::Gitlab::Housekeeper::FilterIdentifiers.new(filter_identifiers)
      end

      def run
        mrs_created_count = 0

        git.with_clean_state do
          @keeps.each do |keep_class|
            @logger.puts "Running keep #{keep_class}"
            keep = keep_class.new(logger: @logger, filter_identifiers: @filter_identifiers)
            keep.each_identified_change do |change|
              change.keep_class ||= keep_class
              branch_name = git.create_branch(change)
              next unless allowed_change?(change, branch_name)

              keep.make_change!(change)

              add_standard_change_data(change)

              unless change.valid?
                @logger.warn "Ignoring invalid change from #{keep_class} with identifier #{change.identifiers}"
                next
              end

              next if skip_change_if_aborted(change, branch_name)

              setup_merge_request(change, branch_name) unless @dry_run

              git.in_branch(branch_name) do
                Gitlab::Housekeeper::Substitutor.perform(change)
                git.create_commit(change)
              end

              print_change_details(change, branch_name)
              create(change, branch_name) unless @dry_run

              mrs_created_count += 1
              break if mrs_created_count >= @max_mrs
            end
            break if mrs_created_count >= @max_mrs
          end
        end

        print_completion_message(mrs_created_count)
      end

      def allowed_change?(change, branch_name)
        unless @filter_identifiers.matches_filters?(change.identifiers)
          @logger.puts "Skipping change: #{change.identifiers} due to not matching filter."
          return false
        end

        if !@dry_run && has_closed_merge_request?(branch_name)
          @logger.puts "Skipping change: #{change.identifiers} as we have closed an MR for this branch #{branch_name}"
          return false
        end

        true
      end

      def print_completion_message(mrs_created_count)
        mr_count_string = "#{mrs_created_count} #{'MR'.pluralize(mrs_created_count)}"

        completion_message = if @dry_run
                               "Dry run complete. Housekeeper would have created #{mr_count_string} on an actual run."
                             else
                               "Housekeeper created #{mr_count_string}."
                             end

        @logger.puts AmazingPrint::Colors.yellowish(completion_message)
        @logger.puts
      end

      def add_standard_change_data(change)
        change.labels ||= []
        change.labels << 'automation:gitlab-housekeeper-authored'
      end

      def skip_change_if_aborted(change, branch_name)
        return false unless change.aborted?

        git.in_branch(branch_name) do
          git.create_commit(change)
        end

        @logger.puts "Skipping change as it is marked aborted."
        @logger.puts "Modified files have been committed to branch #{AmazingPrint::Colors.yellowish(branch_name)}, " \
                     "but will not be pushed."
        @logger.puts
        true
      end

      def setup_merge_request(change, branch_name)
        merge_request = get_existing_merge_request(branch_name) || create(change, branch_name)
        change.mr_web_url = merge_request['web_url']
      end

      def git
        @git ||= ::Gitlab::Housekeeper::Git.new(logger: @logger, branch_from: @target_branch)
      end

      def require_keeps
        Dir.glob("keeps/*.rb").each do |f|
          require(Pathname(f).expand_path.to_s)
        end
      end

      def print_change_details(change, branch_name)
        base_message = "Merge request URL: #{change.mr_web_url || '(known after create)'}, on branch #{branch_name}. " \
                       "Squash commits enabled."
        base_message << " CI skipped." if change.push_options.ci_skip

        @logger.puts AmazingPrint::Colors.yellowish(base_message)
        @logger.puts AmazingPrint::Colors.purple("=> #{change.identifiers.join(': ')}")

        @logger.puts AmazingPrint::Colors.purple('=> Title:')
        @logger.puts AmazingPrint::Colors.purple(change.title)
        @logger.puts

        @logger.puts '=> Description:'
        @logger.puts change.mr_description
        @logger.puts

        if change.labels.present? || change.assignees.present? || change.reviewers.present?
          @logger.puts '=> Attributes:'
          @logger.puts "Labels: #{change.labels.join(', ')}"
          @logger.puts "Assignees: #{change.assignees.join(', ')}"
          @logger.puts "Reviewers: #{change.reviewers.join(', ')}"
          @logger.puts
        end

        @logger.puts '=> Diff:'
        @logger.puts Shell.execute('git', '--no-pager', 'diff', '--color=always', @target_branch, branch_name, '--',
          *change.changed_files)
        @logger.puts
      end

      def create(change, branch_name)
        change.non_housekeeper_changes = gitlab_client.non_housekeeper_changes(
          source_project_id: housekeeper_fork_project_id,
          source_branch: branch_name,
          target_branch: @target_branch,
          target_project_id: housekeeper_target_project_id
        )

        git.push(branch_name, change.push_options) if self.class.should_push_code?(change, @push_when_approved)

        gitlab_client.create_or_update_merge_request(
          change: change,
          source_project_id: housekeeper_fork_project_id,
          source_branch: branch_name,
          target_branch: @target_branch,
          target_project_id: housekeeper_target_project_id
        )
      end

      def get_existing_merge_request(branch_name)
        gitlab_client.get_existing_merge_request(
          source_project_id: housekeeper_fork_project_id,
          source_branch: branch_name,
          target_branch: @target_branch,
          target_project_id: housekeeper_target_project_id
        )
      end

      def has_closed_merge_request?(branch_name)
        gitlab_client.closed_merge_request_exists?(
          source_project_id: housekeeper_fork_project_id,
          source_branch: branch_name,
          target_branch: @target_branch,
          target_project_id: housekeeper_target_project_id
        )
      end

      # We do not want to push code if the MR already has approvals as it will reset the approvals. Also we do not push
      # if someone else has added commits already.
      def self.should_push_code?(change, push_when_approved)
        return false if change.already_approved? && !push_when_approved

        change.update_required?(:code)
      end

      def housekeeper_fork_project_id
        ENV.fetch('HOUSEKEEPER_FORK_PROJECT_ID', housekeeper_target_project_id)
      end

      def housekeeper_target_project_id
        ENV.fetch('HOUSEKEEPER_TARGET_PROJECT_ID')
      end

      def gitlab_client
        @gitlab_client ||= GitlabClient.new
      end

      def all_keeps
        @all_keeps ||= ObjectSpace.each_object(Class).select { |klass| klass < Keep }
      end
    end
  end
end
