#include "telemetry-service.hpp"
#include "config/config.hpp"
#include "environment.hpp"
#include "generated/version.h"
#include "utils.hpp"
#include "vicinae.hpp"
#include <QGuiApplication>
#include <QLocale>
#include <QScreen>
#include <QSysInfo>
#include <QUuid>
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <glaze/json/read.hpp>
#include <glaze/json/write.hpp>
#include <qlogging.h>
#include <qstandardpaths.h>
#include <qsysinfo.h>
#include <qtversionchecks.h>
#include <ranges>
#include <system_error>

namespace fs = std::filesystem;

TelemetryService::TelemetryService(config::Manager &config) : m_config(config) {
  using namespace std::chrono_literals;

  auto const stateDir = Omnicast::stateDir();

  m_client.setBaseUrl(Environment::vicinaeApiBaseUrl());
  m_client.setUserAgent(QString("vicinae/%1").arg(VICINAE_GIT_TAG));

  m_statePath = stateDir / "telemetry.json";
  loadState();

  connect(&m_timer, &QTimer::timeout, this, &TelemetryService::trySendSystemInfo);
  m_timer.setInterval(1h);
}

void TelemetryService::setEnabled(bool v) {
  static std::optional<bool> enabled;

  if (enabled.has_value() && enabled.value() == v) return;

  enabled = v;

  if (enabled.value()) {
    qInfo().noquote() << "Anonymous telemetry is enabled. Learn more:" << Omnicast::DOC_TELEMETRY_URL;
    m_timer.start();
    trySendSystemInfo();
  } else {
    m_timer.stop();
  }
}

void TelemetryService::trySendSystemInfo() {
  static constexpr std::uint64_t ONE_DAY_SECS = 86400;

  auto const now = static_cast<std::uint64_t>(QDateTime::currentSecsSinceEpoch());
  bool const shouldSend = !m_state.systemInfoLastSentAt.has_value() ||
                          (now - m_state.systemInfoLastSentAt.value()) >= ONE_DAY_SECS;

  if (shouldSend) { sendSystemInfo(); }
}

std::string TelemetryService::toLower(const std::string &s) {
  std::string lower = s;
  std::ranges::transform(lower, lower.begin(), ::tolower);
  return lower;
}

void TelemetryService::sendSystemInfo() {
  SystemInfoRequest payload;

  payload.userId = m_state.userId;
  payload.architecture = toLower(QSysInfo::currentCpuArchitecture().toStdString());
  payload.buildProvenance = toLower(VICINAE_PROVENANCE);
  payload.vicinaeVersion = toLower(VICINAE_GIT_TAG);
  payload.desktops = Environment::platformDesktopNames() |
                     std::views::transform([](auto &&s) { return toLower(s); }) |
                     std::ranges::to<std::vector>();
  payload.displayProtocol = QGuiApplication::platformName().toStdString();
  payload.locale = QLocale::system().name().toStdString();
  payload.screens =
      QGuiApplication::screens() | std::views::transform([](QScreen *screen) {
        return ScreenInfo{.resolution = {.width = screen->size().width(), .height = screen->size().height()},
                          .scale = screen->devicePixelRatio()};
      }) |
      std::ranges::to<std::vector>();
  payload.operatingSystem = QSysInfo::kernelType().toStdString();
  payload.chassisType = Environment::chassisType();
  payload.kernelVersion = QSysInfo::kernelVersion().toStdString();
  payload.productId = determineProductId();
  payload.productVersion = QSysInfo::productVersion().toStdString();
  payload.qtVersion = QT_VERSION_STR;

  std::string payloadJson;
  [[maybe_unused]] auto writeErr = glz::write_json(payload, payloadJson);
  qInfo().noquote() << "Sending system info:\n" << glz::prettify_json(payloadJson);

  m_client.post<SystemInfoResponse, SystemInfoRequest>("/telemetry/system-info", std::move(payload))
      .then([this](const http::Client::Result<SystemInfoResponse> &res) {
        if (!res) {
          qWarning() << "Failed to post system info" << res.error();
          return;
        }
        m_state.systemInfoLastSentAt = QDateTime::currentSecsSinceEpoch();
        saveState();
      });
}

QFuture<bool> TelemetryService::forget() {
  return m_client.post<ForgetResponse, ForgetRequest>("/telemetry/forget", {m_state.userId})
      .then([](const http::Client::Result<ForgetResponse> &res) { return res.has_value(); });
}

std::string TelemetryService::generateUserId() {
  return QString("user-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces)).toStdString();
}

std::string TelemetryService::determineProductId() const {
  std::error_code ec;

  // omarchy doesn't override /etc/os-release so it is counted as arch
  // it is very useful to have omarchy be its own distinct demographic, especially since
  // a lot of the users come from macOS and expect a replacement for Raycast.
  if (fs::is_directory(Omnicast::dataHome() / "omarchy", ec)) { return "omarchy"; }

  return QSysInfo::productType().toStdString();
}

void TelemetryService::saveState() {
  std::error_code ec;

  fs::create_directories(m_statePath.parent_path(), ec);

  if (auto const error = glz::write_file_json(m_state, m_statePath.string(), m_buf)) {
    qWarning() << "Failed to write telemetry state file at" << m_statePath.string()
               << glz::format_error(error);
  }
}

void TelemetryService::loadState() {
  if (!fs::is_regular_file(m_statePath)) {
    m_state = State{.userId = generateUserId()};
    saveState();
    return;
  }

  if (auto const error = glz::read_file_json(m_state, m_statePath.string(), m_buf)) {
    qWarning() << "Failed to read telemetry state file at" << m_statePath.string()
               << glz::format_error(error);
  }
}
