module Runbook::Runs
  module SSHKit
    include Runbook::Run

    def self.included(base)
      base.extend(ClassMethods)
    end

    module ClassMethods
      include Runbook::Helpers::SSHKitHelper

      def runbook__entities__step(object, metadata)
        airbrussh_context = Runbook.configuration._airbrussh_context
        airbrussh_context.set_current_task_name(object.title)
        super
      end

      def runbook__statements__assert(object, metadata)
        cmd_ssh_config = find_ssh_config(object, :cmd_ssh_config)

        if metadata[:noop]
          ssh_config_output = render_ssh_config_output(cmd_ssh_config)
          metadata[:toolbox].output(ssh_config_output) unless ssh_config_output.empty?
          interval_msg = "(running every #{object.interval} second(s))"
          metadata[:toolbox].output("[NOOP] Assert: `#{object.cmd}` returns 0 #{interval_msg}")
          if object.timeout > 0 || object.attempts > 0
            timeout_msg = object.timeout > 0 ? "#{object.timeout} second(s)" : nil
            attempts_msg = object.attempts > 0 ? "#{object.attempts} attempts" : nil
            abort_msg = "after #{[timeout_msg, attempts_msg].compact.join(" or ")}, abort..."
            metadata[:toolbox].output(abort_msg)
            if object.abort_statement
              object.abort_statement.parent = object.parent
              object.abort_statement.run(self, metadata.dup)
            end
            metadata[:toolbox].output("and exit")
          end
          return
        end

        should_abort = false
        test_args = ssh_kit_command(object.cmd, raw: object.cmd_raw)
        test_options = ssh_kit_command_options(cmd_ssh_config)

        with_ssh_config(cmd_ssh_config) do
          time = Time.now
          count = object.attempts
          while !(test(*test_args, test_options))
            if ((count -= 1) == 0)
              should_abort = true
              break
            end

            if (object.timeout > 0 && Time.now - time > object.timeout)
              should_abort = true
              break
            end

            sleep(object.interval)
          end
        end

        if should_abort
          error_msg = "Error! Assertion `#{object.cmd}` failed"
          metadata[:toolbox].error(error_msg)
          if object.abort_statement
            object.abort_statement.parent = object.parent
            object.abort_statement.run(self, metadata.dup)
          end
          raise Runbook::Runner::ExecutionError, error_msg
        end
      end

      def runbook__statements__capture(object, metadata)
        _handle_capture(object, metadata) do |ssh_config, capture_args, capture_options|
          if (ssh_config[:servers].size > 1)
            warn_msg = "Warning: `capture` does not support multiple servers. Use `capture_all` instead.\n"
            metadata[:toolbox].warn(warn_msg)
          end

          result = ""
          with_ssh_config(ssh_config) do
            result = capture(*capture_args, capture_options)
          end
          result
        end
      end

      def runbook__statements__capture_all(object, metadata)
        _handle_capture(object, metadata) do |ssh_config, capture_args, capture_options|
          result = {}
          mutex = Mutex.new
          with_ssh_config(ssh_config) do
            hostname = self.host.hostname
            capture_result = capture(*capture_args, capture_options)
            mutex.synchronize { result[hostname] = capture_result }
          end
          result
        end
      end

      def _handle_capture(object, metadata, &block)
        ssh_config = find_ssh_config(object)

        if metadata[:noop]
          ssh_config_output = render_ssh_config_output(ssh_config)
          metadata[:toolbox].output(ssh_config_output) unless ssh_config_output.empty?
          metadata[:toolbox].output("[NOOP] Capture: `#{object.cmd}` into #{object.into}")
          return
        end

        metadata[:toolbox].output("\n") # for formatting

        capture_args = ssh_kit_command(object.cmd, raw: object.raw)
        capture_options = ssh_kit_command_options(ssh_config)
        capture_options[:strip] = object.strip
        capture_options[:verbosity] = Logger::INFO

        capture_msg = "Capturing output of `#{object.cmd}`\n\n"
        metadata[:toolbox].output(capture_msg)

        result = block.call(ssh_config, capture_args, capture_options)

        target = object.parent.dsl
        target.singleton_class.class_eval { attr_accessor object.into }
        target.send("#{object.into}=".to_sym, result)
      end

      def runbook__statements__command(object, metadata)
        ssh_config = find_ssh_config(object)

        if metadata[:noop]
          ssh_config_output = render_ssh_config_output(ssh_config)
          metadata[:toolbox].output(ssh_config_output) unless ssh_config_output.empty?
          metadata[:toolbox].output("[NOOP] Run: `#{object.cmd}`")
          return
        end

        metadata[:toolbox].output("\n") # for formatting

        execute_args = ssh_kit_command(object.cmd, raw: object.raw)
        exec_options = ssh_kit_command_options(ssh_config)

        with_ssh_config(ssh_config) do
          execute(*execute_args, exec_options)
        end
      end

      def runbook__statements__download(object, metadata)
        ssh_config = find_ssh_config(object)

        if metadata[:noop]
          ssh_config_output = render_ssh_config_output(ssh_config)
          metadata[:toolbox].output(ssh_config_output) unless ssh_config_output.empty?
          options = object.options
          to = " to #{object.to}" if object.to
          opts = " with options #{options}" unless options == {}
          noop_msg = "[NOOP] Download: #{object.from}#{to}#{opts}"
          metadata[:toolbox].output(noop_msg)
          return
        end

        metadata[:toolbox].output("\n") # for formatting

        with_ssh_config(ssh_config) do
          download!(object.from, object.to, object.options)
        end
      end

      def runbook__statements__upload(object, metadata)
        ssh_config = find_ssh_config(object)

        if metadata[:noop]
          ssh_config_output = render_ssh_config_output(ssh_config)
          metadata[:toolbox].output(ssh_config_output) unless ssh_config_output.empty?
          options = object.options
          to = " to #{object.to}" if object.to
          opts = " with options #{options}" unless options == {}
          noop_msg = "[NOOP] Upload: #{object.from}#{to}#{opts}"
          metadata[:toolbox].output(noop_msg)
          return
        end

        metadata[:toolbox].output("\n") # for formatting

        with_ssh_config(ssh_config) do
          upload!(object.from, object.to, object.options)
        end
      end
    end

    extend ClassMethods
  end
end
