#if defined(PROJECT_ENABLE_BACKWARD_CPP)
#include "backward.hpp"
#endif // defined(PROJECT_ENABLE_BACKWARD_CPP)

#include <execution>

#include "initialize.hpp"

#include "data_db.hpp"
#include "settings.hpp"
#include "utils/common.hpp"
#include "utils/config.hpp"
#include "utils/file.hpp"
#include "utils/path.hpp"
#include "utils/time.hpp"
#include "utils/unix.hpp"
#include "utils/windows.hpp"

namespace monitarr {
[[nodiscard]] static auto create_client(const server_cfg &server)
    -> httplib::Client {
  httplib::Client cli{server.url};
  cli.set_default_headers({
      {"X-Api-Key", server.api_key},
  });

  return cli;
}

static void remove_stalled(std::string_view download_id, std::string_view title,
                           std::uint64_t episode_id, std::uint64_t movie_id,
                           const server_cfg &server, data_db &state_db) {
  fmt::println("remove and block {}|{}", download_id, title);
  state_db.remove(download_id);

  auto cli = create_client(server);
  auto response = cli.Delete(
      fmt::format("/api/{}/queue/{}?blocklist=true&skipRedownload=false",
                  server.api_version,
                  utils::string::split(download_id, '/', false).at(1U)));
  if (response->status != httplib::StatusCode::OK_200) {
    fmt::println("remove and block result|{}|{}", server.url, response->status);
    return;
  }

  if (utils::string::contains("radarr", server.id)) {
    nlohmann::json data({
        {"name", "MoviesSearch"},
        {"movieIds", {movie_id}},
    });

    response = cli.Post("/api/{}/command", data.dump(), "application/json");
    if (response->status != httplib::StatusCode::OK_200) {
      fmt::println("failed to search radarr|{}|{}|{}|{}", server.url, title,
                   movie_id, response->status);
    }
    return;
  }

  if (utils::string::contains("sonarr", server.id)) {
    nlohmann::json data({
        {"name", "EpisodeSearch"},
        {"episodeIds", {episode_id}},
    });

    response = cli.Post("/api/{}/command", data.dump(), "application/json");
    if (response->status != httplib::StatusCode::OK_200) {
      fmt::println("failed to search sonarr|{}|{}|{}|{}", server.url, title,
                   episode_id, response->status);
    }
    return;
  }
}

static void check_server(const server_cfg &server, data_db &state_db) {
  fmt::println("checking server|{}|{}", server.id, server.url);

  auto cli = create_client(server);

  std::uint16_t page{0U};
  while (++page != 0U) {
    httplib::Params params;
    params.emplace("page", std::to_string(page));
    params.emplace("pageSize", "50");

    auto response =
        cli.Get(fmt::format("/api/{}/queue", server.api_version), params, {});
    if (response->status != httplib::StatusCode::OK_200) {
      fmt::println("check server request failed|{}|{}|{}", server.id,
                   server.url, response->status);
      break;
    }

    auto json_data = nlohmann::json::parse(response->body);
    if (json_data.at("page").get<std::uint32_t>() != page) {
      break;
    }

    auto now = utils::time::get_time_now();

    for (const auto &record : json_data.at("records")) {
      auto download_id = fmt::format(
          "{}/{}", server.id, record.at("downloadId").get<std::string>());
      auto episode_id = record.contains("episodeId")
                            ? record["episodeId"].get<std::uint64_t>()
                            : std::uint64_t{0U};
      auto movie_id = record.contains("movieId")
                          ? record["movieId"].get<std::uint64_t>()
                          : std::uint64_t{0U};
      auto size_left = record.at("sizeleft").get<std::uint64_t>();
      auto title = record.at("title").get<std::string>();

      auto data = state_db.get(download_id);
      const auto update_entry = [&download_id, &now, &size_left, &state_db,
                                 &title, url = server.url]() {
        if (size_left == 0U) {
          state_db.remove(download_id);
          return;
        }

        fmt::println("updating {}|{}|{}|{}", download_id, title, now,
                     size_left);
        state_db.set(data_entry{
            download_id,
            now,
            size_left,
        });
      };

      if (data.has_value()) {
        if (std::chrono::nanoseconds(now - data->last_check) >=
            server.timeout) {
          if (size_left == data->size_left) {
            remove_stalled(download_id, title, episode_id, movie_id, server,
                           state_db);
          } else {
            update_entry();
          }
        } else if (size_left == 0U) {
          state_db.remove(download_id);
        }
      } else {
        update_entry();
      }
    }
  }
}

[[nodiscard]] static auto load_config(std::string &cfg_file) -> app_config {
  auto cfg_dir = utils::get_environment_variable("MONITARR_CFG_DIR");
  if (cfg_dir.empty()) {
    cfg_dir = utils::path::combine(".", {"config"});
  }

  if (not utils::file::directory{cfg_dir}.create_directory()) {
    throw std::runtime_error(fmt::format("failed to create config dir|{}",
                                         cfg_dir,
                                         utils::get_last_error_code()));
  }

  cfg_file = utils::path::combine(cfg_dir, {"monitarr.json"});

  fmt::println("loading config|{}", cfg_file);
  app_config cfg{};
  cfg.load(cfg_file);
  return cfg;
}

[[nodiscard]] static auto load_db() -> data_db {
  auto data_dir = utils::get_environment_variable("MONITARR_DATA_DIR");
  if (data_dir.empty()) {
    data_dir = utils::path::combine(".", {"data"});
  }

  if (not utils::file::directory{data_dir}.create_directory()) {
    throw std::runtime_error(fmt::format("failed to create data dir|{}",
                                         data_dir,
                                         utils::get_last_error_code()));
  }

  data_db state_db{};
  state_db.open(data_dir);
  return state_db;
}
} // namespace monitarr

using namespace monitarr;

auto main(int /* argc */, char ** /* argv */) -> int {
  MONITARR_USES_FUNCTION_NAME();

#if defined(PROJECT_ENABLE_BACKWARD_CPP)
  static backward::SignalHandling sh;
#endif // defined(PROJECT_ENABLE_BACKWARD_CPP)

  if (not monitarr::project_initialize()) {
    return -1;
  }

  static std::mutex mtx;
  static std::condition_variable notify;
  static stop_type stop_requested{false};

  static const auto quit_handler = [](int sig) {
    fmt::println("stop requested|{}", sig);
    stop_requested = true;

    mutex_lock lock(mtx);
    notify.notify_all();
  };

  std::signal(SIGINT, quit_handler);
  std::signal(SIGQUIT, quit_handler);
  std::signal(SIGTERM, quit_handler);

  auto ret{0};

  try {
    std::string cfg_file;
    auto cfg{load_config(cfg_file)};
    auto state_db{load_db()};

    if (cfg.server_list.empty()) {
      fmt::println("no servers have been configured");
      ret = 3;
    } else {
      while (not stop_requested) {
        std::for_each(std::execution::par, cfg.server_list.begin(),
                      cfg.server_list.end(), [&state_db](auto &&server) {
                        if (stop_requested) {
                          return;
                        }

                        try {
                          check_server(server, state_db);
                        } catch (const std::exception &ex) {
                          utils::error::handle_exception(function_name, ex);
                        } catch (...) {
                          utils::error::handle_exception(function_name);
                        }
                      });
        unique_mutex_lock lock(mtx);
        if (stop_requested) {
          continue;
        }

        fmt::println("waiting for next check|{}", cfg.check_interval);
        notify.wait_for(lock, cfg.check_interval);
      }
    }

    cfg.save(cfg_file);
    state_db.close();
  } catch (const std::exception &ex) {
    utils::error::handle_exception(function_name, ex);
    ret = 2;
  } catch (...) {
    utils::error::handle_exception(function_name);
    ret = 2;
  }

  fmt::println("terminating application|{}", ret);
  monitarr::project_cleanup();
  return ret;
}
