#include "favicon/favicon-service.hpp"
#include "favicon/cached-favicon-request.hpp"
#include "favicon/dummy-favicon-request.hpp"
#include "favicon/google-favicon-request.hpp"
#include "favicon/twenty-favicon-request.hpp"
#include <qlogging.h>

static const std::vector<FaviconService::FaviconServiceData> faviconProviders = {
    {.id = "google",
     .name = "Google",
     .icon = ImageURL::builtin(BuiltinIcon::Google),
     .type = FaviconService::Google},
    {.id = "twenty",
     .name = "Twenty",
     .icon = ImageURL::builtin(BuiltinIcon::Twenty),
     .type = FaviconService::Twenty},
    {.id = "none",
     .name = "None",
     .icon = ImageURL::builtin(BuiltinIcon::Image),
     .type = FaviconService::None}};

std::vector<FaviconService::FaviconServiceData> FaviconService::providers() { return faviconProviders; }

FaviconService *FaviconService::instance() {
  assert(_instance && "FaviconService::instance() called before FaviconService::initialize()");
  return _instance;
}

void FaviconService::insertCache(const QString &key, const QPixmap &favicon) {
  if (_cache.size() > maxCacheCount) { _cache.erase(_cache.begin()); }

  _cache.insert({key, favicon});
}

void FaviconService::handleFetchedFavicon(const QString &domain, const QPixmap &favicon) {
  insertCache(domain, favicon);

  if (!favicon.save(_dataDir.filePath(domain), "PNG")) {
    qDebug() << "Failed to save favicon on disk";
    return;
  }

  auto stmt = _db.prepare(R"(
    INSERT INTO favicon (id, created_at, size)
    VALUES (:id, :epoch, :size)
  )");
  stmt.bind(":id", domain);
  stmt.bind(":epoch", static_cast<int64_t>(QDateTime::currentSecsSinceEpoch()));
  stmt.bind(":size", favicon.width());

  if (!stmt.exec()) {
    qDebug() << "Favicon DB: failed to insert favicon:" << stmt.lastError().c_str();
    return;
  }
}

QPixmap FaviconService::retrieveFromCache(const QString &domain) {
  if (auto it = _cache.find(domain); it != _cache.end()) { return it->second; }

  QPixmap pm;

  auto stmt = _db.prepare("UPDATE favicon SET last_used_at = :epoch WHERE id = :domain;");
  stmt.bind(":domain", domain);
  stmt.bind(":epoch", static_cast<int64_t>(QDateTime::currentSecsSinceEpoch()));

  if (!stmt.exec()) { qDebug() << "Favicon DB: failed to update last_used_at" << stmt.lastError().c_str(); }

  QFile favicon(_dataDir.filePath(domain));

  if (favicon.open(QIODevice::ReadOnly)) {
    pm.loadFromData(favicon.readAll());
    insertCache(domain, pm);
  }

  return pm;
}

void FaviconService::setService(const QString &id) {
  if (auto it = std::ranges::find_if(faviconProviders, [&](auto &&item) { return item.id == id; });
      it != faviconProviders.end()) {
    setService(it->type);
    return;
  }

  qCritical() << "no favicon provider for id" << id;
}

void FaviconService::setService(RequesterType type) { _requesterType = type; }

QFuture<FaviconService::FaviconResponse> FaviconService::makeRequest(const QString &domain, QObject *parent) {
  QPromise<FaviconResponse> promise;
  auto future = promise.future();

  if (auto pix = retrieveFromCache(domain); !pix.isNull()) {
    promise.addResult(pix);
    promise.finish();

    return future;
  }

  AbstractFaviconRequest *requester = nullptr;

  switch (_requesterType) {
  case Google:
    requester = new GoogleFaviconRequester(domain, parent);
    break;
  case Twenty:
    requester = new TwentyFaviconRequester(domain, parent);
    break;
  case None:
    promise.addResult(std::unexpected("Favicon fetching is disabled"));
    promise.finish();
    return future;
  default:
    requester = new DummyFaviconRequest(domain, parent);
  }

  qDebug() << "request start !";

  connect(requester, &AbstractFaviconRequest::finished, this,
          [this, requester, domain, promise = std::move(promise)](const QPixmap &favicon) mutable {
            handleFetchedFavicon(domain, favicon);
            promise.addResult(favicon);
            promise.finish();
            requester->deleteLater();
          });

  requester->start();

  return future;
}

FaviconService::FaviconService(const std::filesystem::path &path, QObject *parent)
    : QObject(parent), _requesterType(RequesterType::Google) {
  _dataDir = QFileInfo(path).dir().filePath("favicon-data");
  _dataDir.mkpath(_dataDir.path());

  auto result = db::Database::open(path);

  if (!result) {
    qDebug() << "Failed to open favicon DB" << result.error().c_str();
    return;
  }

  _db = std::move(*result);

  if (!_db.exec(R"(
    CREATE TABLE IF NOT EXISTS favicon (
      id TEXT PRIMARY KEY,
      size INTEGER,
      created_at INTEGER NOT NULL,
      last_used_at INTEGER,
      updated_at INTEGER
    );
  )")) {
    qDebug() << "Failed to init favicon database:" << _db.lastError().c_str();
  }
}
