// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2005-2011, Jari Sundell <jaris@ifi.uio.no>

#include <algorithm>
#include <cstdio>
#include <functional>
#include <torrent/hash_string.h>
#include <torrent/rate.h>
#include <torrent/utils/directory_events.h>
#include <torrent/utils/error_number.h>
#include <torrent/utils/file_stat.h>
#include <torrent/utils/log.h>
#include <torrent/utils/path.h>
#include <torrent/utils/string_manip.h>

#include "core/download.h"
#include "core/download_list.h"
#include "core/manager.h"
#include "core/view_manager.h"
#include "rpc/command_scheduler.h"
#include "rpc/parse.h"
#include "rpc/parse_commands.h"

#include "command_helpers.h"
#include "control.h"
#include "globals.h"

#include "thread_worker.h"

torrent::Object
apply_on_ratio(const torrent::Object& rawArgs) {
  const std::string& groupName = rawArgs.as_string();

  const auto& buffer_size = 32 + groupName.size();
  auto        buffer = static_cast<char*>(calloc(buffer_size, sizeof(char)));
  snprintf(buffer, buffer_size, "group2.%s.view", groupName.c_str());

  core::ViewManager::iterator viewItr = control->view_manager()->find(
    rpc::commands.call(buffer, rpc::make_target()).as_string());

  if (viewItr == control->view_manager()->end())
    throw torrent::input_error("Could not find view.");

  char* bufferStart =
    buffer +
    snprintf(buffer, buffer_size, "group2.%s.ratio.", groupName.c_str());

  // first argument:  minimum ratio to reach
  // second argument: minimum upload amount to reach [optional]
  // third argument:  maximum ratio to reach [optional]
  std::strcpy(bufferStart, "min");
  int64_t minRatio = rpc::commands.call(buffer, rpc::make_target()).as_value();
  std::strcpy(bufferStart, "max");
  int64_t maxRatio = rpc::commands.call(buffer, rpc::make_target()).as_value();
  std::strcpy(bufferStart, "upload");
  int64_t minUpload = rpc::commands.call(buffer, rpc::make_target()).as_value();

  std::vector<core::Download*> downloads;

  for (core::View::iterator itr  = (*viewItr)->begin_visible(),
                            last = (*viewItr)->end_visible();
       itr != last;
       itr++) {
    if (!(*itr)->is_seeding() ||
        rpc::call_command_value("d.ignore_commands", rpc::make_target(*itr)) !=
          0)
      continue;

    //    rpc::parse_command_single(rpc::make_target(*itr), "print={Checked
    //    ratio of download.}");

    int64_t totalDone   = (*itr)->download()->bytes_done();
    int64_t totalUpload = (*itr)->info()->up_rate()->total();

    if (!(totalUpload >= minUpload &&
          totalUpload * 100 >= totalDone * minRatio) &&
        !(maxRatio > 0 && totalUpload * 100 > totalDone * maxRatio))
      continue;

    downloads.push_back(*itr);
  }

  snprintf(buffer, buffer_size, "group.%s.ratio.command", groupName.c_str());

  for (std::vector<core::Download*>::iterator itr  = downloads.begin(),
                                              last = downloads.end();
       itr != last;
       itr++) {
    //    rpc::commands.call("print", rpc::make_target(*itr), "Calling ratio
    //    command.");
    rpc::commands.call_catch(buffer,
                             rpc::make_target(*itr),
                             torrent::Object(),
                             "Ratio reached, but command failed: ");
  }

  free(buffer);
  return torrent::Object();
}

torrent::Object
apply_start_tied() {
  for (core::DownloadList::iterator itr =
         control->core()->download_list()->begin();
       itr != control->core()->download_list()->end();
       ++itr) {
    if (rpc::call_command_value("d.state", rpc::make_target(*itr)) == 1)
      continue;

    torrent::utils::file_stat fs;
    const std::string&        tiedToFile =
      rpc::call_command_string("d.tied_to_file", rpc::make_target(*itr));

    if (!tiedToFile.empty() &&
        fs.update(torrent::utils::path_expand(tiedToFile)))
      rpc::parse_command_single(rpc::make_target(*itr), "d.try_start=");
  }

  return torrent::Object();
}

torrent::Object
apply_stop_untied() {
  for (core::DownloadList::iterator itr =
         control->core()->download_list()->begin();
       itr != control->core()->download_list()->end();
       ++itr) {
    if (rpc::call_command_value("d.state", rpc::make_target(*itr)) == 0)
      continue;

    torrent::utils::file_stat fs;
    const std::string&        tiedToFile =
      rpc::call_command_string("d.tied_to_file", rpc::make_target(*itr));

    if (!tiedToFile.empty() &&
        !fs.update(torrent::utils::path_expand(tiedToFile)))
      rpc::parse_command_single(rpc::make_target(*itr), "d.try_stop=");
  }

  return torrent::Object();
}

torrent::Object
apply_close_untied() {
  for (core::DownloadList::iterator itr =
         control->core()->download_list()->begin();
       itr != control->core()->download_list()->end();
       ++itr) {
    torrent::utils::file_stat fs;
    const std::string&        tiedToFile =
      rpc::call_command_string("d.tied_to_file", rpc::make_target(*itr));

    if (rpc::call_command_value("d.ignore_commands", rpc::make_target(*itr)) ==
          0 &&
        !tiedToFile.empty() &&
        !fs.update(torrent::utils::path_expand(tiedToFile)))
      rpc::parse_command_single(rpc::make_target(*itr), "d.try_close=");
  }

  return torrent::Object();
}

torrent::Object
apply_remove_untied() {
  for (core::DownloadList::iterator itr =
         control->core()->download_list()->begin();
       itr != control->core()->download_list()->end();) {
    torrent::utils::file_stat fs;
    const std::string&        tiedToFile =
      rpc::call_command_string("d.tied_to_file", rpc::make_target(*itr));

    if (!tiedToFile.empty() &&
        !fs.update(torrent::utils::path_expand(tiedToFile))) {
      // Need to clear tied_to_file so it doesn't try to delete it.
      rpc::call_command(
        "d.tied_to_file.set", std::string(), rpc::make_target(*itr));

      itr = control->core()->download_list()->erase(itr);

    } else {
      ++itr;
    }
  }

  return torrent::Object();
}

torrent::Object
apply_schedule(const torrent::Object::list_type& args) {
  if (args.size() != 4)
    throw torrent::input_error("Wrong number of arguments.");

  torrent::Object::list_const_iterator itr = args.begin();

  const std::string& arg1 = (itr++)->as_string();
  const std::string& arg2 = (itr++)->as_string();
  const std::string& arg3 = (itr++)->as_string();

  control->command_scheduler()->parse(arg1, arg2, arg3, *itr);

  return torrent::Object();
}

torrent::Object
apply_load(const torrent::Object::list_type& args, int flags) {
  torrent::Object::list_const_iterator argsItr = args.begin();

  if (argsItr == args.end())
    throw torrent::input_error("Too few arguments.");

  const std::string&               filename = argsItr->as_string();
  core::Manager::command_list_type commands;

  while (++argsItr != args.end())
    commands.push_back(argsItr->as_string());

  return control->core()->try_create_download_expand(filename, flags, commands);
}

void
apply_import(const std::string& path) {
  if (!rpc::parse_command_file(path))
    throw torrent::input_error("Could not open option file: " + path);
}
void
apply_try_import(const std::string& path) {
  if (!rpc::parse_command_file(path))
    control->core()->push_log_std("Could not read resource file: " + path);
}

torrent::Object
apply_close_low_diskspace(int64_t arg) {
  core::DownloadList* downloadList = control->core()->download_list();

  bool                    closed = false;
  core::Manager::DListItr itr    = downloadList->begin();

  while ((itr = std::find_if(itr,
                             downloadList->end(),
                             std::mem_fn(&core::Download::is_downloading))) !=
         downloadList->end()) {
    if ((*itr)->file_list()->free_diskspace() < (uint64_t)arg) {
      downloadList->close(*itr);

      (*itr)->set_hash_failed(true);
      (*itr)->set_message(std::string("Low diskspace."));

      closed = true;
    }

    ++itr;
  }

  if (closed)
    lt_log_print(torrent::LOG_TORRENT_ERROR,
                 "Closed torrents due to low diskspace.");

  return torrent::Object();
}

torrent::Object
apply_download_list(const torrent::Object::list_type& args) {
  torrent::Object::list_const_iterator argsItr = args.begin();

  core::ViewManager*          viewManager = control->view_manager();
  core::ViewManager::iterator viewItr;

  if (argsItr != args.end() && !argsItr->as_string().empty())
    viewItr = viewManager->find((argsItr++)->as_string());
  else
    viewItr = viewManager->find("default");

  if (viewItr == viewManager->end())
    throw torrent::input_error("Could not find view.");

  torrent::Object             result     = torrent::Object::create_list();
  torrent::Object::list_type& resultList = result.as_list();

  for (core::View::const_iterator itr  = (*viewItr)->begin_visible(),
                                  last = (*viewItr)->end_visible();
       itr != last;
       itr++) {
    const torrent::HashString* hashString = &(*itr)->info()->hash();

    resultList.push_back(
      torrent::utils::transform_hex(hashString->begin(), hashString->end()));
  }

  return result;
}

torrent::Object
d_multicall(const torrent::Object::list_type& args) {
  if (args.empty())
    throw torrent::input_error("Too few arguments.");

  core::ViewManager*          viewManager = control->view_manager();
  core::ViewManager::iterator viewItr;

  if (!args.front().as_string().empty())
    viewItr = viewManager->find(args.front().as_string());
  else
    viewItr = viewManager->find("default");

  if (viewItr == viewManager->end())
    throw torrent::input_error("Could not find view.");

  // [(cmd, cmd_args)]
  std::vector<std::pair<rpc::CommandMap::iterator, torrent::Object>> parsed;
  parsed.reserve(args.size() - 1);

  for (size_t i = 1; i < args.size(); ++i) {
    const auto& arg = args[i].as_string();

    char key[128];
    auto cmd_args = torrent::Object();
    auto start    = arg.c_str();

    if (!rpc::parse_line(key, cmd_args, start, start + arg.size())) {
      throw torrent::input_error("Failed to parse command.");
    }

    auto cmd = rpc::commands.find(key);
    if (cmd == rpc::commands.end()) {
      throw torrent::input_error("Command \"" + std::string(key) +
                                 "\" does not exist.");
    }

    parsed.emplace_back(cmd, cmd_args);
  }

  unsigned int     dlist_size = (*viewItr)->size_visible();
  core::Download** dlist =
    static_cast<core::Download**>(malloc(sizeof(core::Download*) * dlist_size));

  std::copy((*viewItr)->begin_visible(), (*viewItr)->end_visible(), dlist);

  auto  resultRaw = torrent::Object::create_list();
  auto& result    = resultRaw.as_list();

  result.resize(dlist_size, torrent::Object::create_list());

  for (size_t i = 0; i < dlist_size; ++i) {
    torrent::Object::list_type& row = result[i].as_list();

    row.reserve(parsed.size());

    for (const auto& [cmd, cmd_args] : parsed) {
      row.push_back(
        rpc::parse_command_(rpc::make_target(dlist[i]), cmd, cmd_args));
    }
  }

  free(dlist);

  return resultRaw;
}

torrent::Object
d_multicall_filtered(const torrent::Object::list_type& args) {
  if (args.size() < 2)
    throw torrent::input_error(
      "d.multicall.filtered requires at least 2 arguments.");
  torrent::Object::list_const_iterator arg = args.begin();

  // Find the given view
  core::ViewManager*          viewManager = control->view_manager();
  core::ViewManager::iterator viewItr =
    viewManager->find(arg->as_string().empty() ? "default" : arg->as_string());

  if (viewItr == viewManager->end())
    throw torrent::input_error("Could not find view '" + arg->as_string() +
                               "'.");

  // Make a filtered copy of the current item list
  core::View::base_type dlist;
  (*viewItr)->filter_by(*++arg, dlist);

  // Generate result by iterating over all items
  torrent::Object             resultRaw = torrent::Object::create_list();
  torrent::Object::list_type& result    = resultRaw.as_list();
  ++arg; // skip to first command

  for (core::View::iterator item = dlist.begin(); item != dlist.end(); ++item) {
    // Add empty row to result
    torrent::Object::list_type& row =
      result.insert(result.end(), torrent::Object::create_list())->as_list();

    // Call the provided commands and assemble their results
    for (torrent::Object::list_const_iterator command = arg;
         command != args.end();
         command++) {
      const std::string& cmdstr = command->as_string();
      row.push_back(rpc::parse_command(rpc::make_target(*item),
                                       cmdstr.c_str(),
                                       cmdstr.c_str() + cmdstr.size())
                      .first);
    }
  }

  return resultRaw;
}

static void
call_watch_command(const std::string& command, const std::string& path) {
  rpc::commands.call_catch(command.c_str(), rpc::make_target(), path);
}

torrent::Object
directory_watch_added(const torrent::Object::list_type& args) {
  if (args.size() != 2)
    throw torrent::input_error("Too few arguments.");

  const std::string& path    = args.front().as_string();
  const std::string& command = args.back().as_string();

  if (!control->directory_events()->open())
    throw torrent::input_error(
      "Could not open inotify:" +
      torrent::utils::error_number::current().message());

  control->directory_events()->notify_on(
    path.c_str(),
    torrent::directory_events::flag_on_added |
      torrent::directory_events::flag_on_updated,
    [command](const auto& path) { return call_watch_command(command, path); });
  return torrent::Object();
}

void
initialize_command_events() {
  CMD2_ANY_STRING("on_ratio", [](const auto&, const auto& rawArgs) {
    return apply_on_ratio(rawArgs);
  });

  CMD2_ANY("start_tied",
           [](const auto&, const auto&) { return apply_start_tied(); });
  CMD2_ANY("stop_untied",
           [](const auto&, const auto&) { return apply_stop_untied(); });
  CMD2_ANY("close_untied",
           [](const auto&, const auto&) { return apply_close_untied(); });
  CMD2_ANY("remove_untied",
           [](const auto&, const auto&) { return apply_remove_untied(); });

  CMD2_ANY_LIST("schedule2", [](const auto&, const auto& args) {
    return apply_schedule(args);
  });
  CMD2_ANY_STRING_V("schedule_remove2", [](const auto&, const auto& key) {
    control->command_scheduler()->erase_str(key);
  });

  CMD2_ANY_STRING_V(
    "import", [](const auto&, const auto& path) { return apply_import(path); });
  CMD2_ANY_STRING_V("try_import", [](const auto&, const auto& path) {
    return apply_try_import(path);
  });

  CMD2_ANY_LIST("load.normal", [](const auto&, const auto& args) {
    return apply_load(args,
                      core::Manager::create_quiet | core::Manager::create_tied);
  });
  CMD2_ANY_LIST("load.throw", [](const auto&, const auto& args) {
    return apply_load(args,
                      core::Manager::create_throw | core::Manager::create_tied);
  });
  CMD2_ANY_LIST("load.verbose", [](const auto&, const auto& args) {
    return apply_load(args, core::Manager::create_tied);
  });
  CMD2_ANY_LIST("load.start", [](const auto&, const auto& args) {
    return apply_load(args,
                      core::Manager::create_quiet | core::Manager::create_tied |
                        core::Manager::create_start);
  });
  CMD2_ANY_LIST("load.start_throw", [](const auto&, const auto& args) {
    return apply_load(args,
                      core::Manager::create_start |
                        core::Manager::create_throw |
                        core::Manager::create_tied);
  });
  CMD2_ANY_LIST("load.start_verbose", [](const auto&, const auto& args) {
    return apply_load(args,
                      core::Manager::create_tied | core::Manager::create_start);
  });
  CMD2_ANY_LIST("load.raw", [](const auto&, const auto& args) {
    return apply_load(
      args, core::Manager::create_quiet | core::Manager::create_raw_data);
  });
  CMD2_ANY_LIST("load.raw_verbose", [](const auto&, const auto& args) {
    return apply_load(args, core::Manager::create_raw_data);
  });
  CMD2_ANY_LIST("load.raw_start", [](const auto&, const auto& args) {
    return apply_load(args,
                      core::Manager::create_quiet |
                        core::Manager::create_start |
                        core::Manager::create_raw_data);
  });
  CMD2_ANY_LIST("load.raw_start_verbose", [](const auto&, const auto& args) {
    return apply_load(
      args, core::Manager::create_start | core::Manager::create_raw_data);
  });

  CMD2_ANY_VALUE("close_low_diskspace", [](const auto&, const auto& arg) {
    return apply_close_low_diskspace(arg);
  });

  CMD2_ANY_LIST("download_list", [](const auto&, const auto& args) {
    return apply_download_list(args);
  });
  CMD2_ANY_LIST("d.multicall2", [](const auto&, const auto& args) {
    return d_multicall(args);
  });
  CMD2_ANY_LIST("d.multicall.filtered", [](const auto&, const auto& args) {
    return d_multicall_filtered(args);
  });

  CMD2_ANY_LIST("directory.watch.added", [](const auto&, const auto& args) {
    return directory_watch_added(args);
  });
}
