# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1

require "log4r"
require 'vagrant/util/experimental'

module Vagrant
  module Action
    # The action warden is a middleware which injects itself between
    # every other middleware, watching for exceptions which are raised
    # and performing proper cleanup on every action by calling the `recover`
    # method. The warden therefore allows middlewares to not worry about
    # exceptional events, and by providing a simple callback, can clean up
    # in any erroneous case.
    #
    # Warden will "just work" behind the scenes, and is not of particular
    # interest except to those who are curious about the internal workings
    # of Vagrant.
    class Warden
      attr_accessor :actions, :stack

      def initialize(actions, env)
        @stack      = []
        @actions    = actions.map { |m| finalize_action(m, env) }.flatten
        @logger     = Log4r::Logger.new("vagrant::action::warden")
        @last_error = nil
      end

      def call(env)
        return if @actions.empty?

        begin
          # Call the next middleware in the sequence, appending to the stack
          # of "recoverable" middlewares in case something goes wrong!
          raise Errors::VagrantInterrupt if env[:interrupted]
          action = @actions.shift
          @logger.info("Calling IN action: #{action}")
          @stack.unshift(action).first.call(env)
          raise Errors::VagrantInterrupt if env[:interrupted]
          @logger.info("Calling OUT action: #{action}")
        rescue SystemExit, NoMemoryError
          # This means that an "exit" or "abort" was called, or we have run out
          # of memory. In these cases, we just exit immediately.
          raise
        rescue Exception => e
          # We guard this so that the Warden only outputs this once for
          # an exception that bubbles up.
          if e != @last_error
            @logger.error("Error occurred: #{e}")
            @last_error = e
          end

          env["vagrant.error"] = e

          # Something went horribly wrong. Start the rescue chain then
          # reraise the exception to properly kick us out of limbo here.
          recover(env)
          raise
        end
      end

      # We implement the recover method ourselves in case a Warden is
      # embedded within another Warden. To recover, we just do our own
      # recovery process on our stack.
      def recover(env)
        @logger.info("Beginning recovery process...")

        @stack.each do |act|
          if act.respond_to?(:recover)
            @logger.info("Calling recover: #{act}")
            act.recover(env)
          end
        end

        @logger.info("Recovery complete.")

        # Clear stack so that warden down the middleware chain doesn't
        # rescue again.
        @stack.clear
      end

      # A somewhat confusing function which simply initializes each
      # middleware properly to call the next middleware in the sequence.
      def finalize_action(action, env)
        if action.is_a?(Builder::StackItem)
          klass = action.middleware
          args = action.arguments.parameters
          keywords = action.arguments.keywords
          block = action.arguments.block
        else
          klass = action
          args = []
          keywords = {}
        end

        args = nil if args.empty?
        keywords = nil if keywords.empty?

        if klass.is_a?(Class)
          # NOTE: We need to detect if we are passing args and/or
          #       keywords and do it explicitly. Earlier versions
          #       are not as lax about splatting keywords when the
          #       target method is not expecting them.
          if args && keywords
            klass.new(self, env, *args, **keywords, &block)
          elsif args
            klass.new(self, env, *args, &block)
          elsif keywords
            klass.new(self, env, **keywords, &block)
          else
            klass.new(self, env, &block)
          end
        elsif klass.respond_to?(:call)
          # Make it a lambda which calls the item then forwards
          # up the chain
          lambda do |e|
            klass.call(e)
            self.call(e)
          end
        else
          raise "Invalid action: #{action.inspect}"
        end
      end
    end
  end
end
