#include "ipc-command-server.hpp"
#include "argument.hpp"
#include "common.hpp"
#include "common/entrypoint.hpp"
#include "common/enumerate.hpp"
#include "generated/ipc-server.hpp"
#include "ipc-command-handler.hpp"
#include "config/config.hpp"
#include "service-registry.hpp"
#include "services/browser-extension-service.hpp"
#include "services/files-service/abstract-file-indexer.hpp"
#include "services/files-service/file-service.hpp"
#include "qml/dmenu-view-host.hpp"
#include "utils.hpp"
#include "root-search/extensions/extension-root-provider.hpp"
#include <algorithm>
#include <cstdint>
#include <optional>
#include <qfuture.h>
#include <qlogging.h>
#include <ranges>
#include <sstream>
#include <string_view>
#include <utility>
#include "services/app-service/app-service.hpp"
#include "services/window-manager/window-manager.hpp"
#include "navigation-controller.hpp"
#include "generated/version.h"

// IpcTransport

void IpcCommandServer::IpcTransport::send(std::string_view data) {
  if (!conn) {
    qWarning() << "IPC transport send called with null conn!";
    return;
  }
  uint32_t size = data.size();
  conn->write(reinterpret_cast<const char *>(&size), sizeof(size));
  conn->write(data.data(), data.size());
}

void IpcCommandServer::IpcTransport::bindReply(int id) {
  if (conn) pendingReplies[id] = conn;
}

void IpcCommandServer::IpcTransport::activateReply(int id) {
  if (auto it = pendingReplies.find(id); it != pendingReplies.end()) {
    conn = it->second;
    pendingReplies.erase(it);
  }
}

void IpcCommandServer::IpcTransport::forgetConn(QLocalSocket *dead) {
  std::erase_if(pendingReplies, [dead](const auto &kv) { return kv.second == dead; });
}

// IpcService

IpcService::IpcService(ipc_gen::RpcTransport &transport, ApplicationContext &ctx)
    : ipc_gen::AbstractIpc(transport), m_ctx(ctx) {}

namespace {

struct FileCategoryDefinition {
  std::string_view name;
  vicinae::FileCategory category;
};

constexpr FileCategoryDefinition FILE_CATEGORIES[] = {
    {.name = "other", .category = vicinae::FileCategory::Other},
    {.name = "directory", .category = vicinae::FileCategory::Directory},
    {.name = "image", .category = vicinae::FileCategory::Image},
    {.name = "video", .category = vicinae::FileCategory::Video},
    {.name = "audio", .category = vicinae::FileCategory::Audio},
    {.name = "document", .category = vicinae::FileCategory::Document},
    {.name = "archive", .category = vicinae::FileCategory::Archive},
    {.name = "application", .category = vicinae::FileCategory::Application},
};

std::optional<vicinae::FileCategory> fileCategoryFromString(std::string_view category) {
  for (const auto &definition : FILE_CATEGORIES) {
    if (definition.name == category) return definition.category;
  }
  return std::nullopt;
}

std::string_view fileCategoryToString(vicinae::FileCategory category) {
  for (const auto &definition : FILE_CATEGORIES) {
    if (definition.category == category) return definition.name;
  }
  return "other";
}

std::expected<ArgumentValues, std::string> buildLaunchArguments(const EntrypointId &id,
                                                                const ArgumentList &manifestArgs,
                                                                const std::vector<std::string> &values) {
  const auto fail = [&](auto &&message) {
    std::ostringstream oss;

    oss << message << "\n" << "Usage: vicinae cmd launch " << std::string{id};

    for (const auto &[idx, arg] : manifestArgs | vicinae::enumerate) {
      auto openChar = arg.required ? '<' : '[';
      auto closeChar = arg.required ? '>' : ']';
      oss << " " << openChar << arg.name.toStdString() << closeChar;
    }

    return std::unexpected(oss.str());
  };

  if (values.size() > manifestArgs.size()) {
    return fail(
        std::format("Too many arguments: expected at most {}, got {}", manifestArgs.size(), values.size()));
  }

  ArgumentValues arguments;
  arguments.reserve(values.size());

  for (const auto [idx, value] : values | vicinae::enumerate) {
    const auto &arg = manifestArgs.at(idx);
    const QString qvalue = QString::fromStdString(value);

    if (arg.type == CommandArgument::Dropdown && arg.data) {
      auto matchesValue = [&](const CommandArgument::DropdownData &option) { return option.value == qvalue; };
      if (!std::ranges::any_of(*arg.data, matchesValue)) {
        return fail(std::format("Invalid value '{}' for argument '{}'", value, arg.name.toStdString()));
      }
    }

    arguments.emplace_back(arg.name, qvalue);
  }

  for (size_t idx = values.size(); idx != manifestArgs.size(); ++idx) {
    const auto &arg = manifestArgs.at(idx);
    if (arg.required) { return fail(std::format("Missing required argument '{}'", arg.name.toStdString())); }
  }

  return arguments;
}

LaunchContext makeLaunchContext(const ipc_gen::LaunchCommandRequest &req) { return LaunchContext{}; }

} // namespace

ipc_gen::Result<ipc_gen::PingResponse>::Future IpcService::ping() {
  return ipc_gen::Result<ipc_gen::PingResponse>::ok(
      {.version = VICINAE_GIT_TAG, .pid = static_cast<int>(QCoreApplication::applicationPid())});
}

ipc_gen::Result<ipc_gen::DeeplinkResponse>::Future IpcService::deeplink(ipc_gen::DeeplinkRequest req) {
  IpcCommandHandler handler(m_ctx);
  QUrl const url(req.url.c_str());

  if (!url.isValid()) return ipc_gen::Result<ipc_gen::DeeplinkResponse>::fail("Not a valid URL");

  const auto res = handler.handleUrl(url);
  if (!res) return ipc_gen::Result<ipc_gen::DeeplinkResponse>::fail(res.error());

  return ipc_gen::Result<ipc_gen::DeeplinkResponse>::ok({});
}

ipc_gen::Result<ipc_gen::DescribeResponse>::Future IpcService::describe() {
  return ipc_gen::Result<ipc_gen::DescribeResponse>::ok(
      {.open = m_ctx.navigation->isWindowOpened(),
       .entrypoint = m_ctx.navigation->activeCommand()->uniqueId()});
}

ipc_gen::Result<std::vector<ipc_gen::FileResult>>::Future IpcService::fsQuery(std::string q,
                                                                              ipc_gen::FsQueryParams params) {
  auto files = m_ctx.services->fileService();
  IndexerQueryParams queryParams{.limit = params.limit};

  if (params.category) {
    auto category = fileCategoryFromString(*params.category);
    if (!category) {
      return ipc_gen::Result<std::vector<ipc_gen::FileResult>>::fail("Unknown file category: " +
                                                                     *params.category);
    }
    queryParams.category = *category;
  }

  return files->queryAsync(q, queryParams).then([](const std::vector<IndexerFileResult> &results) {
    auto fileResults =
        results | std::views::transform([](const IndexerFileResult &result) {
          return ipc_gen::FileResult{.path = result.path.string(),
                                     .score = static_cast<std::uint32_t>(result.rank),
                                     .category = std::string{fileCategoryToString(result.category)},
                                     .mimeType = result.mimeType};
        }) |
        std::ranges::to<std::vector>();

    return std::expected<std::vector<ipc_gen::FileResult>, std::string>{std::move(fileResults)};
  });
}

ipc_gen::Result<ipc_gen::LaunchAppResponse>::Future IpcService::launchApp(ipc_gen::LaunchAppRequest req) {
  auto appDb = m_ctx.services->appDb();
  auto wm = m_ctx.services->windowManager();
  auto app = appDb->findById(req.appId.c_str());

  if (!app) return ipc_gen::Result<ipc_gen::LaunchAppResponse>::fail("No app with id");

  if (!req.newInstance) {
    if (auto wins = wm->findAppWindows(*app); !wins.empty()) {
      auto &win = wins.front();
      wm->provider()->focusWindowSync(*win);
      return ipc_gen::Result<ipc_gen::LaunchAppResponse>::ok(
          {.focusedWindowTitle = win->title().toStdString()});
    }
  }

  std::vector<QString> const args = Utils::toQStringVec(req.args);

  if (!appDb->launch(*app, args))
    return ipc_gen::Result<ipc_gen::LaunchAppResponse>::fail(
        std::format("Failed to launch app with id {}", req.appId));

  return ipc_gen::Result<ipc_gen::LaunchAppResponse>::ok({});
}

ipc_gen::Result<ipc_gen::ListCommandsResponse>::Future IpcService::listCommands() {
  auto root = m_ctx.services->rootItemManager();
  auto commands =
      root->allItems() | std::views::transform([](auto &&item) {
        return ipc_gen::CommandInfo{.id = item.item->uniqueId(), .name = item.item->title().toStdString()};
      }) |
      std::ranges::to<std::vector>();

  std::ranges::sort(commands, [](const auto &a, const auto &b) { return a.id < b.id; });

  return ipc_gen::Result<ipc_gen::ListCommandsResponse>::ok({.commands = std::move(commands)});
}

ipc_gen::Result<ipc_gen::LaunchCommandResponse>::Future
IpcService::launchCommand(ipc_gen::LaunchCommandRequest req) {
  auto id = EntrypointId::fromSerialized(req.entrypoint);

  if (!id.isValid()) {
    return ipc_gen::Result<ipc_gen::LaunchCommandResponse>::fail(
        std::format("Ill-formed command entrypoint: {}", req.entrypoint));
  }

  auto root = m_ctx.services->rootItemManager();
  auto *item = root->findItemById(id);

  if (!item) {
    return ipc_gen::Result<ipc_gen::LaunchCommandResponse>::fail(
        std::format("Unknown command entrypoint: {}", req.entrypoint));
  }

  auto arguments = buildLaunchArguments(id, item->arguments(), req.args);

  if (!arguments) return ipc_gen::Result<ipc_gen::LaunchCommandResponse>::fail(arguments.error());

  LaunchProps props{
      .arguments = std::move(arguments).value(),
      .launchContext = makeLaunchContext(req),
      .fallbackText = req.query.transform(QString::fromStdString),
      .cwd = req.cwd.transform(QString::fromStdString),
  };

  if (!m_ctx.navigation->activateEntrypoint(id, {.props = props, .toggleIfAlreadyActive = false})) {
    return ipc_gen::Result<ipc_gen::LaunchCommandResponse>::fail("Failed to launch command");
  }

  return ipc_gen::Result<ipc_gen::LaunchCommandResponse>::ok({});
}

ipc_gen::Result<ipc_gen::DMenuResponse>::Future IpcService::dmenu(ipc_gen::DMenuRequest request) {
  static constexpr const int DMENU_SMALL_WIDTH_THRESHOLD = 500;
  auto &nav = m_ctx.navigation;
  auto &cfg = m_ctx.services->config()->value();

  QPromise<ipc_gen::Result<ipc_gen::DMenuResponse>::Type> promise;
  auto future = promise.future();

  if (request.width.has_value() && request.width.value() < DMENU_SMALL_WIDTH_THRESHOLD) {
    qInfo() << "dmenu: disabling quicklook and footer because width is too low";
    request.noFooter = true;
    request.noQuickLook = true;
  }

  // Convert to the DMenu-specific request type used by the view host
  ipc_gen::DMenuRequest viewReq = std::move(request);
  auto view = new DMenuViewHost(viewReq);

  using Watcher = QFutureWatcher<ipc_gen::Result<ipc_gen::DMenuResponse>::Type>;
  auto watcher = new Watcher;
  watcher->setFuture(future);

  QObject::connect(watcher, &Watcher::canceled, [nav = nav.get()]() { nav->closeWindow(); });
  QObject::connect(watcher, &Watcher::finished, [watcher]() { watcher->deleteLater(); });
  QObject::connect(view, &DMenuViewHost::selected,
                   [promise = std::move(promise)](const QString &text) mutable {
                     promise.addResult(ipc_gen::DMenuResponse{.output = text.toStdString()});
                     promise.finish();
                   });

  nav->popToRoot({.clearSearch = false});
  nav->pushView(view);
  nav->setInstantDismiss(true);

  if (request.width || request.height) {
    int const w = request.width.value_or(cfg.launcherWindow.size.width);
    int const h = request.height.value_or(cfg.launcherWindow.size.height);
    nav->requestWindowSize(QSize(w, h));
  }

  nav->showWindow();

  return future;
}

ipc_gen::Result<void>::Future IpcService::browserInit(ipc_gen::BrowserInitRequest req) {
  if (!m_caller) return ipc_gen::Result<void>::fail("No caller context");

  m_caller->browser = req;
  m_ctx.services->browserExtension()->registerBrowser({.id = req.id, .name = req.name});

  return ipc_gen::Result<void>::ok();
}

ipc_gen::Result<void>::Future IpcService::browserTabsChanged(std::vector<ipc_gen::BrowserTabInfo> tabs) {
  if (!m_caller || !m_caller->browser)
    return ipc_gen::Result<void>::fail("Only browser extensions can do this");

  m_ctx.services->browserExtension()->setTabs(m_caller->browser->id, tabs);

  return ipc_gen::Result<void>::ok();
}

// IpcCommandServer

IpcCommandServer::IpcCommandServer(ApplicationContext *ctx, QObject *parent)
    : QObject(parent), m_ctx(*ctx), m_rpc(m_transport), m_service(m_rpc, *ctx),
      m_ipcServer(m_rpc, &m_service) {

  connect(
      ctx->services->browserExtension(), &BrowserExtensionService::tabActionRequested, this,
      [this](std::string_view browserId, int tabId, BrowserExtensionService::TabAction action) {
        auto it = std::ranges::find_if(
            m_clients, [&](auto &&client) { return client.browser && client.browser->id == browserId; });

        if (it == m_clients.end()) {
          qWarning() << "focus tab requested but there is no browser extension client currently connected";
          return;
        }

        m_transport.conn = it->conn;

        switch (action) {
        case BrowserExtensionService::TabAction::Focus:
          m_service.emitfocusTab({.tabId = tabId});
          break;
        case BrowserExtensionService::TabAction::Close:
          m_service.emitcloseTab({.tabId = tabId});
          break;
        }

        m_transport.conn = nullptr;
      });
}

void IpcCommandServer::processFrame(QLocalSocket *conn, QByteArrayView frame) {
  auto clientInfoIt =
      std::ranges::find_if(m_clients, [conn](const ClientInfo &info) { return info.conn == conn; });

  if (clientInfoIt == m_clients.end()) return;

  m_transport.conn = conn;
  m_service.setCallerInfo(&(*clientInfoIt));

  // qDebug() << "IPC incoming:" << frame.toByteArray();

  m_ipcServer.route({frame.data(), static_cast<size_t>(frame.size())});
  m_service.setCallerInfo(nullptr);
  m_transport.conn = nullptr;
}

void IpcCommandServer::handleRead(QLocalSocket *conn) {
  auto it = std::ranges::find_if(m_clients, [conn](const ClientInfo &info) { return info.conn == conn; });

  if (it == m_clients.end()) {
    qWarning() << "CommandServer::handleRead: could not find client info";
    conn->disconnect();
    return;
  }

  while (conn->bytesAvailable() > 0) {
    it->frame.data.append(conn->readAll());

    while (std::cmp_greater_equal(it->frame.data.size(), sizeof(uint32_t))) {
      uint32_t const length = *reinterpret_cast<uint32_t *>(it->frame.data.data());
      bool const isComplete = it->frame.data.size() - sizeof(uint32_t) >= length;

      if (!isComplete) break;

      auto packet = QByteArrayView(it->frame.data).sliced(sizeof(uint32_t), length);

      processFrame(conn, packet);

      it->frame.data = it->frame.data.sliced(sizeof(uint32_t) + length);
    }
  }
}

void IpcCommandServer::handleDisconnection(QLocalSocket *conn) {
  auto it = std::ranges::find_if(m_clients, [conn](const ClientInfo &info) { return info.conn == conn; });

  if (it->browser) { m_ctx.services->browserExtension()->unregisterBrowser(it->browser->id); }

  m_transport.forgetConn(conn);
  if (m_transport.conn == conn) m_transport.conn = nullptr;

  m_clients.erase(it);
  conn->deleteLater();
}

void IpcCommandServer::handleConnection() {
  QLocalSocket *conn = m_server.nextPendingConnection();

  qDebug() << "ipc command server received new connection";

  m_clients.push_back({.conn = conn});
  connect(conn, &QLocalSocket::disconnected, this, [this, conn]() { handleDisconnection(conn); });
  connect(conn, &QLocalSocket::readyRead, this, [this, conn]() { handleRead(conn); });
}

bool IpcCommandServer::start(const std::string &name) {
#ifndef _WIN32
  // Named pipes have no filesystem entry; only a POSIX socket file can go stale and block listen().
  if (std::filesystem::exists(name)) { std::filesystem::remove(name); }
#endif

  if (!m_server.listen(QString::fromStdString(name))) {
    qDebug() << "CommandServer failed to listen" << m_server.errorString();
    return false;
  }

  connect(&m_server, &QLocalServer::newConnection, this, &IpcCommandServer::handleConnection);

  qDebug() << "Server started, listening on:" << name.c_str();

  return true;
}
