# frozen_string_literal: true

module Environments
  class ScheduleToDeleteReviewAppsService < ::BaseService
    include ::Gitlab::ExclusiveLeaseHelpers

    EXCLUSIVE_LOCK_KEY_BASE = 'environments:delete_review_apps:lock'
    LOCK_TIMEOUT = 2.minutes

    def execute
      if validation_error = validate
        return validation_error
      end

      mark_deletable_environments
    end

    private

    def key
      "#{EXCLUSIVE_LOCK_KEY_BASE}:#{project.id}"
    end

    def dry_run?
      return true if params[:dry_run].nil?

      params[:dry_run]
    end

    def validate
      return if can?(current_user, :destroy_environment, project)

      Result.new(error_message: "You do not have permission to destroy environments in this project", status: :unauthorized)
    end

    def mark_deletable_environments
      in_lock(key, ttl: LOCK_TIMEOUT, retries: 1) do
        unsafe_mark_deletable_environments
      end

    rescue FailedToObtainLockError
      Result.new(error_message: "Another process is already processing a delete request. Please retry later.", status: :conflict)
    end

    def unsafe_mark_deletable_environments
      result = Result.new
      environments = project.environments
                            .not_scheduled_for_deletion
                            .stopped_review_apps(params[:before], params[:limit])

      # Check if the actor has write permission to a potentially-protected environment.
      deletable, failed = *environments.partition { |env| current_user.can?(:destroy_environment, env) }

      if deletable.any? && failed.empty?
        mark_for_deletion(deletable) unless dry_run?
        result.set_status(:ok)
        result.set_scheduled_entries(deletable)
      else
        result.set_status(
          :bad_request,
          error_message: "No environments found for scheduled deletion. Either your query did not match any environments (default parameters match environments that are 30 days or older), or you have insufficient permissions to delete matching environments."
        )

        result.set_unprocessable_entries(failed)
      end

      result
    end

    def mark_for_deletion(deletable_environments)
      Environment.id_in(deletable_environments).schedule_to_delete
    end

    class Result
      attr_accessor :scheduled_entries, :unprocessable_entries, :error_message, :status

      def initialize(scheduled_entries: [], unprocessable_entries: [], error_message: nil, status: nil)
        self.scheduled_entries = scheduled_entries
        self.unprocessable_entries = unprocessable_entries
        self.error_message = error_message
        self.status = status
      end

      def success?
        status == :ok
      end

      def set_status(status, error_message: nil)
        self.status = status
        self.error_message = error_message
      end

      def set_scheduled_entries(entries)
        self.scheduled_entries = entries
      end

      def set_unprocessable_entries(entries)
        self.unprocessable_entries = entries
      end
    end
  end
end
