#include "file-indexer.hpp"
#include <cstdint>
#include <cstring>
#include <QDateTime>
#include <QJsonArray>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QTimer>
#include <common/file-category.hpp>
#include <filesystem>
#include <utility>
#include "common/common.hpp"
#include "log/message-handler.hpp"

void FileIndexerBus::send(std::string_view data) {
  uint32_t size = data.size();
  m_device->write(reinterpret_cast<const char *>(&size), sizeof(size));
  m_device->write(data.data(), data.size());
}

void FileIndexerBus::readyRead() {
  while (m_device->bytesAvailable() > 0) {
    auto read = m_device->readAll();
    m_message.data.append(read);

    while (std::cmp_greater_equal(m_message.data.size(), sizeof(uint32_t))) {
      uint32_t length = 0;
      std::memcpy(&length, m_message.data.constData(), sizeof(length));
      bool const isComplete = m_message.data.size() - sizeof(uint32_t) >= length;

      if (!isComplete) break;

      auto packet = m_message.data.sliced(sizeof(uint32_t), length);
      emit messageReceived(packet);
      m_message.data = m_message.data.sliced(sizeof(uint32_t) + length);
    }
  }
}

FileIndexerBus::FileIndexerBus(QIODevice *device) : m_device(device) {
  connect(device, &QIODevice::readyRead, this, &FileIndexerBus::readyRead);
}

static AbstractFileIndexer::ScanKind toScanKind(file_indexer_gen::ScanKind kind) {
  switch (kind) {
  case file_indexer_gen::ScanKind::Full:
    return AbstractFileIndexer::ScanKind::Full;
  case file_indexer_gen::ScanKind::Incremental:
    return AbstractFileIndexer::ScanKind::Incremental;
  }
  return AbstractFileIndexer::ScanKind::Full;
}

static AbstractFileIndexer::ScanState toScanState(file_indexer_gen::ScanState state) {
  switch (state) {
  case file_indexer_gen::ScanState::Started:
    return AbstractFileIndexer::ScanState::Started;
  case file_indexer_gen::ScanState::Succeeded:
    return AbstractFileIndexer::ScanState::Succeeded;
  case file_indexer_gen::ScanState::Failed:
    return AbstractFileIndexer::ScanState::Failed;
  case file_indexer_gen::ScanState::Interrupted:
    return AbstractFileIndexer::ScanState::Interrupted;
  }
  return AbstractFileIndexer::ScanState::Failed;
}

static file_indexer_gen::FileCategory toFileCategory(vicinae::FileCategory category) {
  switch (category) {
  case vicinae::FileCategory::Other:
    return file_indexer_gen::FileCategory::Other;
  case vicinae::FileCategory::Directory:
    return file_indexer_gen::FileCategory::Directory;
  case vicinae::FileCategory::Image:
    return file_indexer_gen::FileCategory::Image;
  case vicinae::FileCategory::Video:
    return file_indexer_gen::FileCategory::Video;
  case vicinae::FileCategory::Audio:
    return file_indexer_gen::FileCategory::Audio;
  case vicinae::FileCategory::Document:
    return file_indexer_gen::FileCategory::Document;
  case vicinae::FileCategory::Archive:
    return file_indexer_gen::FileCategory::Archive;
  case vicinae::FileCategory::Application:
    return file_indexer_gen::FileCategory::Application;
  }
  return file_indexer_gen::FileCategory::Other;
}

static vicinae::FileCategory toFileCategory(file_indexer_gen::FileCategory category) {
  switch (category) {
  case file_indexer_gen::FileCategory::Other:
    return vicinae::FileCategory::Other;
  case file_indexer_gen::FileCategory::Directory:
    return vicinae::FileCategory::Directory;
  case file_indexer_gen::FileCategory::Image:
    return vicinae::FileCategory::Image;
  case file_indexer_gen::FileCategory::Video:
    return vicinae::FileCategory::Video;
  case file_indexer_gen::FileCategory::Audio:
    return vicinae::FileCategory::Audio;
  case file_indexer_gen::FileCategory::Document:
    return vicinae::FileCategory::Document;
  case file_indexer_gen::FileCategory::Archive:
    return vicinae::FileCategory::Archive;
  case file_indexer_gen::FileCategory::Application:
    return vicinae::FileCategory::Application;
  }
  return vicinae::FileCategory::Other;
}

FileIndexer::FileIndexer() : m_bus(&m_process), m_rpc(m_bus), m_client(m_rpc) {
  connect(&m_process, &QProcess::readyReadStandardError, this, &FileIndexer::handleStderr);
  connect(&m_process, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) {
    qCritical() << "file indexer process error occured" << m_process.errorString();
  });

  connect(&m_process, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus status) {
    interruptActiveScans();

    if (status == QProcess::CrashExit || exitCode != 0) {
      qWarning() << "file indexer crashed with code" << exitCode;
      handleCrash();
    } else {
      qInfo() << "file indexer exited with code" << exitCode;
    }
  });

  connect(m_client.fileindexer(), &file_indexer_gen::FileIndexerService::scanStatusChanged, this,
          [this](const file_indexer_gen::ScanStatusEvent &event) { handleScanStatus(event); });

  connect(&m_bus, &FileIndexerBus::messageReceived, this, [this](const QByteArray &msg) {
    std::string_view view{msg.constData(), static_cast<size_t>(msg.size())};
    if (auto res = m_client.route(view); !res) {
      qWarning() << "Failed to route file indexer message" << res.error();
    }
  });
}

void FileIndexer::start() {
  if (isRunning()) return;
  m_wantRunning = true;
  startProcess();
}

void FileIndexer::stopProcess() {
  m_wantRunning = false;
  m_crashCount = 0;
  m_process.close();
}

void FileIndexer::startProcess() {
  auto path = vicinae::findHelperProgram("vicinae-file-indexer");

  if (!path) {
    qWarning() << "could not find vicinae-file-indexer helper binary, file search will not work";
    return;
  }

  m_process.setProgram(path->c_str());

#if defined(DEBUG)
  auto env = QProcessEnvironment::systemEnvironment();
  if (!env.contains(QStringLiteral("VICINAE_FILE_INDEXER_LOG"))) {
    env.insert(QStringLiteral("VICINAE_FILE_INDEXER_LOG"), QStringLiteral("debug"));
  }
  m_process.setProcessEnvironment(env);
#endif

  m_process.start();

  if (!m_process.waitForStarted()) {
    qCritical() << "Failed to start file indexer" << m_process.error();
    return;
  }

  m_crashCount = 0;
  sendConfigure();
}

void FileIndexer::handleScanStatus(const file_indexer_gen::ScanStatusEvent &event) {
  qDebug() << "scan status" << event.scan_id << "kind" << static_cast<int>(event.kind) << "state"
           << static_cast<int>(event.state) << "processed" << event.processed_file_count << "entrypoint"
           << event.entrypoint.c_str();
  ScanStatus const status{.scanId = event.scan_id,
                          .kind = toScanKind(event.kind),
                          .state = toScanState(event.state),
                          .entrypoint = std::filesystem::path(event.entrypoint),
                          .processedFileCount = event.processed_file_count};

  if (status.isTerminal()) {
    m_activeScans.erase(status.scanId);
  } else {
    m_activeScans[status.scanId] = status;
  }

  emit scanStatusChanged(status);
}

void FileIndexer::interruptActiveScans() {
  auto scans = std::exchange(m_activeScans, {});

  for (auto &[id, status] : scans) {
    status.state = ScanState::Interrupted;
    emit scanStatusChanged(status);
  }
}

void FileIndexer::sendConfigure() {
  if (!isRunning()) return;
  m_client.fileindexer()->configure(m_config);
}

void FileIndexer::handleCrash() {
  if (!m_wantRunning) return;

  ++m_crashCount;

  if (m_crashCount > MAX_RESTART_ATTEMPTS) {
    qCritical() << "file indexer crashed" << m_crashCount << "times, giving up on restart";
    return;
  }

  const int delay = BASE_RESTART_DELAY_MS * (1 << (m_crashCount - 1));
  qWarning() << "file indexer crashed, restarting in" << delay << "ms"
             << "(attempt" << m_crashCount << "/" << MAX_RESTART_ATTEMPTS << ")";

  QTimer::singleShot(delay, this, [this]() { startProcess(); });
}

void FileIndexer::rebuildIndex() {
  if (!isRunning()) return;
  m_client.fileindexer()->rebuildIndex();
}

bool FileIndexer::isAvailable() const { return isRunning(); }

void FileIndexer::preferenceValuesChanged(const QJsonObject &preferences) {
  auto arrayField = [&](const char *key) {
    std::vector<std::string> out;
    const auto paths = preferences.value(key).toArray();

    out.reserve(paths.size());
    for (const auto &path : paths) {
      if (path.isString()) { out.emplace_back(path.toString().toStdString()); }
    }

    return out;
  };

  m_config.paths = arrayField("indexingPaths");
  m_config.excluded_paths = arrayField("excludedIndexingPaths");
  m_wantRunning = preferences.value("autoIndexing").toBool();

  if (m_wantRunning) {
    if (isRunning()) {
      sendConfigure();
    } else {
      start();
    }
  } else {
    stopProcess();
  }
}

QFuture<std::vector<IndexerFileResult>> FileIndexer::queryAsync(std::string_view view,
                                                                const IndexerQueryParams &params) {
  if (!isRunning()) { return QtFuture::makeReadyValueFuture(std::vector<IndexerFileResult>{}); }

  file_indexer_gen::QueryRequest req{.text = std::string{view}, .limit = params.limit};
  if (params.category) { req.category = toFileCategory(*params.category); }

  return m_client.fileindexer()->query(req).then(
      [](std::expected<file_indexer_gen::QueryResponse, std::string> result) {
        std::vector<IndexerFileResult> results;
        if (!result) { return results; }

        results.reserve(result->matches.size());

        for (const auto &match : result->matches) {
          results.emplace_back(IndexerFileResult{.path = std::filesystem::path(match.path),
                                                 .rank = match.rank,
                                                 .category = toFileCategory(match.category),
                                                 .mimeType = match.mime_type});
        }

        return results;
      });
}

void FileIndexer::handleStderr() {
  m_stderrBuf += m_process.readAllStandardError();

  int idx = 0;
  while ((idx = m_stderrBuf.indexOf('\n')) != -1) {
    QByteArray const lineBytes = m_stderrBuf.left(idx);
    m_stderrBuf = m_stderrBuf.sliced(idx + 1);

    QString const line = QString::fromUtf8(lineBytes);
    int const tab = line.indexOf('\t');

    if (tab == -1) {
      vicinae::log::subprocessLine(vicinae::log::FILE_INDEXER, {}, line);
      continue;
    }

    vicinae::log::subprocessLine(vicinae::log::FILE_INDEXER, line.left(tab), line.sliced(tab + 1));
  }
}
