/*
 * Copyright (c) 2014, 2022, Oracle and/or its affiliates.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2.0,
 * as published by the Free Software Foundation.
 *
 * This program is also distributed with certain software (including
 * but not limited to OpenSSL) that is licensed under separate terms, as
 * designated in a particular file or component or in included license
 * documentation.  The authors of MySQL hereby grant you an additional
 * permission to link the program and your derivative works with the
 * separately licensed software that they have included with MySQL.
 * This program is distributed in the hope that it will be useful,  but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
 * the GNU General Public License, version 2.0, for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */
#include "shellcore/base_shell.h"

#include <tuple>

#include "mysqlshdk/libs/db/mysql/session.h"
#include "mysqlshdk/libs/utils/fault_injection.h"
#include "mysqlshdk/libs/utils/logger.h"
#include "mysqlshdk/libs/utils/threads.h"
#include "mysqlshdk/shellcore/shell_console.h"
#include "shellcore/base_session.h"
#include "shellcore/interrupt_handler.h"
#include "shellcore/ishell_core.h"
#include "shellcore/shell_notifications.h"
#include "shellcore/shell_resultset_dumper.h"
#include "utils/utils_file.h"
#include "utils/utils_general.h"
#include "utils/utils_path.h"
#include "utils/utils_string.h"
#ifdef HAVE_V8
#include "mysqlshdk/shellcore/provider_javascript.h"
#include "shellcore/shell_jscript.h"
#endif
#ifdef HAVE_PYTHON
#include "mysqlshdk/shellcore/provider_python.h"
#include "shellcore/shell_python.h"
#endif
#include "shellcore/shell_sql.h"

namespace mysqlsh {

namespace {
const std::map<std::string, std::string> k_shell_hooks = {
    {"ClassicResult", "dump"},       {"Result", "dump"},
    {"DocResult", "dump"},           {"RowResult", "dump"},
    {"SqlResult", "dump"},           {"CollectionAdd", "execute"},
    {"CollectionFind", "execute"},   {"CollectionModify", "execute"},
    {"CollectionRemove", "execute"}, {"TableDelete", "execute"},
    {"TableInsert", "execute"},      {"TableSelect", "execute"},
    {"TableUpdate", "execute"},      {"SqlExecute", "execute"},
};
}

namespace {
void print_diag(const std::string &s) { current_console()->print_diag(s); }

void println(const std::string &s) { current_console()->println(s); }

void print(const std::string &s) { current_console()->print(s); }

void print_value(const shcore::Value &value, const std::string &s) {
  current_console()->print_value(value, s);
}

}  // namespace

Base_shell::Base_shell(const std::shared_ptr<Shell_options> &cmdline_options)
    : m_shell_options{cmdline_options},
      _deferred_output(std::make_unique<std::string>()) {
  shcore::current_interrupt()->setup();

  _input_mode = shcore::Input_state::Ok;

  // Whether the Shell_core is interactive or not will depend from the
  // interactive option which is set depending on how the shell is started
  _shell = std::make_shared<shcore::Shell_core>(
      m_shell_options.get()->get().interactive);
  _completer_object_registry =
      std::make_shared<shcore::completer::Object_registry>();
}

Base_shell::~Base_shell() {}

std::string Base_shell::get_shell_hook(const std::string &class_name) {
  auto item = k_shell_hooks.find(class_name);

  return item == k_shell_hooks.end() ? "" : item->second;
}

void Base_shell::finish_init() {
  current_console()->set_verbose(options().verbose_level);

  shcore::IShell_core::Mode initial_mode = options().initial_mode;
  if (initial_mode == shcore::IShell_core::Mode::None) {
#ifdef HAVE_V8
    initial_mode = shcore::IShell_core::Mode::JavaScript;
#else
#ifdef HAVE_PYTHON
    initial_mode = shcore::IShell_core::Mode::Python;
#else
    initial_mode = shcore::IShell_core::Mode::SQL;
#endif
#endif
  }

  // If we're not in the main thread, it means the shell object is created
  // again, because of this we don't need need any mode initialized by default
  // as it's very unlikely that we will be parsing any input directly. If
  // however this will be needed, switch_shell_mode can be used to fix this.
  if (!mysqlshdk::utils::in_main_thread()) {
    initial_mode = shcore::IShell_core::Mode::None;
  }

  // Final initialization that must happen outside the constructor
  switch_shell_mode(initial_mode, {}, true);

  // Pre-init the SQL completer, since it's used in places other than SQL mode
  _provider_sql.reset(new shcore::completer::Provider_sql());
  _completer.add_provider(
      shcore::IShell_core::Mode_mask(shcore::IShell_core::Mode::SQL),
      _provider_sql);
}

// load scripts for standard locations in order to be able to implement standard
// routines
void Base_shell::init_scripts(shcore::Shell_core::Mode mode) {
  std::string extension;

  switch (mode) {
    case shcore::Shell_core::Mode::JavaScript:
      extension.append(".js");
      break;

    case shcore::Shell_core::Mode::Python:
      extension.append(".py");
      break;

    case shcore::Shell_core::Mode::None:
    case shcore::Shell_core::Mode::SQL:
      return;
  }

  try {
    std::vector<std::string> script_paths;
    const auto startup_script = "mysqlshrc" + extension;

    {
      // Checks existence of global startup script
      auto path = shcore::path::join_path(shcore::get_global_config_path(),
                                          startup_script);
      if (shcore::is_file(path)) {
#ifdef WIN32
        // Global configuration files are no longer supported in windows as the
        // %programdata% permissions are open-wide, which opens the door for
        // unauthorized users defining malicious startup files
        current_console()->print_warning(
            shcore::str_format("Global startup scripts are no longer "
                               "supported, '%s' will be ignored.",
                               path.c_str()));
#else
        script_paths.emplace_back(std::move(path));
#endif
      }
    }

    {
      // Checks existence of startup script at MYSQLSH_HOME
      // Or the binary location if not a standard installation
      const auto home = shcore::get_mysqlx_home_path();
      std::string path;

      if (!home.empty()) {
        path =
            shcore::path::join_path(home, "share", "mysqlsh", startup_script);
      } else {
        path = shcore::path::join_path(shcore::get_binary_folder(),
                                       startup_script);
      }

      if (shcore::is_file(path)) script_paths.emplace_back(std::move(path));
    }

    {
      // Checks existence of user startup script
      auto path = shcore::path::join_path(shcore::get_user_config_path(),
                                          startup_script);
      if (shcore::is_file(path)) script_paths.emplace_back(std::move(path));
    }

    for (const auto &script : script_paths) {
      std::ifstream stream(script);
      if (stream && stream.peek() != std::ifstream::traits_type::eof()) {
        process_file(script, {script});
      }
    }
  } catch (const std::exception &e) {
    std::string error(e.what());
    error += "\n";
    print_diag(error);
  }
}

void Base_shell::load_default_modules(shcore::Shell_core::Mode mode) {
  std::string tmp;
  if (mode == shcore::Shell_core::Mode::JavaScript) {
    tmp = "var mysqlx = require('mysqlx');";
    _shell->handle_input(tmp, _input_mode);
    tmp = "var mysql = require('mysql');";
    _shell->handle_input(tmp, _input_mode);
  } else if (mode == shcore::Shell_core::Mode::Python) {
    tmp = "from mysqlsh import mysqlx";
    _shell->handle_input(tmp, _input_mode);
    tmp = "from mysqlsh import mysql";
    _shell->handle_input(tmp, _input_mode);
  }
}

/**
 * Simple, default prompt handler.
 *
 * @return prompt string
 */
std::string Base_shell::prompt() {
  std::string ret_val;

  switch (_shell->interactive_mode()) {
    case shcore::IShell_core::Mode::None:
      break;
    case shcore::IShell_core::Mode::SQL:
      ret_val = "mysql-sql> ";
      break;
    case shcore::IShell_core::Mode::JavaScript:
      ret_val = "mysql-js> ";
      break;
    case shcore::IShell_core::Mode::Python:
      ret_val = "mysql-py> ";
      break;
  }
  // The continuation prompt should be used if state != Ok
  if (_input_mode != shcore::Input_state::Ok) {
    if (ret_val.length() > 4)
      ret_val = std::string(ret_val.length() - 4, ' ');
    else
      ret_val.clear();

    ret_val.append("... ");
  }

  return ret_val;
}

std::map<std::string, std::string> *Base_shell::prompt_variables() {
  update_prompt_variables();

  return &_prompt_variables;
}

void Base_shell::request_prompt_variables_update(bool clear_cache) {
  const auto type = clear_cache ? Prompt_variables_update_type::CLEAR_CACHE
                                : Prompt_variables_update_type::UPDATE;

  if (type > m_pending_update) {
    m_pending_update = type;
  }
}

void Base_shell::update_prompt_variables() {
  FI_SUPPRESS(mysql);
  FI_SUPPRESS(mysqlx);

  if (Prompt_variables_update_type::CLEAR_CACHE == m_pending_update) {
    _prompt_variables.clear();
  }

  const auto session = _shell->get_dev_session();
  if (_prompt_variables.empty()) {
    _prompt_variables["system_user"] = shcore::get_system_user();

    if (session && session->is_open()) {
      const mysqlshdk::db::Connection_options &options(
          session->get_connection_options());
      std::string socket;
      std::string port;

      _prompt_variables["ssl"] = session->get_ssl_cipher().empty() ? "" : "SSL";
      _prompt_variables["uri"] =
          session->uri(mysqlshdk::db::uri::formats::user_transport());
      _prompt_variables["user"] = options.has_user() ? options.get_user() : "";
      _prompt_variables["host"] =
          options.has_host() ? options.get_host() : "localhost";

      const auto &ssh = options.get_ssh_options();
      if (ssh.has_data()) {
        _prompt_variables["ssh_host"] = ssh.get_host();
      } else {
        _prompt_variables["ssh_host"] = "";
      }

      if (session->get_member("__connection_info").descr().find("TCP") !=
          std::string::npos) {
        port = options.has_port() ? std::to_string(options.get_port()) : "";
      } else {
        if (options.has_socket())
          socket = options.get_socket();
        else
          socket = "default";
      }
      if (session->session_type() == mysqlsh::SessionType::Classic) {
        _prompt_variables["session"] = "c";
        if (socket.empty() && port.empty()) port = "3306";
      } else {
        if (socket.empty() && port.empty()) port = "33060";
        _prompt_variables["session"] = "x";
      }
      _prompt_variables["port"] = port;
      _prompt_variables["socket"] = socket;
      _prompt_variables["node_type"] = session->get_node_type();
      _prompt_variables["connection_id"] =
          std::to_string(session->get_connection_id());
    } else {
      _prompt_variables["ssl"] = "";
      _prompt_variables["uri"] = "";
      _prompt_variables["user"] = "";
      _prompt_variables["host"] = "";
      _prompt_variables["port"] = "";
      _prompt_variables["socket"] = "";
      _prompt_variables["session"] = "";
      _prompt_variables["node_type"] = "";
      _prompt_variables["connection_id"] = "";
    }
  }

  _prompt_variables["trx"] = "";
  _prompt_variables["autocommit"] = "";
  _prompt_variables["slow_query"] = "";
  if (session && session->is_open()) {
    if (m_pending_update != Prompt_variables_update_type::NO_UPDATE) {
      try {
        _prompt_variables["schema"] = session->get_current_schema();
      } catch (...) {
        _prompt_variables["schema"] = "";
      }
    }

    if (auto s = std::dynamic_pointer_cast<mysqlshdk::db::mysql::Session>(
            session->get_core_session())) {
      auto status = s->get_server_status();
      if (status & SERVER_STATUS_IN_TRANS) _prompt_variables["trx"] = "*";
      if (status & SERVER_STATUS_IN_TRANS_READONLY)
        _prompt_variables["trx"] = "^";
      if ((status & SERVER_STATUS_AUTOCOMMIT) == 0)
        _prompt_variables["autocommit"] = ".";
      if (status & SERVER_QUERY_WAS_SLOW) _prompt_variables["slow_query"] = "&";
    }
  } else {
    if (m_pending_update != Prompt_variables_update_type::NO_UPDATE) {
      _prompt_variables["schema"] = "";
    }
  }
  switch (_shell->interactive_mode()) {
    case shcore::IShell_core::Mode::None:
      break;
    case shcore::IShell_core::Mode::SQL:
      _prompt_variables["mode"] = "sql";
      _prompt_variables["Mode"] = "SQL";
      break;
    case shcore::IShell_core::Mode::JavaScript:
      _prompt_variables["mode"] = "js";
      _prompt_variables["Mode"] = "JS";
      break;
    case shcore::IShell_core::Mode::Python:
      _prompt_variables["mode"] = "py";
      _prompt_variables["Mode"] = "Py";
      break;
  }

  m_pending_update = Prompt_variables_update_type::NO_UPDATE;
}

bool Base_shell::switch_shell_mode(shcore::Shell_core::Mode mode,
                                   const std::vector<std::string> & /*args*/,
                                   bool initializing,
                                   bool prompt_variables_update) {
  shcore::Shell_core::Mode old_mode = _shell->interactive_mode();
  bool lang_initialized = false;

  if (old_mode != mode) {
    _input_mode = shcore::Input_state::Ok;
    _input_buffer.clear();

    switch (mode) {
      case shcore::Shell_core::Mode::None:
        break;
      case shcore::Shell_core::Mode::SQL:
        if (_shell->switch_mode(mode) && !initializing)
          println("Switching to SQL mode... Commands end with ;");
        {
          auto sql =
              static_cast<shcore::Shell_sql *>(_shell->language_object(mode));
          if (!sql->result_processor()) {
            sql->set_result_processor(
                std::bind(&Base_shell::process_sql_result, this, _1, _2));
            lang_initialized = true;
          }
        }
        break;
      case shcore::Shell_core::Mode::JavaScript:
#ifdef HAVE_V8
        if (_shell->switch_mode(mode) && !initializing)
          println("Switching to JavaScript mode...");
        {
          auto js = static_cast<shcore::Shell_javascript *>(
              _shell->language_object(mode));
          if (!js->result_processor()) {
            js->set_result_processor(
                std::bind(&Base_shell::process_result, this, _1, _2));
            _completer.add_provider(
                shcore::IShell_core::Mode_mask(
                    shcore::IShell_core::Mode::JavaScript),
                std::unique_ptr<shcore::completer::Provider>(
                    new shcore::completer::Provider_javascript(
                        _completer_object_registry, js->javascript_context())));
            lang_initialized = true;
          }
        }
#else
        println("JavaScript mode is not supported, command ignored.");
#endif
        break;
      case shcore::Shell_core::Mode::Python:
#ifdef HAVE_PYTHON
        if (_shell->switch_mode(mode) && !initializing)
          println("Switching to Python mode...");
        {
          auto py = static_cast<shcore::Shell_python *>(
              _shell->language_object(mode));
          if (!py->result_processor()) {
            py->set_result_processor(
                std::bind(&Base_shell::process_result, this, _1, _2));
            _completer.add_provider(
                shcore::IShell_core::Mode_mask(
                    shcore::IShell_core::Mode::Python),
                std::unique_ptr<shcore::completer::Provider>(
                    new shcore::completer::Provider_python(
                        _completer_object_registry, py->python_context())));
            lang_initialized = true;
          }
        }
#else
        println("Python mode is not supported, command ignored.");
#endif
        break;
    }

    // load scripts for standard locations
    if (lang_initialized) {
      load_default_modules(mode);
      init_scripts(mode);
    }
  }

  if (prompt_variables_update) request_prompt_variables_update();

  return lang_initialized;
}

/**
 * Print output after the shell initialization is done (after Copyright info)
 * @param str text to be printed.
 */
void Base_shell::println_deferred(const std::string &str) {
  // This can't be called once the deferred output is flushed
  assert(_deferred_output != nullptr);
  _deferred_output->append(str + "\n");
}

void Base_shell::clear_input() {
  _input_mode = shcore::Input_state::Ok;
  _input_buffer.clear();
  _shell->clear_input();
}

void Base_shell::process_line(const std::string &line) {
  if (_input_mode == shcore::Input_state::ContinuedBlock && line.empty()) {
    _input_mode = shcore::Input_state::Ok;
  }

  // Appends the line, no matter if it is an empty line
  _input_buffer.append(_shell->preprocess_input_line(line));

  // Appends the new line if anything has been added to the buffer
  if (!_input_buffer.empty()) {
    _input_buffer.append("\n");
  }

  if (_input_mode != shcore::Input_state::ContinuedBlock &&
      !_input_buffer.empty()) {
    execute_buffered_code(false);
  }
}

void Base_shell::execute_buffered_code(bool flush) {
  std::string to_history;
  try {
    if (flush) {
      shcore::Scoped_callback reset_state([&]() { clear_input(); });
      _shell->flush_input(_input_buffer);
    } else {
      _shell->handle_input(_input_buffer, _input_mode);
    }

    // Here we analyze the input mode as it was let after executing the code
    if (_input_mode == shcore::Input_state::Ok) {
      to_history = _shell->get_handled_input();
    }
  } catch (shcore::Exception &exc) {
    print_value(shcore::Value(exc.error()), "error");
    to_history = _input_buffer;
  } catch (const std::exception &exc) {
    std::string error(exc.what());
    error += "\n";
    print_diag(error);
    to_history = _input_buffer;
  }

  // TODO: Do we need this cleanup? i.e. in case of exceptions above??
  // Clears the buffer if OK, if continued, buffer will contain
  // the non executed code
  if (_input_mode == shcore::Input_state::Ok) {
    _input_buffer.clear();
  }

  if (!to_history.empty()) {
    notify_executed_statement(to_history);
  }
}

void Base_shell::flush_input() { execute_buffered_code(true); }

void Base_shell::notify_executed_statement(const std::string &line) {
  shcore::Value::Map_type_ref data(new shcore::Value::Map_type());
  (*data)["statement"] = shcore::Value(line);
  shcore::ShellNotifications::get()->notify("SN_STATEMENT_EXECUTED", nullptr,
                                            data);
}

void Base_shell::process_sql_result(
    const std::shared_ptr<mysqlshdk::db::IResult> &result,
    const shcore::Sql_result_info & /*info*/) {
  if (!result) {
    // Return value of undefined implies an error processing
    // TODO(alfredo) - signaling of errors should be moved down
    _shell->set_error_processing();
  }
}

void Base_shell::print_result(const shcore::Value &result) {
  if (result) {
    if (result.type == shcore::Object) {
      auto object = result.as_object();
      auto shell_hook = get_shell_hook(object->class_name());
      if (!shell_hook.empty()) {
        if (object->has_member(shell_hook)) {
          shcore::Value hook_result = object->call(shell_hook, {});

          // Recursive call to continue processing shell hooks if any
          process_result(hook_result, false);

          return;
        }
      }
    }

    // In JSON mode: the json representation is used for Object, Array and
    // Map For anything else a map is printed with the "value" key
    std::string tag;
    if (result.type != shcore::Object && result.type != shcore::Array &&
        result.type != shcore::Map)
      tag = "value";

    print_value(result, tag);
  }
}

void Base_shell::process_result(const shcore::Value &result, bool got_error) {
  assert(_shell->interactive_mode() != shcore::Shell_core::Mode::SQL);

  if (options().interactive) print_result(result);

  if (got_error) _shell->set_error_processing();
}

int Base_shell::process_file(const std::string &path,
                             const std::vector<std::string> &argv) {
  // Default return value will be 1 indicating there were errors
  int ret_val = 1;

  if (path.empty()) {
    print_diag("Invalid filename");
  } else {
    std::string file = shcore::path::expand_user(path);
    if (shcore::is_folder(file)) {
      print_diag(
          shcore::str_format("Failed to open file: '%s' is a "
                             "directory\n",
                             file.c_str()));
      return ret_val;
    }

#ifdef _WIN32
    std::ifstream s(shcore::utf8_to_wide(file));
#else
    std::ifstream s(file.c_str());
#endif

    if (!s.fail()) {
      // The return value now depends on the stream processing
      ret_val = process_stream(s, file, argv);

      // When force is used, we do not care of the processing
      // errors
      if (options().force) ret_val = 0;

      s.close();
    } else {
      // TODO: add a log entry once logging is
      print_diag(shcore::str_format("Failed to open file '%s', error: %s\n",
                                    file.c_str(), std::strerror(errno)));
    }
  }

  return ret_val;
}

int Base_shell::run_module(const std::string &module,
                           const std::vector<std::string> &argv) {
  if (module.empty()) {
    print_diag("Invalid module name");
    return 1;
  }

  _shell->execute_module(module, argv);

  return 0;
}

int Base_shell::process_stream(std::istream &stream, const std::string &source,
                               const std::vector<std::string> &argv,
                               bool force_batch) {
  _shell->set_argv(argv);

  // If interactive is set, it means that the shell was started with the option
  // to Emulate interactive mode while processing the stream
  if (!force_batch && options().interactive) {
    if (options().full_interactive) print(prompt());

    bool comment_first_js_line =
        _shell->interactive_mode() == shcore::IShell_core::Mode::JavaScript;

    std::string line;
    while (!stream.eof()) {
      line.clear();

      shcore::getline(stream, line);

      // When processing JavaScript files, validates the very first line to
      // start with #! If that's the case, it is replaced by a comment indicator
      // //
      if (comment_first_js_line && line.size() > 1 && line[0] == '#' &&
          line[1] == '!')
        line.replace(0, 2, "//");

      comment_first_js_line = false;

      if (options().full_interactive) println(line);

      process_line(line);

      if (options().full_interactive) print(prompt());
    }

    // If stream ended, and state is in continuation mode, the execution is
    // forced
    if (_input_mode == shcore::Input_state::ContinuedBlock ||
        _input_mode == shcore::Input_state::ContinuedSingle) {
      flush_input();
    }

    // Being interactive, we do not care about the return value
    return 0;
  } else {
    return _shell->process_stream(stream, source);
  }
}

void Base_shell::set_global_object(
    const std::string &name,
    const std::shared_ptr<shcore::Cpp_object_bridge> &object,
    shcore::IShell_core::Mode_mask modes) {
  if (object) {
    _shell->set_global(name, shcore::Value(object), modes);
  } else {
    _shell->set_global(name, shcore::Value(), modes);
  }
}

}  // namespace mysqlsh
