module Runbook
  module Run
    def self.included(base)
      base.extend(ClassMethods)
      _register_kill_all_panes_hook(base)
      _register_additional_step_whitespace_hook(base)
      ::Runbook::Util::Repo.register_save_repo_hook(base)
      ::Runbook::Util::Repo.register_delete_stored_repo_hook(base)
      ::Runbook::Util::StoredPose.register_save_pose_hook(base)
      ::Runbook::Util::StoredPose.register_delete_stored_pose_hook(base)
    end

    module ClassMethods
      include Runbook::Hooks
      include Runbook::Helpers::FormatHelper
      include Runbook::Helpers::TmuxHelper

      def execute(object, metadata)
        return if should_skip?(metadata)

        method = _method_name(object)
        if respond_to?(method)
          send(method, object, metadata)
        else
          msg = "ERROR! No execution rule for #{object.class} (#{_method_name(object)}) in #{to_s}"
          metadata[:toolbox].error(msg)
          return
        end
      end

      def runbook__entities__book(object, metadata)
        metadata[:toolbox].output("Executing #{object.title}...\n\n")
      end

      def runbook__entities__section(object, metadata)
        metadata[:toolbox].output("Section #{metadata[:position]}: #{object.title}\n\n")
      end

      def runbook__entities__setup(object, metadata)
        metadata[:toolbox].output("Setup:\n\n")
      end

      def runbook__entities__step(object, metadata)
        toolbox = metadata[:toolbox]
        title = " #{object.title}".rstrip
        toolbox.output("Step #{metadata[:position]}:#{title}\n\n")
        return if metadata[:auto] || metadata[:noop] ||
          !metadata[:paranoid] || object.title.nil?
        step_choices = _step_choices(object, metadata)
        continue_result = toolbox.expand("Continue?", step_choices)
        _handle_continue_result(continue_result, object, metadata)
      end

      def runbook__statements__ask(object, metadata)
        target = object.parent.dsl
        existing_val = target.instance_variable_get(:"@#{object.into}")
        default = existing_val || object.default

        if metadata[:auto]
          if default
            target.singleton_class.class_eval { attr_accessor object.into }
            target.send("#{object.into}=".to_sym, default)
            return
          end

          error_msg = "ERROR! Can't execute ask statement without default in automatic mode!"
          metadata[:toolbox].error(error_msg)
          raise Runbook::Runner::ExecutionError, error_msg
        end

        if metadata[:noop]
          default_msg = default ? " (default: #{default})" : ""
          echo_msg = object.echo ? "" : " (echo: false)"
          metadata[:toolbox].output("[NOOP] Ask: #{object.prompt} (store in: #{object.into})#{default_msg}#{echo_msg}")
          return
        end

        result = metadata[:toolbox].ask(object.prompt, default: default, echo: object.echo)

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

      def runbook__statements__confirm(object, metadata)
        if metadata[:auto]
          metadata[:toolbox].output("Skipping confirmation (auto): #{object.prompt}")
        else
          if metadata[:noop]
            metadata[:toolbox].output("[NOOP] Prompt: #{object.prompt}")
            return
          end

          result = metadata[:toolbox].yes?(object.prompt)
          metadata[:toolbox].exit(1) unless result
        end
      end

      def runbook__statements__description(object, metadata)
        metadata[:toolbox].output("Description:")
        metadata[:toolbox].output("#{object.msg}\n")
      end

      def runbook__statements__layout(object, metadata)
        if metadata[:noop]
          metadata[:toolbox].output(
            "[NOOP] Layout: #{object.structure.inspect}"
          )
          unless ENV["TMUX"]
            msg = "Warning: layout statement called outside a tmux pane."
            metadata[:toolbox].warn(msg)
          end
          return
        end

        unless ENV["TMUX"]
          error_msg = "Error: layout statement called outside a tmux pane. Exiting..."
          metadata[:toolbox].error(error_msg)
          metadata[:toolbox].exit(1)
        end

        structure = object.structure
        title = object.parent.title
        layout_panes = setup_layout(structure, runbook_title: title)
        metadata[:layout_panes].merge!(layout_panes)
      end

      def runbook__statements__note(object, metadata)
        metadata[:toolbox].output("Note: #{object.msg}")
      end

      def runbook__statements__notice(object, metadata)
        metadata[:toolbox].warn("Notice: #{object.msg}")
      end

      def runbook__statements__tmux_command(object, metadata)
        if metadata[:noop]
          metadata[:toolbox].output("[NOOP] Run: `#{object.cmd}` in pane #{object.pane}")
          return
        end

        send_keys(object.cmd, metadata[:layout_panes][object.pane])
      end

      def runbook__statements__ruby_command(object, metadata)
        if metadata[:noop]
          metadata[:toolbox].output("[NOOP] Run the following Ruby block:\n")
          begin
            source = deindent(object.block.source)
            metadata[:toolbox].output("```ruby\n#{source}\n```\n")
          rescue ::MethodSource::SourceNotFoundError => e
            metadata[:toolbox].output("Unable to retrieve source code")
          end
          return
        end

        next_index = metadata[:index] + 1
        parent_items = object.parent.items
        remaining_items = parent_items.slice!(next_index..-1)
        object.parent.dsl.instance_exec(object, metadata, self, &object.block)
        parent_items[next_index..-1].each { |item| item.dynamic! }
        parent_items.push(*remaining_items)
      end

      def runbook__statements__wait(object, metadata)
        if metadata[:noop]
          metadata[:toolbox].output("[NOOP] Sleep #{object.time} seconds")
          return
        end

        time = object.time
        message = "Sleeping #{time} seconds [:bar] :current/:total"
        pastel = Pastel.new
        yellow = pastel.on_yellow(" ")
        green = pastel.on_green(" ")
        progress_bar = TTY::ProgressBar.new(
          message,
          total: time,
          width: 60,
          head: ">",
          incomplete: yellow,
          complete: green,
        )
        progress_bar.start
        time.times do
          sleep(1)
          progress_bar.advance(1)
        end
      end

      def should_skip?(metadata)
        if metadata[:reversed] && metadata[:position].empty?
          current_pose = "0"
        else
          current_pose = metadata[:position]
        end
        return false if current_pose.empty?
        position = Gem::Version.new(current_pose)
        start_at = Gem::Version.new(metadata[:start_at])
        return position < start_at
      end

      def start_at_is_substep?(object, metadata)
        return false unless object.is_a?(Entity)
        return true if metadata[:position].empty?
        metadata[:start_at].start_with?(metadata[:position])
      end

      def past_position?(current_position, position)
        current_pose = Gem::Version.new(current_position)
        pose = Gem::Version.new(position)
        return pose <= current_pose
      end

      def _method_name(object)
        object.class.to_s.underscore.gsub("/", "__")
      end

      def _step_choices(object, metadata)
        [
          {key: "c", name: "Continue to execute this step", value: :continue},
          {key: "s", name: "Skip this step", value: :skip},
          {key: "j", name: "Jump to the specified position", value: :jump},
          {key: "P", name: "Disable paranoid mode", value: :no_paranoid},
          {key: "e", name: "Exit the runbook", value: :exit},
        ]
      end

      def _handle_continue_result(result, object, metadata)
        toolbox = metadata[:toolbox]
        case result
        when :continue
          return
        when :skip
          position = metadata[:position]
          split_position = position.split(".")
          new_step = (split_position.pop.to_i + 1).to_s
          start_at = (split_position <<  new_step).join(".")
          metadata[:start_at] = start_at
        when :jump
          result = toolbox.ask("What position would you like to jump to?")
          if past_position?(metadata[:position], result)
            metadata[:reverse] = true
            metadata[:reversed] = true
          end
          metadata[:start_at] = result
        when :no_paranoid
          metadata[:paranoid] = false
        when :exit
          toolbox.exit(0)
        end
      end
    end

    def self._register_kill_all_panes_hook(base)
      base.register_hook(
        :kill_all_panes_after_book,
        :after,
        Runbook::Entities::Book,
      ) do |object, metadata|
        next if metadata[:noop] || metadata[:layout_panes].none? || metadata[:keep_panes]
        if metadata[:auto]
          metadata[:toolbox].output("Killing all opened tmux panes...")
          kill_all_panes(metadata[:layout_panes])
        else
          prompt = "Kill all opened panes?"
          result = metadata[:toolbox].yes?(prompt)
          if result
            kill_all_panes(metadata[:layout_panes])
          end
        end
      end
    end

    def self._register_additional_step_whitespace_hook(base)
      base.register_hook(
        :add_additional_step_whitespace_hook,
        :after,
        Runbook::Statement,
      ) do |object, metadata|
        if object.parent.is_a?(Runbook::Entities::Step) ||
            object.parent.is_a?(Runbook::Entities::Setup)
          if object.parent.items.last == object
            metadata[:toolbox].output("\n")
          end
        end
      end
    end
  end
end
