#include "navigation-controller.hpp"
#include <QTimer>
#include "command-controller.hpp"
#include "extension/extension-command.hpp"
#include "service-registry.hpp"
#include "qml/missing-preference-view-host.hpp"
#include "services/root-item-manager/root-item-manager.hpp"
#include "extension/manager/extension-manager.hpp"
#include "services/toast/toast-service.hpp"
#include "root-search/extensions/extension-root-provider.hpp"
#include "ui/action-pannel/action-panel-view.hpp"
#include "ui/alert/alert.hpp"
#include "ui/views/base-view.hpp"
#include "utils/environment.hpp"
#include <chrono>
#include <qlogging.h>
#include <QProcessEnvironment>
#include <ranges>
#include <utility>

NavigationController::NavigationController(ApplicationContext &ctx) : m_ctx(ctx) {}

void NavigationController::setNavigationTitle(const QString &navigationTitle, const BaseView *caller) {
  if (auto state = findViewState(VALUE_OR(caller, topView()))) {
    state->navigation.title = navigationTitle;

    if (state->sender == topView()) {
      emit navigationStatusChanged(state->navigation.title, state->navigation.icon);
    }
  }
}

bool NavigationController::hasCompleter() const {
  if (auto state = topState()) { return state->completer.has_value(); }
  return false;
}

void NavigationController::setInstantDismiss(bool value) { m_instantDismiss = value; }

void NavigationController::goBack(const GoBackOptions &opts) {
  if (!opts.ignoreInstantDismiss && viewStackSize() == 2 && m_instantDismiss) closeWindow();

  if (isRootSearch()) {
    if (searchText().isEmpty()) closeWindow();
    clearSearchText();
  }

  popCurrentView();
}

void NavigationController::broadcastSearchText(const QString &text, const BaseView *caller) {
  if (auto state = findViewState(VALUE_OR(caller, topView()))) {
    state->searchText = text;
    if (state->sender == topView()) { state->sender->textChanged(text); }
  }
}

void NavigationController::setSearchText(const QString &text, const BaseView *caller) {
  broadcastSearchText(text, caller);
  if (caller == nullptr || caller == topView()) { emit searchTextTampered(text); }
}

void NavigationController::setSearchPlaceholderText(const QString &text, const BaseView *caller) {
  if (auto state = findViewState(VALUE_OR(caller, topView()))) {
    state->placeholderText = text;
    if (state->sender == topView()) { emit searchPlaceholderTextChanged(state->placeholderText); }
  }
}

void NavigationController::setLoading(bool value, const BaseView *caller) {
  if (auto state = findViewState(VALUE_OR(caller, topView()))) {
    state->isLoading = value;
    if (state->sender == topView()) { emit loadingChanged(value); }
  }
}

void NavigationController::setBackButtonVisibility(bool value, const BaseView *caller) {
  if (auto state = findViewState(VALUE_OR(caller, topView()))) {
    state->showBackButton = value;
    if (state->sender == topView()) { emit backButtonVisibilityChanged(value); }
  }
}

void NavigationController::showHud(const QString &title, const std::optional<ImageURL> &icon) {
  closeWindow();

  if (Environment::isHudDisabled()) return;

  emit showHudRequested(title, icon);
}

void NavigationController::applyPopToRoot(const PendingPopToRoot &settings) {
  auto resolveApplicablePopToRoot = [&]() {
    if (settings.type == PopToRootType::Default)
      return m_popToRootOnClose ? PopToRootType::Immediate : PopToRootType::Suspended;
    return settings.type;
  };

  PopToRootType const popToRootType = resolveApplicablePopToRoot();

  switch (popToRootType) {
  case PopToRootType::Immediate:
    popToRoot({.clearSearch = true});
    break;
  default:
    break;
  }

  if (isRootSearch() && settings.clearSearch) clearSearchText();
}

void NavigationController::setDialog(DialogContentWidget *widget) {
  if (!m_windowOpened) {
    m_pendingPopToRoot.reset();
    showWindow();
  }
  emit confirmAlertRequested(widget);
}

void NavigationController::confirmAlert(const QString &title, const QString &description,
                                        const std::function<void()> &onConfirm) {
  auto widget = new CallbackAlertWidget;

  widget->setTitle(title);
  widget->setMessage(description);
  widget->setConfirmCallback(onConfirm);
  setDialog(widget);
}

void NavigationController::clearSearchText() { setSearchText(""); }

NavigationController::ViewState::~ViewState() {
  // Defer deletion so the C++ host outlives StackView's QML item cleanup.
  // StackView items have JavaScriptOwnership and are GC'd non-deterministically;
  // a plain deleteLater() can destroy the host first, causing QML bindings to
  // re-evaluate with a null reference.
  auto *s = sender;
  QTimer::singleShot(0, s, [s]() { s->deleteLater(); });
}

void NavigationController::openActionPanel() {
  if (m_isPanelOpened) return;

  m_isPanelOpened = true;
  emit actionPanelVisibilityChanged(m_isPanelOpened);
}

void NavigationController::closeActionPanel() {
  if (!m_isPanelOpened) return;

  m_isPanelOpened = false;
  emit actionPanelVisibilityChanged(m_isPanelOpened);
}

void NavigationController::toggleActionPanel() {
  m_isPanelOpened = !m_isPanelOpened;
  emit actionPanelVisibilityChanged(m_isPanelOpened);
}

void NavigationController::createCompletion(const ArgumentList &args, const ImageURL &icon) {
  static constexpr int MAX_COMPLETER_ARGS = 3;

  if (auto state = topState()) {
    // Limit arguments to what the UI can display
    auto truncated = args;
    if (static_cast<int>(truncated.size()) > MAX_COMPLETER_ARGS) truncated.resize(MAX_COMPLETER_ARGS);

    CompleterState completer(truncated, icon);

    // Initialize values with empty entries so validation can check them
    for (const auto &arg : truncated) {
      completer.values.emplace_back(arg.name, QString());
    }

    state->completer = completer;
    emit completionCreated(state->completer.value_or(completer));
  }
}

void NavigationController::destroyCurrentCompletion() {
  if (auto state = topState()) {
    state->completer.reset();
    emit completionDestroyed();
  }
}

ArgumentValues NavigationController::completionValues() const {
  if (auto state = topState()) {
    if (auto completer = state->completer) { return completer->values; }

    return {};
  }

  return {};
}

std::vector<QString> NavigationController::unnamedCompletionValues() const {
  return completionValues() | std::views::transform([](auto &&p) { return p.second; }) |
         std::ranges::to<std::vector>();
}

void NavigationController::setCompletionValues(const ArgumentValues &values) {
  if (auto state = topState()) {
    if (state->completer) {
      state->completer->values = values;
      state->sender->argumentValuesChanged(values);
      emit completionValuesChanged(values);
    }
  }
}

void NavigationController::setNavigationIcon(const ImageURL &icon) {
  if (auto state = topState()) {
    state->navigation.icon = icon;

    if (state->sender == topView()) {
      emit navigationStatusChanged(state->navigation.title, state->navigation.icon);
    }
  }
}

void NavigationController::setNavigationSuffixIcon(const std::optional<ImageURL> &icon) {
  if (auto state = topState()) {
    if (state->sender == topView()) { emit navigationSuffixIconChanged(icon); }
  }
}

void NavigationController::popCurrentView() {
  if (isRootSearch()) return;

  auto &state = m_views.back();

  state->sender->beforePop();
  emit viewPoped(state->sender);

  m_views.pop_back();

  // always turn off instant dismiss if we are back to the root search
  // for any reason. This typically happens when `goToBack` is called with
  // `ignoreInstantDismiss` set to true and we pop back to the root.
  // This is the default behaviour of hitting backspace in an empty search bar.
  if (isRootSearch()) { m_instantDismiss = false; }

  auto &next = m_views.back();

  emit currentViewChanged(*next.get());

  emit searchTextTampered(next->searchText);
  emit searchPlaceholderTextChanged(next->placeholderText);
  emit navigationStatusChanged(next->navigation.title, next->navigation.icon);
  emit headerVisiblityChanged(next->needsTopBar);
  emit searchVisibilityChanged(next->supportsSearch);
  emit searchInteractiveChanged(next->searchInteractive);
  emit statusBarVisiblityChanged(next->needsStatusBar);
  emit loadingChanged(next->isLoading);
  emit backButtonVisibilityChanged(next->showBackButton);

  if (auto cmpl = next->completer) {
    createCompletion(cmpl->args, cmpl->icon);
    setCompletionValues(cmpl->values);
  } else {
    destroyCurrentCompletion();
  }

  emit activeActionPanelChanged();

  selectSearchText();

  if (m_frames.empty()) return;

  auto &frame = m_frames.back();

  frame->viewCount -= 1;

  if (frame->viewCount == 0) { m_frames.pop_back(); }
}

void NavigationController::popToRoot(const PopToRootOptions &opts) {
  m_pendingPopToRoot.reset();
  if (!m_frames.empty() && m_frames.back()->viewCount == 0) { m_frames.pop_back(); }

  while (m_views.size() > 1) {
    popCurrentView();
  }

  if (opts.clearSearch) clearSearchText();
}

void NavigationController::selectSearchText() const { emit searchTextSelected(); }

QString NavigationController::searchText(const BaseView *caller) const {
  if (auto state = findViewState(VALUE_OR(caller, topView()))) { return state->searchText; }

  return QString();
}

bool NavigationController::isLoading(const BaseView *caller) const {
  if (auto state = findViewState(VALUE_OR(caller, topView()))) { return state->isLoading; }

  return false;
}

void NavigationController::clearActions(const BaseView *caller) {
  if (auto vs = findViewState(VALUE_OR(caller, topView()))) { vs->sender->clearActions(); }
}

QString NavigationController::navigationTitle(const BaseView *caller) const {
  if (auto state = findViewState(VALUE_OR(caller, topView()))) { return state->navigation.title; }

  return QString();
}

void NavigationController::setPopToRootOnClose(bool value) { m_popToRootOnClose = value; }

void NavigationController::closeWindow(const CloseWindowOptions &settings, std::chrono::milliseconds delay) {
  QTimer::singleShot(delay, [this, settings]() { closeWindow(settings); });
}

void NavigationController::closeWindow(const CloseWindowOptions &settings) {
  if (!m_windowOpened) return;

  PopToRootType type = settings.popToRootType;

  if (m_instantDismiss) {
    qDebug() << "Consumed instantDismiss flag";
    type = PopToRootType::Immediate;
    m_instantDismiss = false;
  }

  m_pendingPopToRoot = PendingPopToRoot{.type = type, .clearSearch = settings.clearRootSearch};
  m_windowOpened = false;
  emit windowVisiblityChanged(false);
}

bool NavigationController::windowActivated() { return m_windowActivated; }

void NavigationController::setCloseOnFocusLoss(bool value) { m_closeOnFocusLoss = value; }

void NavigationController::setWindowActivated(bool value) {
  if (m_windowActivated == value) return;

  if (!value && m_closeOnFocusLoss) { closeWindow(); }

  m_windowActivated = value;
  emit windowActivationChanged(value);
}

void NavigationController::toggleWindow() {
  if (m_windowOpened) {
    if (m_windowActivated) {
      closeWindow();
      return;
    }

    // if window was not activated, we reactivate it by closing then showing (most reliable way to ensure
    // reactivation)
    closeWindow();
  }

  showWindow();
}

bool NavigationController::isWindowOpened() const { return m_windowOpened; }

void searchPlaceholderText(const QString &text) {}

bool NavigationController::executePrimaryAction() {
  auto state = topState();
  if (!state) return false;

  auto *root = state->sender->actionPanelRoot();
  if (!root) return false;

  auto *action = root->primaryAction();
  if (!action) return false;

  executeAction(action);
  return false;
}

void NavigationController::setHeaderVisiblity(bool value, const BaseView *caller) {
  if (auto state = findViewState(VALUE_OR(caller, topView()))) {
    state->needsTopBar = value;
    if (state->sender == topView()) { emit headerVisiblityChanged(value); }
  }
}

void NavigationController::setSearchVisibility(bool value, const BaseView *caller) {
  if (auto state = findViewState(VALUE_OR(caller, topView()))) {
    state->supportsSearch = value;
    if (state->sender == topView()) { emit searchVisibilityChanged(value); }
  }
}

void NavigationController::setSearchInteractive(bool value, const BaseView *caller) {
  if (auto state = findViewState(VALUE_OR(caller, topView()))) {
    state->searchInteractive = value;
    if (state->sender == topView()) { emit searchInteractiveChanged(value); }
  }
}

void NavigationController::setStatusBarVisibility(bool value, const BaseView *caller) {
  if (auto state = findViewState(VALUE_OR(caller, topView()))) {
    state->needsStatusBar = value;
    if (state->sender == topView()) { emit statusBarVisiblityChanged(value); }
  }
}

void NavigationController::executeAction(AbstractAction *action) {
  auto state = topState();
  if (!state) return;

  std::shared_ptr<AbstractAction> guard;
  if (auto *root = state->sender->actionPanelRoot()) { guard = root->retainAction(action); }

  if (!guard) {
    executeActionNow(action);
    return;
  }

  // The action might tear down the window and the panel before the qml handler gets to run,
  // so defer execution to next event loop turn
  QMetaObject::invokeMethod(this, [this, action, guard] { executeActionNow(action); }, Qt::QueuedConnection);
}

void NavigationController::executeActionNow(AbstractAction *action) {
  auto state = topState();
  if (!state) return;

  state->sender->beforeActionExecuted(action);

  if (action->isSubmenu()) {
    openActionPanel();
    return;
  }

  if (auto cmpl = state->completer; cmpl && action->isPrimary()) {
    for (size_t i = 0; i != cmpl->args.size() && i != cmpl->values.size(); ++i) {
      const auto &arg = cmpl->args[i];
      const auto &value = cmpl->values[i];

      if (arg.required && value.second.isEmpty()) {
        emit invalidCompletionFired();
        return;
      }
    }
  }

  action->execute(&m_ctx);
  if (action->autoClose()) { closeWindow({.clearRootSearch = true}); }
  closeActionPanel();
}

void NavigationController::activateView(const ViewState &state) {
  emit headerVisiblityChanged(state.needsTopBar);
  emit searchVisibilityChanged(state.supportsSearch);
  emit searchInteractiveChanged(state.searchInteractive);
  emit statusBarVisiblityChanged(state.needsStatusBar);
  emit backButtonVisibilityChanged(state.showBackButton);
  emit loadingChanged(state.isLoading);
  emit activeActionPanelChanged();
  emit searchPlaceholderTextChanged(state.placeholderText);
  destroyCurrentCompletion();

  state.sender->initialize();
  state.sender->activate();

  emit currentViewChanged(state);
  emit searchTextTampered(state.searchText);
  emit navigationStatusChanged(state.navigation.title, state.navigation.icon);
}

void NavigationController::replaceView(BaseView *view) {
  QTimer::singleShot(0, this, [this, view]() {
    m_views.back() = createViewState(view);
    emit viewReplaced();
    activateView(*topState());
  });
}

void NavigationController::pushView(BaseView *view) {
  m_frames.back()->viewCount++;
  m_views.emplace_back(createViewState(view));
  activateView(*topState());
  emit viewPushed(view);
}

void NavigationController::setActions(std::unique_ptr<ActionPanelState> panel, const BaseView *caller) {
  if (auto vs = findViewState(VALUE_OR(caller, topView()))) { vs->sender->setActions(std::move(panel)); }
}

void NavigationController::setActions(ActionPanelView *view, const BaseView *caller) {
  if (auto vs = findViewState(VALUE_OR(caller, topView()))) { vs->sender->setActions(view); }
}

void NavigationController::notifyActionPanelChanged(const BaseView *caller) {
  if (VALUE_OR(caller, topView()) == topView()) { emit activeActionPanelChanged(); }
}

size_t NavigationController::viewStackSize() const { return m_views.size(); }

void NavigationController::showWindow() {
  if (m_windowOpened) return;
  if (auto popToRoot = m_pendingPopToRoot) {
    applyPopToRoot(*popToRoot);
    m_pendingPopToRoot.reset();
  }
  m_windowOpened = true;
  emit windowVisiblityChanged(true);
}

void NavigationController::requestWindowSize(QSize size) { emit windowSizeRequested(size); }

NavigationController::ViewState *NavigationController::topState() {
  if (m_views.empty()) return nullptr;

  return m_views.back().get();
}

const NavigationController::ViewState *NavigationController::topState() const {
  if (m_views.empty()) return nullptr;

  return m_views.back().get();
}

NavigationController::ViewState *NavigationController::findViewState(const BaseView *view) {
  // NOLINTBEGIN(cppcoreguidelines-pro-type-const-cast)
  return const_cast<ViewState *>(std::as_const(*this).findViewState(view));
  // NOLINTEND(cppcoreguidelines-pro-type-const-cast)
}

bool NavigationController::isRootSearch() const { return m_views.size() == 1; }

const NavigationController::ViewState *NavigationController::findViewState(const BaseView *view) const {
  auto pred = [&](auto &&state) { return state->sender == view; };

  if (auto it = std::ranges::find_if(m_views, pred); it != m_views.end()) { return it->get(); }

  return nullptr;
}

const BaseView *NavigationController::topView() const {
  if (m_views.empty()) return nullptr;

  return m_views.back()->sender;
}

bool NavigationController::reloadActiveCommand() {
  auto cmd = activeCommand();

  if (!cmd) return false;

  // we only store the id, as the pointer will likely become invalid
  // after unloading.
  EntrypointId const id = cmd->uniqueId();

  unloadActiveCommand();

  auto *item = m_ctx.services->rootItemManager()->findItemById(id);
  if (auto *ext = dynamic_cast<CommandRootItem *>(item)) { launch(ext->command()); }

  return true;
}

const AbstractCmd *NavigationController::activeCommand() const {
  if (m_frames.empty()) return nullptr;

  return m_frames.back()->command.get();
}

void NavigationController::unloadActiveCommand() {
  if (m_frames.empty()) {
    qWarning() << "unloadActiveCommand called while no commands are loaded";
    return;
  }

  auto size = m_frames.back()->viewCount;

  for (int i = 0; std::cmp_less(i, size); ++i) {
    popCurrentView();
  }
}

bool NavigationController::activateEntrypoint(const EntrypointId &id,
                                              const ActivateEntrypointOptions &options) {
  const auto *root = m_ctx.services->rootItemManager();
  const auto *entrypoint = root->findItemById(id);

  if (!entrypoint) {
    qWarning() << "activateEntrypoint: unknown entrypoint" << QString::fromStdString(std::string{id});
    return false;
  }

  const auto previouslyActive = activeCommand();
  const bool initialOpenState = m_ctx.navigation->isWindowOpened();
  const bool isSameView = previouslyActive->uniqueId() == id && previouslyActive->isView();

  // toggle visibility if we are already showing
  if (options.toggleIfAlreadyActive && initialOpenState && isSameView) {
    popToRoot({.clearSearch = false});
    m_ctx.navigation->closeWindow();
    return true;
  }

  popToRoot({.clearSearch = false});

  // programmatic activation bypasses the action execution funnel, so the view hook
  // never fires for it: register the visit explicitly.
  m_ctx.services->rootItemManager()->registerVisit(id);

  // FIXME: we need a unified interface for this
  if (auto *ext = dynamic_cast<const CommandRootItem *>(entrypoint)) {
    launch(ext->command(), options.props);
  } else {
    // FIXME: hacky, again we need a proper unified interface for this
    createCompletion(entrypoint->arguments(), entrypoint->iconUrl());
    setCompletionValues(options.props.arguments);

    auto panel = entrypoint->newActionPanel(&m_ctx, root->itemMetadata(id));
    panel->finalize();
    auto *action = panel->primaryAction();
    if (!action) {
      qWarning() << "activateEntrypoint: no primary action for" << QString::fromStdString(std::string{id});
      return false;
    }
    action->execute(&m_ctx);
  }

  if (auto fallback = options.props.fallbackText) { setSearchText(fallback.value()); }

  if (!isRootSearch() && !initialOpenState) {
    setInstantDismiss();
    setBackButtonVisibility(false);
    showWindow();
  }

  return true;
}

void NavigationController::launch(const std::shared_ptr<AbstractCmd> &cmd) {
  launch(cmd, LaunchProps{.arguments = completionValues()});
}

void NavigationController::launch(const std::shared_ptr<AbstractCmd> &cmd, const ArgumentValues &arguments) {
  launch(cmd, LaunchProps{.arguments = arguments});
}

void NavigationController::launch(const std::shared_ptr<AbstractCmd> &cmd, const LaunchProps &props) {
  // unload stalled no-view command
  if (!m_frames.empty() && m_frames.back()->viewCount == 0) { m_frames.pop_back(); }

  if (cmd->type() == CommandType::CommandTypeExtension && !m_ctx.services->extensionManager()->isRunning()) {
    m_ctx.services->toastService()->failure(tr("Extension manager is not running"));
    return;
  }

  bool const shouldCheckPreferences = cmd->type() == CommandType::CommandTypeExtension;

  if (shouldCheckPreferences) {
    auto itemId = cmd->uniqueId();
    auto manager = m_ctx.services->rootItemManager();
    auto preferences = manager->getMergedItemPreferences(itemId);
    auto preferenceValues = manager->getPreferenceValues(itemId);

    for (const auto &preference : preferences) {
      QJsonValue const value = preferenceValues.value(preference.name());
      bool const hasValue = !(value.isUndefined() || value.isNull());
      bool const hasDefault = !preference.defaultValue().isUndefined();
      bool const isMissing = preference.required() && !hasValue && !hasDefault;

      if (!isMissing) continue;

      if (cmd->type() == CommandType::CommandTypeExtension) {
        auto extensionCommand = std::static_pointer_cast<ExtensionCommand>(cmd);

        m_ctx.navigation->pushView(
            new MissingPreferenceViewHost(extensionCommand, preferences, preferenceValues));
        m_ctx.navigation->setNavigationTitle(cmd->name());
        m_ctx.navigation->setNavigationIcon(cmd->iconUrl());
        return;
      }

      qDebug() << "MISSING PREFERENCE" << preference.title();
    }
  }

  auto frame = std::make_unique<CommandFrame>();

  frame->controller = std::make_unique<CommandController>(m_ctx, *cmd, props);
  frame->context.reset(cmd->createContext(cmd));
  frame->command = cmd;
  frame->viewCount = 0;
  frame->context->setContext(&m_ctx);
  m_frames.emplace_back(std::move(frame));
  m_frames.back()->context->load(props);
}

std::unique_ptr<NavigationController::ViewState> NavigationController::createViewState(BaseView *view) const {
  auto state = std::make_unique<ViewState>();
  state->sender = view;
  state->sender->setContext(&m_ctx);
  state->sender->setCommandController(m_frames.back()->controller.get());
  state->supportsSearch = view->supportsSearch();
  state->searchInteractive = view->searchInteractive();
  state->needsTopBar = view->needsGlobalTopBar();
  state->needsStatusBar = view->needsGlobalStatusBar();
  state->showBackButton = view->showBackButton();
  state->placeholderText = view->initialSearchPlaceholderText();
  state->navigation.title = view->initialNavigationTitle();
  state->navigation.icon = view->initialNavigationIcon();
  return state;
}
