#include <cstdlib>
#include <filesystem>
#include <format>
#include <fstream>
#include <ranges>
#include <QStyleHints>
#include <qlogging.h>
#include <qstylehints.h>
#include <string_view>
#include <QGuiApplication>
#include <utility>
#include "utils.hpp"
#include "config.hpp"

namespace fs = std::filesystem;

namespace config {
static constexpr const char *TOP_COMMENT =
    R"(// This configuration is merged with the default vicinae configuration file, which you can obtain by running the `vicinae config default` command.
// Every item defined in this file takes precedence over the values defined in the default config or any other imported file.
//
// You can make manual edits to this file, however you should keep in mind that this file may be written to by vicinae when a configuration change is made through the GUI.
// When that happens, any custom comments or formatting will be lost.
//
// If you want to maintain a configuration file with your own comments and formatting, you should create a separate file and add it to the 'imports' array.
//
// Learn more about configuration at https://docs.vicinae.com/config)";

template <typename T> T static merge(const auto &v1, const auto &v2) {
  std::string buf;
  if (auto error = glz::write_json(glz::merge{v1, v2}, buf)) {
    std::cerr << "Failed to merge " << glz::format_error(error);
    // todo: do smth about that
  }
  T cfg;
  if (auto error = glz::read_json(cfg, buf)) {
    qWarning() << "Failed to read merged " << glz::format_error(error);
  }
  return cfg;
}

const SystemThemeConfig &ConfigValue::systemTheme() const {
  switch (QGuiApplication::styleHints()->colorScheme()) {
  case Qt::ColorScheme::Light:
    return theme.light;
  default:
    return theme.dark;
  }
}

Manager::Manager(fs::path path) : m_userPath(std::move(path)) {
  auto file = QFile(":config.jsonc");

  if (!file.open(QIODevice::ReadOnly)) { throw std::runtime_error("Failed to open default config"); }

  m_defaultData = file.readAll().toStdString();

  if (auto error = glz::read<glz::opts{.comments = true, .error_on_unknown_keys = false}>(m_defaultConfig,
                                                                                          m_defaultData)) {
    throw std::runtime_error(
        std::format("Failed to parse default config file: {}", glz::format_error(error, m_defaultData)));
  }

  m_fsDebounce.setSingleShot(true);
  m_fsDebounce.setInterval(100);

  if (const char *envOverrides = std::getenv("VICINAE_OVERRIDES")) {
    m_envOverrides =
        std::string_view{envOverrides} | std::views::split(':') |
        std::views::transform([](const auto &part) { return std::string{part.begin(), part.end()}; }) |
        std::ranges::to<std::vector<std::string>>();

    qInfo() << "Loaded" << m_envOverrides.size() << "path(s) from VICINAE_OVERRIDES";

    for (const auto &override : m_envOverrides) {
      qInfo() << override;
    }
  }

  initConfig();
  reloadConfig();

  connect(&m_watcher, &QFileSystemWatcher::fileChanged, this, [this](const QString &path) {
    m_fsDebounce.start();
    m_watcher.addPath(QString::fromStdString(m_userPath.string()));
  });
  connect(&m_fsDebounce, &QTimer::timeout, this, [this]() { reloadConfig(); });
}

ConfigValue Manager::defaultConfig() const { return m_defaultConfig; }
const char *Manager::defaultConfigData() const { return m_defaultData.c_str(); }

void Manager::print(const ConfigValue &value) {
  std::string buf;
  [[maybe_unused]] auto res = glz::write_json(value, buf);
  std::cout << glz::prettify_json(buf) << std::endl;
}

bool Manager::mergeProviderWithUser(std::string_view id, const Partial<ProviderData> &data) {
  return mergeWithUser({.providers = std::map<std::string, Partial<ProviderData>>{{std::string{id}, data}}});
}

bool Manager::updateUser(const std::function<void(Partial<ConfigValue> &value)> &updater) {
  std::string buf;
  Partial<ConfigValue> user;

  if (auto error =
          glz::read_file_jsonc<glz::opts{.error_on_unknown_keys = false}>(user, m_userPath.string(), buf)) {
    qWarning() << "Failed to read user config as partial";
    return false;
  }

  updater(user);
  return writeUser(user);
}

bool Manager::mergeEntrypointWithUser(const EntrypointId &id, const ProviderItemData &data) {
  std::map<std::string, Partial<ProviderData>> providers;
  providers[id.provider] =
      Partial<ProviderData>{.entrypoints = std::map<std::string, ProviderItemData>{{id.entrypoint, data}}};
  return mergeWithUser({.providers = providers});
}

bool Manager::mergeThemeConfig(const config::Partial<config::SystemThemeConfig> &cfg) {
  switch (QGuiApplication::styleHints()->colorScheme()) {
  case Qt::ColorScheme::Light:
    return mergeWithUser({.theme = config::Partial<config::ThemeConfig>{.light = cfg}});
  default:
    return mergeWithUser({.theme = config::Partial<config::ThemeConfig>{.dark = cfg}});
  }
}

bool Manager::mergeWithUser(const Partial<ConfigValue> &patch) {
  std::string buf;
  Partial<ConfigValue> user;

  if (auto error =
          glz::read_file_jsonc<glz::opts{.error_on_unknown_keys = false}>(user, m_userPath.string(), buf)) {
    qWarning() << "Failed to read user config as partial, config changes haven't been applied.";
    return false;
  }

  if (auto error = glz::write_json(glz::merge{user, patch}, buf)) {
    qWarning() << "Failed to merge partials: config changes haven't been applied";
    return false;
  }

  if (auto error = glz::read_json(user, buf)) {
    qWarning() << "Failed to read merged partials: config changes haven't been applied";
    return false;
  }

  prunePartial(user);

  return writeUser(user);
}

bool Manager::writeUser(const Partial<ConfigValue> &cfg) {
  std::string buf;

  if (auto error = glz::write_json(cfg, buf)) {
    qWarning() << "Failed to write json" << glz::format_error(error);
    return false;
  }

  {
    std::ofstream ofs(m_userPath);
    ofs << TOP_COMMENT << "\n\n" << glz::prettify_json(buf);
  }

  reloadConfig();

  return true;
}

Manager::ConfigResult Manager::loadUser(const LoadingOptions &opts) {
  return load(m_userPath, opts).transform([&](auto &&res) {
    return merge<ConfigValue>(defaultConfig(), res);
  });
}

void Manager::reloadConfig() {
  std::error_code ec;
  if (!std::filesystem::is_regular_file(m_userPath, ec)) return;

  std::unordered_set<std::filesystem::path> visited;
  auto res = loadUser({.resolveImports = true, .visited = visited});

  if (!res) {
    emit configLoadingError(res.error());
    return;
  }

  ConfigValue const prev = std::move(m_user);
  m_user = std::move(res.value());

  std::string prevJson;
  std::string nextJson;
  bool const comparable = !glz::write_json(prev, prevJson) && !glz::write_json(m_user, nextJson);
  if (comparable && prevJson == nextJson) return;

  emit configChanged(m_user, prev);
}

void Manager::initConfig() {
  std::error_code ec;

  if (!fs::is_regular_file(m_userPath, ec)) {
    fs::create_directories(m_userPath.parent_path());
    writeUser({});
  }
}

std::filesystem::path Manager::resolvePath(const std::filesystem::path &path,
                                           const std::filesystem::path &cwd) {
  std::string importPath = expandPath(path).string();

  if (!importPath.starts_with('/')) { importPath = (cwd.parent_path() / importPath).string(); }

  return std::filesystem::weakly_canonical(importPath);
}

Manager::PartialConfigResult Manager::load(const std::filesystem::path &path, const LoadingOptions &opts) {
  m_watcher.addPath(QString::fromStdString(path.string()));

  std::string buf;
  Partial<ConfigValue> cfg;
  auto glzError = glz::read_file_jsonc<glz::opts{.error_on_unknown_keys = false}>(cfg, path.string(), buf);

  if (glzError) {
    std::string glzErrMsg = glz::format_error(glzError);
    return std::unexpected(std::format("Failed to read JSONC file at {}: {}", path.string(), glzErrMsg));
  }

  auto importFile = [this, &opts](Partial<ConfigValue> &cfg, const std::string &importPath,
                                  bool override) -> std::expected<Partial<ConfigValue>, std::string> {
    if (opts.visited.contains(importPath)) {
      qWarning().nospace() << "Circular import detected for " << importPath << ", ignoring...";
      return cfg;
    }

    opts.visited.insert(importPath);

    if (std::filesystem::exists(importPath)) {
      PartialConfigResult imported = load(importPath, opts);

      if (!imported) {
        return std::unexpected(std::format("Failed to import file \"{}\": {}", importPath, imported.error()));
      }

      if (override) return merge<Partial<ConfigValue>>(cfg, imported);
      return merge<Partial<ConfigValue>>(imported, cfg);
    } else {
      qWarning().nospace() << "Imported config file not found: " << importPath;
      return cfg;
    }
  };

  if (opts.resolveImports) {
    for (const auto &imp : cfg.imports.value_or(std::vector<std::string>{})) {
      auto result = importFile(cfg, resolvePath(imp, path).string(), false);
      if (!result) return result;
      cfg = std::move(result).value();
    }

    for (const auto &overridePath : m_envOverrides) {
      auto result = importFile(cfg, resolvePath(overridePath, path).string(), true);
      if (!result) return result;
      cfg = std::move(result).value();
    }
  }

  return cfg;
}

void Manager::prunePartial(Partial<ConfigValue> &user) {
  auto prunePreferences = [](glz::generic::object_t &obj) {
    for (auto it = obj.begin(); it != obj.end();) {
      if (it->second.is_null()) {
        it = obj.erase(it);
      } else {
        ++it;
      }
    }
  };

  if (user.providers) {
    auto &pvd = user.providers.value();

    for (auto it = pvd.begin(); it != pvd.end();) {
      auto &v = it->second;
      auto currentIt = it++;

      if (v.preferences) {
        prunePreferences(v.preferences.value());
        if (v.preferences.value().empty()) { v.preferences.reset(); }
      }

      if (v.entrypoints) {
        auto &entrypoints = *v.entrypoints;
        for (auto it2 = entrypoints.begin(); it2 != entrypoints.end();) {
          auto currentIt = it2++;
          ProviderItemData &vi = currentIt->second;

          if (vi.preferences) {
            prunePreferences(vi.preferences.value());
            if (vi.preferences->empty()) { vi.preferences.reset(); }
          }

          if (vi.alias && vi.alias->empty()) { vi.alias.reset(); }
          if (vi.shortcut && vi.shortcut->empty()) { vi.shortcut.reset(); }
          if (!vi.enabled.has_value() && vi.preferences.value_or(glz::generic::object_t{}).empty() &&
              !vi.alias && !vi.shortcut) {
            entrypoints.erase(currentIt);
          }
        }

        if (entrypoints.empty()) { v.entrypoints.reset(); }
      }

      if (!v.enabled && v.preferences.value_or(glz::generic::object_t{}).empty() && !v.entrypoints) {
        pvd.erase(currentIt);
      }
    }

    if (pvd.empty()) { user.providers.reset(); }
  }
}

}; // namespace config
