# -*- coding: binary -*-
module Rex
module Post
module Sql
module Ui
module Console

###
#
# Mixin that is meant to extend a sql client class in a
# manner that adds interactive capabilities.
#
###
module InteractiveSqlClient

  include Rex::Ui::Interactive

  #
  # Interacts with self.
  #
  def _interact
    while self.interacting
      sql_input = _multiline_with_fallback
      self.interacting = (sql_input[:status] != :exit)

      if sql_input[:status] == :help
        client_dispatcher.query_interactive_help
      end

      # We need to check that the user is still interacting, i.e. if ctrl+z is triggered when requesting user input
      break unless (self.interacting && sql_input[:result])

      self.on_command_proc.call(sql_input[:result].strip) if self.on_command_proc

      formatted_query = client_dispatcher.process_query(query: sql_input[:result])
      print_status "Executing query: #{formatted_query}"
      client_dispatcher.cmd_query(formatted_query)
    end
  end

  #
  # Called when an interrupt is sent.
  #
  def _interrupt
    prompt_yesno('Terminate interactive SQL prompt?')
  end

  #
  # Suspends interaction with the interactive REPL interpreter
  #
  def _suspend
    if (prompt_yesno('Background interactive SQL prompt?') == true)
      self.interacting = false
    end
  end

  #
  # We don't need to do any clean-up when finishing the interaction with the REPL
  #
  def _interact_complete
    # noop
  end

  def _winch
    # noop
  end

  # Try getting multi-line input support provided by Reline, fall back to Readline.
  def _multiline_with_fallback
    name = session.type
    query = {}
    history_file = Msf::Config.history_file_for_session_type(session_type: name, interactive: true)
    return { status: :fail, errors: ["Unable to get history file for session type: #{name}"] } if history_file.nil?

    # Multiline (Reline) and fallback (Readline) have separate history contexts as they are two different libraries.
    framework.history_manager.with_context(history_file: history_file , name: name, input_library: :reline) do
      query = _multiline
    end

    if query[:status] == :fail
      framework.history_manager.with_context(history_file: history_file, name: name, input_library: :readline) do
        query = _fallback
      end
    end

    query
  end

  def _multiline
    begin
      require 'reline' unless defined?(::Reline)
    rescue ::LoadError => e
      elog('Failed to load Reline', e)
      return { status: :fail, errors: [e] }
    end

    stop_words = %w[stop s exit e end quit q].freeze
    help_words = %w[help h].freeze

    finished = false
    help = false
    begin
      result = nil
      prompt_proc_before = ::Reline.prompt_proc
      ::Reline.prompt_proc = proc { |line_buffer| line_buffer.each_with_index.map { |_line, i| i > 0 ? 'SQL *> ' : 'SQL >> ' } }

      # We want to do this in a loop
      # multiline_input is the whole string that the user has input, not just the current line.
      raw_query = ::Reline.readmultiline('SQL >> ', use_history = true) do |multiline_input|
        # The user pressed ctrl + c or ctrl + z and wants to background our SQL prompt
        unless self.interacting
          result = { status: :exit, result: nil }
          next true
        end

        # When the user has pressed the enter key with no input, don't run any queries;
        # simply give them a new prompt on a new line.
        if multiline_input.chomp.empty?
          result = { status: :success, result: nil }
          next true
        end

        if multiline_input.split.count == 1
          # In the case only a stop word was input, exit out of the REPL shell
          finished = stop_words.include?(multiline_input.split.last)
          # In the case when only a help word was input call the help command
          help = help_words.include?(multiline_input.split.last)
        end

        finished || help || multiline_input.split.last&.end_with?(';')
      end
    rescue ::StandardError => e
      elog('Failed to get multi-line SQL query from user', e)
    ensure
      ::Reline.prompt_proc = prompt_proc_before
    end

    if result
      return result
    end

    if help
      return { status: :help, result: nil }
    end

    if finished
      self.interacting = false
      print_status 'Exiting Interactive mode.'
      return { status: :exit, result: nil }
    end

    { status: :success, result: raw_query }
  end

  def _fallback
    stop_words = %w[stop s exit e end quit q].freeze
    line_buffer = []
    while (line = ::Readline.readline(prompt = line_buffer.empty? ? 'SQL >> ' : 'SQL *> ', add_history = true))
      return { status: :exit, result: nil } unless self.interacting

      if stop_words.include? line.chomp.downcase
        self.interacting = false
        print_status 'Exiting Interactive mode.'
        return { status: :exit, result: nil }
      end

      next if line.empty?

      line_buffer.append line

      break if line.end_with? ';'
    end

    { status: :success, result: line_buffer.join(' ') }
  end

  attr_accessor :on_log_proc, :client_dispatcher

  private

  def framework
    client_dispatcher.shell.framework
  end

  def session
    client_dispatcher.shell.session
  end

end
end
end
end
end
end
