#include <QClipboard>
#include "clipboard-service.hpp"
#include <filesystem>
#include <numeric>
#include <QGuiApplication>
#include "services/app-service/abstract-app-db.hpp"
#include "x11/x11-clipboard-server.hpp"
#include <qclipboard.h>
#include <qimagereader.h>
#include <qlogging.h>
#include <qmimedata.h>
#include <qnamespace.h>
#include <qstringview.h>
#include <qt6keychain/keychain.h>
#include <QtConcurrent/QtConcurrent>
#include <QFutureWatcher>
#include <QBuffer>
#include <QImage>
#include "clipboard-server-factory.hpp"
#include <quuid.h>
#include "services/app-service/app-service.hpp"
#include "services/clipboard/clipboard-db.hpp"
#include "services/clipboard/clipboard-encrypter.hpp"
#include "services/clipboard/clipboard-server.hpp"
#include "utils.hpp"
#ifdef Q_OS_LINUX
#include "services/clipboard/gnome/gnome-clipboard-server.hpp"
#include "data-control/data-control-clipboard-server.hpp"
#endif

namespace fs = std::filesystem;

/**
 * If any of these is found in a selection, we ignore the entire selection.
 */
static const std::set<QString> IGNORED_MIME_TYPES = {
    Clipboard::CONCEALED_MIME_TYPE,
};

static const std::set<QString> PASSWORD_MIME_TYPES = {
    "x-kde-passwordManagerHint",
};

bool ClipboardService::setPinned(const QString &id, bool pinned) {
  if (!ClipboardDatabase().setPinned(id, pinned)) { return false; }

  emit selectionPinStatusChanged(id, pinned);

  return true;
}

bool ClipboardService::clear() {
  QGuiApplication::clipboard()->clear();
  return true;
}

bool ClipboardService::supportsMonitoring() const { return m_clipboardServer->id() != "dummy"; }

bool ClipboardService::copyContent(const Clipboard::Content &content, const Clipboard::CopyOptions options) {
  struct ContentVisitor {
    ClipboardService &service;
    const Clipboard::CopyOptions &options;

    bool operator()(const Clipboard::NoData &dummy) const {
      qWarning() << "attempt to copy NoData content";
      return false;
    }
    bool operator()(const Clipboard::Html &html) const { return service.copyHtml(html, options); }
    bool operator()(const Clipboard::File &file) const { return service.copyFile(file.path, options); }
    bool operator()(const Clipboard::Text &text) const { return service.copyText(text.text, options); }
    bool operator()(const ClipboardSelection &selection) const {
      return service.copySelection(selection, options);
    }
    bool operator()(const Clipboard::SelectionRecordHandle &handle) const {
      return service.copySelectionRecord(handle.id, options);
    }

    ContentVisitor(ClipboardService &service, const Clipboard::CopyOptions &options)
        : service(service), options(options) {}
  };

  ContentVisitor visitor(*this, options);

  return std::visit(visitor, content);
}

bool ClipboardService::copyFile(const std::filesystem::path &path, const Clipboard::CopyOptions &options) {
  QMimeData *data = new QMimeData;

  // copying files should normally copy a link to the file, not the file itself
  // This is what text/uri-list is used for. On Windows or other systems we might have
  // to do something else, I'm not sure.
  data->setData("text/uri-list", QString("file://%1").arg(path.c_str()).toUtf8());

  return copyQMimeData(data, options);
}

void ClipboardService::setRecordAllOffers(bool value) { m_recordAllOffers = value; }

void ClipboardService::setEncryption(bool value) {
  m_encrypter.reset();

  if (value) {
    m_encrypter = std::make_unique<ClipboardEncrypter>();
    m_encrypter->loadKey();
  }
}

bool ClipboardService::isEncryptionReady() const { return m_encrypter.get(); }

void ClipboardService::setIgnorePasswords(bool value) { m_ignorePasswords = value; }

void ClipboardService::setMonitoring(bool value) {
  if (m_monitoring == value) return;

  if (value) {
    qInfo() << "Starting clipboard server" << m_clipboardServer->id();
    if (m_clipboardServer->start()) {
      qInfo() << "Clipboard server" << m_clipboardServer->id() << "started successfully.";
    } else {
      qWarning() << "Failed to start clipboard server" << m_clipboardServer->id();
    }
  } else {
    qInfo() << "Stopping clipboard server" << m_clipboardServer->id();
    if (m_clipboardServer->stop()) {
      qInfo() << "Clipboard server" << m_clipboardServer->id() << "stopped successfully.";
    } else {
      qWarning() << "Failed to stop clipboard server" << m_clipboardServer->id();
    }
  }

  m_monitoring = value;
  emit monitoringChanged(value);
}

bool ClipboardService::monitoring() const { return m_monitoring; }

bool ClipboardService::copyHtml(const Clipboard::Html &data, const Clipboard::CopyOptions &options) {
  auto mimeData = new QMimeData;

  mimeData->setData("text/html", data.html.toUtf8());

  if (auto text = data.text) mimeData->setData("text/plain", text->toUtf8());

  return copyQMimeData(mimeData, options);
}

bool ClipboardService::copyText(const QString &text, const Clipboard::CopyOptions &options) {
  auto mimeData = new QMimeData;

  mimeData->setData("text/plain", text.toUtf8());
  mimeData->setData("text/plain;charset=utf-8", text.toUtf8());

  if (options.concealed) mimeData->setData(Clipboard::CONCEALED_MIME_TYPE, "1");

  return copyQMimeData(mimeData, options);
}

void ClipboardService::scheduleClipboardRestore(int delayMs) {
  if (!m_lastSelection || m_lastSelection->offers.empty()) return;

  m_restoreTimer.stop();
  m_restoreTimer.setSingleShot(true);
  m_restoreTimer.setInterval(delayMs);
  m_restoreTimer.disconnect();
  connect(&m_restoreTimer, &QTimer::timeout, this, &ClipboardService::restoreClipboard);
  m_restoreTimer.start();
}

QFuture<PaginatedResponse<ClipboardHistoryEntry>>
ClipboardService::listAll(int limit, int offset, const ClipboardListSettings &opts) const {
  return QtConcurrent::run(
      [opts, limit, offset]() { return ClipboardDatabase().query(limit, offset, opts); });
}

ClipboardOfferKind ClipboardService::getKind(const ClipboardDataOffer &offer) {
  if (offer.mimeType == "text/uri-list") {
    QString const text = offer.data;
    auto uris = text.split("\r\n", Qt::SkipEmptyParts);
    if (uris.size() == 1 && QUrl(uris.front()).isLocalFile()) return ClipboardOfferKind::File;
    return ClipboardOfferKind::Text;
  }

  if (offer.mimeType.startsWith("image/")) { return ClipboardOfferKind::Image; }
  if (offer.mimeType == "text/html") { return ClipboardOfferKind::Text; }

  if (Utils::isTextMimeType(offer.mimeType)) {
    auto url = QUrl::fromEncoded(offer.data, QUrl::StrictMode);

    if (url.isLocalFile()) { return ClipboardOfferKind::File; }
    if (!url.scheme().isEmpty()) { return ClipboardOfferKind::Link; }

    return ClipboardOfferKind::Text;
  }

  return ClipboardOfferKind::Unknown;
}

QString ClipboardService::getSelectionPreferredMimeType(const ClipboardSelection &selection) {
  static const std::vector<QString> plainTextMimeTypes = {
      "text/uri-list", "text/plain;charset=utf-8", "text/plain", "UTF8_STRING", "STRING", "TEXT",
      "COMPOUND_TEXT"};

  for (const auto &mime : plainTextMimeTypes) {
    auto it = std::ranges::find_if(
        selection.offers, [&](const auto &offer) { return offer.mimeType == mime && !offer.data.isEmpty(); });
    if (it != selection.offers.end()) return it->mimeType;
  }

  auto imageIt = std::ranges::find_if(selection.offers, [](const auto &offer) {
    return offer.mimeType.startsWith("image/") && !offer.data.isEmpty();
  });
  if (imageIt != selection.offers.end()) return imageIt->mimeType;

  auto htmlIt = std::ranges::find_if(selection.offers, [](const auto &offer) {
    return offer.mimeType == "text/html" && !offer.data.isEmpty();
  });
  if (htmlIt != selection.offers.end()) return htmlIt->mimeType;

  auto fallbackIt = std::ranges::find_if(selection.offers, [](const auto &offer) {
    return !offer.mimeType.startsWith("text/_moz_html") && !offer.data.isEmpty();
  });
  if (fallbackIt != selection.offers.end()) return fallbackIt->mimeType;

  if (!selection.offers.empty()) return selection.offers.front().mimeType;

  return {};
}

bool ClipboardService::removeSelection(const QString &selectionId) {
  ClipboardDatabase cdb;

  for (const auto &offer : cdb.removeSelection(selectionId)) {
    fs::remove(m_dataDir / offer.toStdString());
  }

  emit selectionRemoved(selectionId);

  return true;
}

std::expected<QByteArray, ClipboardService::OfferDecryptionError>
ClipboardService::decryptOffer(const QByteArray &data, ClipboardEncryptionType type) const {
  switch (type) {
  case ClipboardEncryptionType::Local: {
    if (!m_encrypter) { return std::unexpected(OfferDecryptionError::DecryptionRequired); }
    auto decryption = m_encrypter->decrypt(data);
    if (!decryption) { return std::unexpected(OfferDecryptionError::DecryptionFailed); }
    return decryption.value();
  }
  default:
    return data;
  }
}

std::expected<QByteArray, ClipboardService::OfferDecryptionError>
ClipboardService::getMainOfferData(const QString &selectionId) const {
  ClipboardDatabase cdb;

  auto offer = cdb.findPreferredOffer(selectionId);

  if (!offer) {
    qWarning() << "Can't find preferred offer for selection" << selectionId;
    return {};
  };

  fs::path const path = m_dataDir / offer->id.toStdString();

  QFile file(path);

  if (!file.open(QIODevice::ReadOnly)) {
    qWarning() << "Failed to open file at" << path;
    return {};
  }

  return decryptOffer(file.readAll(), offer->encryption);
}

QByteArray ClipboardService::computeSelectionHash(const ClipboardSelection &selection) const {
  QCryptographicHash hash(QCryptographicHash::Md5);

  for (const auto &offer : selection.offers) {
    hash.addData(QCryptographicHash::hash(offer.data, QCryptographicHash::Md5));
  }

  return hash.result();
}

bool ClipboardService::isClearSelection(const ClipboardSelection &selection) const {
  return std::accumulate(selection.offers.begin(), selection.offers.end(), 0,
                         [](size_t acc, auto &&item) { return acc + item.data.size(); }) == 0;
}

QString ClipboardService::getOfferTextPreview(const ClipboardDataOffer &offer) {
  switch (getKind(offer)) {
  case ClipboardOfferKind::Text:
  case ClipboardOfferKind::Link:
  case ClipboardOfferKind::File:
    return offer.data.simplified().mid(0, 50);
  case ClipboardOfferKind::Image: {
    QBuffer buffer;
    QImageReader const reader(&buffer);

    buffer.setData(offer.data);
    if (auto size = reader.size(); size.isValid()) {
      return QString("Image (%1x%2)").arg(size.width()).arg(size.height());
    }
    return "Image";
  }
  default:
    return "Unknown";
  }
}

std::optional<QString> ClipboardService::retrieveKeywords(const QString &id) {
  return ClipboardDatabase().retrieveKeywords(id);
}

bool ClipboardService::setKeywords(const QString &id, const QString &keywords) {
  return ClipboardDatabase().setKeywords(id, keywords);
}

bool ClipboardService::isConcealedSelection(const ClipboardSelection &selection) {
  return std::ranges::any_of(selection.offers,
                             [](auto &&offer) { return IGNORED_MIME_TYPES.contains(offer.mimeType); });
}

bool ClipboardService::isPasswordSelection(const ClipboardSelection &selection) {
  return std::ranges::any_of(selection.offers,
                             [](auto &&offer) { return PASSWORD_MIME_TYPES.contains(offer.mimeType); });
}

ClipboardSelection &ClipboardService::sanitizeSelection(ClipboardSelection &selection) {
  std::ranges::sort(selection.offers, [](auto &&a, auto &&b) {
    return std::ranges::lexicographical_compare(a.mimeType, b.mimeType);
  });
  const auto [first, last] =
      std::ranges::unique(selection.offers, [](auto &&a, auto &&b) { return a.mimeType == b.mimeType; });

  selection.offers.erase(first, last);

  return selection;
}

void ClipboardService::saveSelection(ClipboardSelection selection) {
  sanitizeSelection(selection);

  m_lastSelection = selection;

  if (!m_monitoring) return;

  qInfo() << "Received new clipboard selection with" << selection.offers.size() << "offers";

  for (const auto &offer : selection.offers) {
    qInfo().nospace() << offer.mimeType << " (size=" << formatSize(offer.data.size())
                      << ", password=" << PASSWORD_MIME_TYPES.contains(offer.mimeType) << ")";
  }

  if (isConcealedSelection(selection)) {
    qInfo() << "Ignoring concealed selection";
    return;
  }

  if (m_ignorePasswords && isPasswordSelection(selection)) {
    qInfo() << "Ignored password clipboard selection";
    return;
  }

  if (isClearSelection(selection)) {
    qInfo() << "Ignored clipboard clear selection";
    return;
  }

  QString preferredMimeType = getSelectionPreferredMimeType(selection);
  ClipboardHistoryEntry insertedEntry;
  ClipboardDatabase cdb;
  auto preferredOfferIt =
      std::ranges::find_if(selection.offers, [&](auto &&o) { return o.mimeType == preferredMimeType; });

  if (preferredOfferIt == selection.offers.end()) {
    qCritical() << "preferredOfferIt is invalid, this should not be possible!";
    return;
  }

  auto preferredKind = getKind(*preferredOfferIt);

  if (preferredKind == ClipboardOfferKind::Unknown) {
    qWarning() << "Ignoring selection with primary offer of unknown kind" << preferredMimeType;
    return;
  }

  auto selectionHash = QCryptographicHash::hash(preferredOfferIt->data, QCryptographicHash::Md5).toHex();

  if (preferredKind == ClipboardOfferKind::Text && preferredOfferIt->data.trimmed().isEmpty()) {
    qInfo() << "Ignored text selection with empty text";
    return;
  }

  cdb.transaction([&](ClipboardDatabase *db) {
    if (db->tryBubbleUpSelection(selectionHash)) {
      qInfo() << "A similar clipboard selection is already indexed: moving it on top of the history";
      return true;
    }

    QString const selectionId = QUuid::createUuid().toString(QUuid::WithoutBraces);

    if (!db->insertSelection({.id = selectionId,
                              .offerCount = static_cast<int>(selection.offers.size()),
                              .hash = selectionHash,
                              .preferredMimeType = preferredMimeType,
                              .kind = preferredKind,
                              .source = selection.sourceApp})) {
      qWarning() << "failed to insert selection";
      return false;
    }

    // Index all offers, including empty ones
    for (const auto &offer : selection.offers) {
      ClipboardOfferKind const kind = getKind(offer);
      bool const isIndexableText = kind == ClipboardOfferKind::Text || kind == ClipboardOfferKind::Link;
      QString const textPreview = getOfferTextPreview(offer);

      if (isIndexableText && !offer.data.isEmpty()) {
        if (!db->indexSelectionContent(selectionId, offer.data)) {
          qWarning() << "Failed to index selection content for offer" << offer.mimeType;
          return false;
        }
      }

      auto md5sum = QCryptographicHash::hash(offer.data, QCryptographicHash::Md5).toHex();
      auto offerId = QUuid::createUuid().toString(QUuid::WithoutBraces);
      ClipboardEncryptionType encryption = ClipboardEncryptionType::None;

      if (m_encrypter) encryption = ClipboardEncryptionType::Local;

      InsertClipboardOfferPayload dto{
          .id = offerId,
          .selectionId = selectionId,
          .mimeType = offer.mimeType,
          .textPreview = textPreview,
          .md5sum = md5sum,
          .encryption = encryption,
          .size = static_cast<quint64>(offer.data.size()),
      };

      if (kind == ClipboardOfferKind::Link) {
        auto url = QUrl::fromEncoded(offer.data, QUrl::StrictMode);
        if (url.scheme().startsWith("http")) { dto.urlHost = url.host(); }
      }

      if (!db->insertOffer(dto)) {
        qWarning() << "Failed to insert offer" << offer.mimeType;
        return false;
      }

      fs::path const targetPath = m_dataDir / offerId.toStdString();
      QFile targetFile(targetPath);

      if (!targetFile.open(QIODevice::WriteOnly)) { continue; }

      if (m_encrypter) {
        if (auto encrypted = m_encrypter->encrypt(offer.data)) {
          targetFile.write(encrypted.value());
        } else {
          qWarning() << "Failed to encrypt clipboard selection";
          return false;
        }
      } else {
        targetFile.write(offer.data);
      }

      // Set the insertedEntry for the preferred offer
      if (offer.mimeType == preferredMimeType) {
        insertedEntry.id = selectionId;
        insertedEntry.pinnedAt = 0;
        insertedEntry.updatedAt = {};
        insertedEntry.mimeType = offer.mimeType;
        insertedEntry.md5sum = md5sum;
        insertedEntry.textPreview = textPreview;
      }
    }

    return true;
  });

  emit itemInserted(insertedEntry);
}

std::optional<ClipboardSelection> ClipboardService::retrieveSelectionById(const QString &id) {
  ClipboardDatabase cdb;
  ClipboardSelection populatedSelection;
  const auto selection = cdb.findSelection(id);

  if (!selection) return std::nullopt;

  for (const auto &offer : selection->offers) {
    ClipboardDataOffer populatedOffer;
    fs::path const path = m_dataDir / offer.id.toStdString();
    QFile file(path);

    if (!file.open(QIODevice::ReadOnly)) { continue; }

    auto data = decryptOffer(file.readAll(), offer.encryption);

    if (!data) return {};

    populatedOffer.data = data.value();
    populatedOffer.mimeType = offer.mimeType;
    populatedSelection.offers.emplace_back(populatedOffer);
  }

  return populatedSelection;
}

bool ClipboardService::copyQMimeData(QMimeData *data, const Clipboard::CopyOptions &options) {
  if (options.concealed) { data->setData(Clipboard::CONCEALED_MIME_TYPE, "1"); }

  return m_clipboardServer->setClipboardContent(data);
}

void ClipboardService::restoreClipboard() {
  if (!m_lastSelection || m_lastSelection->offers.empty()) return;

  auto *data = new QMimeData;
  for (const auto &offer : m_lastSelection->offers) {
    data->setData(offer.mimeType, offer.data);
  }

  data->setData(Clipboard::CONCEALED_MIME_TYPE, {});
  m_clipboardServer->setClipboardContent(data);
  m_lastSelection.reset();
}

bool ClipboardService::copySelection(const ClipboardSelection &selection,
                                     const Clipboard::CopyOptions &options) {
  if (selection.offers.empty()) {
    qWarning() << "Not copying selection with no offers";
    return false;
  }

  QMimeData *mimeData = new QMimeData;

  for (const auto &offer : selection.offers) {
    if (offer.mimeType == "application/x-qt-image") continue; // we handle that ourselves
    if (offer.mimeType.startsWith("image/") && !mimeData->hasImage()) {
      auto img = QImage::fromData(offer.data);

      if (img.isNull()) {
        qWarning() << offer.mimeType << "could not be converted to valid image format";
        mimeData->setData(offer.mimeType, offer.data);
      } else {
        mimeData->setData(offer.mimeType, offer.data);
        mimeData->setImageData(img);
        qDebug() << "ClipboardService: Set image data with mime type" << offer.mimeType
                 << "size:" << offer.data.size();
      }
    } else {
      if (Utils::isTextMimeType(offer.mimeType)) {
        mimeData->setText(QString::fromUtf8(offer.data));
      } else {
        mimeData->setData(offer.mimeType, offer.data);
      }
    }
  }

  return copyQMimeData(mimeData, options);
}

bool ClipboardService::copySelectionRecord(const QString &id, const Clipboard::CopyOptions &options) {
  auto selection = retrieveSelectionById(id);

  if (!selection) {
    qWarning() << "copySelectionRecord: could not get selection for ID" << id;
    return false;
  }

  ClipboardDatabase db;

  if (!db.tryBubbleUpSelection(id)) {
    qWarning() << "Failed to bubble up selection with id" << id;
    return false;
  }

  // we don't want subscribers to block before the actual copy happens
  QMetaObject::invokeMethod(this, [this]() { emit selectionUpdated(); }, Qt::QueuedConnection);

  return copySelection(*selection, options);
}

QString ClipboardService::readText() { return QGuiApplication::clipboard()->text(); }

Clipboard::ReadContent ClipboardService::readContent() {
  Clipboard::ReadContent content;
  const QMimeData *mimeData = QGuiApplication::clipboard()->mimeData();

  if (!mimeData) return content;

  if (mimeData->hasUrls()) {
    for (const auto &url : mimeData->urls()) {
      if (url.isLocalFile()) {
        content.file = url.toLocalFile();
        break;
      }
    }
  }

  if (mimeData->hasHtml()) { content.html = mimeData->html(); }
  if (mimeData->hasText()) { content.text = mimeData->text(); }

  return content;
}

bool ClipboardService::removeAllSelections() {
  ClipboardDatabase db;

  if (!db.removeAll()) {
    qWarning() << "Failed to remove all clipboard selections";
    return false;
  }

  fs::remove_all(m_dataDir);
  fs::create_directories(m_dataDir);

  emit allSelectionsRemoved();

  return true;
}

AbstractClipboardServer *ClipboardService::clipboardServer() const { return m_clipboardServer.get(); }

ClipboardService::ClipboardService(const std::filesystem::path &path) {
  m_dataDir = path.parent_path() / "clipboard-data";

  {
    ClipboardServerFactory factory;

#ifdef Q_OS_LINUX
    factory.registerServer<GnomeClipboardServer>();
    factory.registerServer<DataControlClipboardServer>();
    factory.registerServer<X11ClipboardServer>();
#endif
    m_clipboardServer = factory.createFirstActivatable();
    qInfo() << "Activated clipboard server" << m_clipboardServer->id();
  }

  fs::create_directories(m_dataDir);
  ClipboardDatabase().runMigrations();

  connect(m_clipboardServer.get(), &AbstractClipboardServer::selectionAdded, this,
          &ClipboardService::saveSelection);
}
