// CoolerControl - monitor and control your cooling and other devices
// Copyright (c) 2021-2025  Guy Boldon and contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

#include "main_window.h"

#include <QAction>
#include <QApplication>
#include <QDebug>
#include <QDesktopServices>
#include <QDir>
#include <QFileDialog>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QMenu>
#include <QMessageBox>
#include <QNetworkCookieJar>
#include <QNetworkReply>
#include <QSettings>
#include <QShortcut>
#include <QStringBuilder>  // for % operator
#include <QSystemTrayIcon>
#include <QThread>
#include <QTimer>
#include <QWebEngineCookieStore>
#include <QWebEngineDownloadRequest>
#include <QWebEngineFullScreenRequest>
#include <QWebEngineNewWindowRequest>
#include <QWebEngineSettings>
#include <QWebEngineView>
#include <QWizardPage>

#include "constants.h"
#include "notifier.h"

class PersistentCookieJar final : public QNetworkCookieJar {
 public:
  explicit PersistentCookieJar(QObject* parent = nullptr) : QNetworkCookieJar(parent) { load(); }

  bool insertCookie(const QNetworkCookie& cookie) override {
    if (QNetworkCookieJar::insertCookie(cookie)) {
      save();
      return true;
    }
    return false;
  }

  bool deleteCookie(const QNetworkCookie& cookie) override {
    if (QNetworkCookieJar::deleteCookie(cookie)) {
      save();
      return true;
    }
    return false;
  }

 private:
  void save() const {
    QSettings settings;
    QByteArray cookieData;
    for (const auto& cookie : allCookies()) {
      cookieData.append(cookie.toRawForm());
      cookieData.append("\n");
    }
    settings.setValue("networkCookies", cookieData);
  }

  void load() {
    const QSettings settings;
    const auto cookieData = settings.value("networkCookies").toByteArray();
    if (cookieData.isEmpty()) return;
    const auto cookies = QNetworkCookie::parseCookies(cookieData);
    for (const auto& cookie : cookies) {
      QNetworkCookieJar::insertCookie(cookie);
    }
    qDebug() << "Loaded" << cookies.size() << "persisted cookies for QNetworkAccessManager";
  }
};

MainWindow::MainWindow(QWidget* parent)
    : QMainWindow(parent),
      m_view(new QWebEngineView(parent)),
      m_profile(new QWebEngineProfile(WEBENGINE_PROFILE_NAME.c_str(), m_view)),
      m_page(new QWebEnginePage(m_profile)),
      m_channel(new QWebChannel(m_page)),
      m_ipc(new IPC(this)),
      m_wizard(new QWizard(parent)),
      m_manager(new QNetworkAccessManager(parent)),
      m_retryTimer(new QTimer(parent)) {
  setCentralWidget(m_view);
  m_profile->settings()->setAttribute(QWebEngineSettings::Accelerated2dCanvasEnabled, true);
  m_profile->settings()->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, true);
  m_profile->settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, false);
  m_profile->settings()->setAttribute(QWebEngineSettings::PluginsEnabled, false);
  m_profile->settings()->setAttribute(QWebEngineSettings::JavascriptCanAccessClipboard, true);
  m_profile->settings()->setAttribute(QWebEngineSettings::PdfViewerEnabled, false);
  // local storage: ~/.local/share/{APP_NAME}
  m_profile->settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, true);
  m_profile->setPersistentCookiesPolicy(
      QWebEngineProfile::PersistentCookiesPolicy::ForcePersistentCookies);
  connect(m_profile, &QWebEngineProfile::downloadRequested,
          [this](QWebEngineDownloadRequest* download) {
            Q_ASSERT(download && download->state() == QWebEngineDownloadRequest::DownloadRequested);
            if (download->isSavePageDownload()) {
              qInfo() << "Saving web pages is disabled.";
              return;
            }
            const QString path = QFileDialog::getSaveFileName(
                this, tr("Save as"),
                QDir(download->downloadDirectory()).filePath(download->downloadFileName()));
            if (path.isEmpty()) return;  // cancelled

            download->setDownloadDirectory(QFileInfo(path).path());
            download->setDownloadFileName(QFileInfo(path).fileName());
            download->accept();
          });
  m_channel->registerObject("ipc", m_ipc);
  m_page->setWebChannel(m_channel);
  // This allows external links in our app to be opened by the external browser:
  connect(m_page, &QWebEnginePage::newWindowRequested,
          [](QWebEngineNewWindowRequest const& request) {
            QDesktopServices::openUrl(request.requestedUrl());
          });
  connect(m_page, &QWebEnginePage::fullScreenRequested,
          [this](QWebEngineFullScreenRequest request) {
            // Qt/WebEngine has a strange bug here where toggleOn() is aligned with the JS engine
            //  and not with the Window's real full-screen state. Can cause an edge case issue when
            //  shortcut and toggle are used and the user refreshes the UI.
            qDebug() << "FullScreen request: " << request.toggleOn();
            request.accept();
            setWindowState(windowState() ^ Qt::WindowFullScreen);
          });
  m_view->setPage(m_page);
  m_manager->setCookieJar(new PersistentCookieJar(m_manager));
  const auto cookieStore = m_profile->cookieStore();
  connect(cookieStore, &QWebEngineCookieStore::cookieAdded,
          [this](const QNetworkCookie& cookie) { m_manager->cookieJar()->insertCookie(cookie); });
  connect(cookieStore, &QWebEngineCookieStore::cookieRemoved,
          [this](const QNetworkCookie& cookie) { m_manager->cookieJar()->deleteCookie(cookie); });
  cookieStore->loadAllCookies();

  connect(this, &MainWindow::daemonConnectionLost, this, &MainWindow::reestablishDaemonConnection,
          Qt::QueuedConnection);
  connect(this, &MainWindow::watchForSSE, this, &MainWindow::startWatchingSSE,
          Qt::QueuedConnection);
  m_retryTimer->setInterval(DEFAULT_CONNECTION_RETRY_INTERVAL_MS);
  connect(m_retryTimer, &QTimer::timeout, this, &MainWindow::tryDaemonConnection);
  connect(m_ipc, &IPC::forceWindowShow, this, [this]() {
    // This is used so the UI Window will show when password input is required
    setAttribute(Qt::WidgetAttribute::WA_DontShowOnScreen, false);
    showNormal();
    qInfo() << "Force UI Window to show.";
  });

  initWizard();
  initDelay();
  initSystemTray();
  initWebUI();

  const auto fullscreenKeyToggle = new QShortcut(this);
  fullscreenKeyToggle->setKey(Qt::Key_F11);
  connect(fullscreenKeyToggle, &QShortcut::activated, [this]() {
    qDebug() << "FullScreen Key Triggered";
    setWindowState(windowState() ^ Qt::WindowFullScreen);
    emit m_ipc->fullScreenToggled(isFullScreen());
  });
}

void MainWindow::initWizard() {
  m_wizard->setWindowTitle("Daemon Connection Error");
  m_wizard->setOption(QWizard::IndependentPages, true);
  m_wizard->setButtonText(QWizard::WizardButton::FinishButton, "&Apply");
  m_wizard->setOption(QWizard::CancelButtonOnLeft, true);
  m_wizard->setButtonText(QWizard::CustomButton1, "&Retry");
  m_wizard->setOption(QWizard::HaveCustomButton1, true);
  m_wizard->setButtonText(QWizard::HelpButton, "&Quit App");
  m_wizard->setOption(QWizard::HaveHelpButton, true);
  m_wizard->addPage(new IntroPage(m_wizard));
  const auto addressPage = new AddressPage(m_wizard);
  m_wizard->addPage(addressPage);
  m_wizard->setMinimumSize(640, 480);
  connect(m_wizard, &QWizard::helpRequested, this, &MainWindow::forceQuit, Qt::QueuedConnection);
  connect(m_wizard, &QWizard::customButtonClicked, [this](const int which) {
    if (which == 6) {  // Retry CustomButton1
      delay(500);
      m_uiLoadingStopped = false;
      m_view->load(getDaemonUrl());
      m_wizard->hide();
    }
  });
  connect(m_wizard, &QDialog::accepted, [this]() {
    QSettings settings;
    settings.setValue(SETTING_DAEMON_ADDRESS.data(), m_wizard->field("address").toString());
    settings.setValue(SETTING_DAEMON_PORT.data(), m_wizard->field("port").toInt());
    settings.setValue(SETTING_DAEMON_SSL_ENABLED.data(), m_wizard->field("ssl").toBool());
    m_changeAddress = true;
    emit dropConnections();
    delay(300);  // give signals a moment to process.
    m_startup = true;
    m_changeAddress = false;
    m_uiLoadingStopped = false;
    m_view->load(getDaemonUrl());
  });
}

void MainWindow::initDelay() const {
  if (const auto startupDelay = m_ipc->getStartupDelay(); startupDelay > 0) {
    qInfo() << "Waiting for startup delay: " << startupDelay << "s";
    QThread::sleep(startupDelay);
  }
}

void MainWindow::initSystemTray() {
  m_sysTrayIcon = new QSystemTrayIcon(this->parent());
  const auto ccHeader = new QAction(
      QIcon::fromTheme(APP_ID.data(), QIcon(":/icons/org.coolercontrol.CoolerControl.svg")),
      tr("CoolerControl"), m_sysTrayIcon);
  ccHeader->setDisabled(true);
  m_showAction = m_ipc->getStartInTray() ? new QAction(tr("&Show"), m_sysTrayIcon)
                                         : new QAction(tr("&Hide"), m_sysTrayIcon);
  connect(
      m_showAction, &QAction::triggered, this,
      [this]() {
        if (isVisible()) {
          hide();
        } else {
          showNormal();
          raise();
          activateWindow();
        }
      },
      Qt::QueuedConnection);

  m_addressAction = new QAction(tr("&Daemon Address"), m_sysTrayIcon);
  connect(m_addressAction, &QAction::triggered, [this]() { displayAddressWizard(); });

  m_quitAction =
      new QAction(QIcon::fromTheme("application-exit", QIcon()), tr("&Quit"), m_sysTrayIcon);
  connect(m_quitAction, &QAction::triggered, this, &MainWindow::forceQuit, Qt::QueuedConnection);
  m_trayIconMenu = new QMenu(this);
  m_trayIconMenu->setTitle("CoolerControl");
  m_trayIconMenu->addAction(ccHeader);
  m_trayIconMenu->addSeparator();
  m_modesTrayMenu = new QMenu(this);
  m_modesTrayMenu->setTitle("Modes");
  m_modesTrayMenu->setEnabled(false);
  m_trayIconMenu->addMenu(m_modesTrayMenu);
  m_trayIconMenu->addAction(m_showAction);
  m_trayIconMenu->addAction(m_addressAction);
  m_trayIconMenu->addSeparator();
  m_trayIconMenu->addAction(m_quitAction);

  m_sysTrayIcon->setContextMenu(m_trayIconMenu);
  m_sysTrayIcon->setIcon(QIcon::fromTheme(
      APP_ID_SYMBOLIC.data(),
      QIcon::fromTheme(APP_ID.data(),
                       QIcon(":/icons/org.coolercontrol.CoolerControl-symbolic.svg"))));
  m_sysTrayIcon->setToolTip("CoolerControl");
  m_sysTrayIcon->show();

  // left click:
  connect(
      m_sysTrayIcon, &QSystemTrayIcon::activated, this,
      [this](auto reason) {
        if (reason == QSystemTrayIcon::Trigger) {
          if (isVisible()) {
            hide();
          } else {
            showNormal();
            raise();
            activateWindow();
          }
        }
      },
      Qt::QueuedConnection);
}

void MainWindow::initWebUI() {
  connect(m_page, &QWebEnginePage::certificateError, [](QWebEngineCertificateError error) {
    QList<QSslCertificate> chain = error.certificateChain();
    qDebug() << error.description()
             << tr(" there are %1 certificates in chain. From %2")
                    .arg(chain.count())
                    .arg(error.url().toDisplayString());
    auto has_self_signed = false;
    foreach (QSslCertificate cert, chain) {
      qDebug() << tr("name: %1 expires:%2 isSelf-signed: %3")
                      .arg(cert.subjectDisplayName(), cert.expiryDate().toString(),
                           cert.isSelfSigned() ? "yes" : "no");
      if (cert.isSelfSigned()) {
        has_self_signed = true;
      }
    }
    if (error.type() == QWebEngineCertificateError::CertificateAuthorityInvalid &&
        has_self_signed) {
      qInfo() << "Accepting self-signed certificate.";
      return error.acceptCertificate();
    }
    return error.defer();
  });
  m_view->load(getDaemonUrl());
  connect(m_view, &QWebEngineView::loadFinished, [this](const bool pageLoadedSuccessfully) {
    if (!pageLoadedSuccessfully) {
      if (m_uiLoadRetryCount < MAX_UI_LOAD_RETRIES) {
        m_uiLoadRetryCount++;
        qDebug() << "UI load failed, retrying..." << m_uiLoadRetryCount << "/"
                 << MAX_UI_LOAD_RETRIES;
        QTimer::singleShot(1500, this, [this]() { m_view->load(getDaemonUrl()); });
        return;
      }
      m_uiLoadingStopped = true;
      m_uiLoadRetryCount = 0;
      displayAddressWizard();
      notifyDaemonConnectionError();
    } else {
      m_uiLoadingStopped = false;
      m_uiLoadRetryCount = 0;
      qInfo() << "Successfully loaded UI at: " << getDaemonUrl().url();
      if (m_startup) {  // don't do this for Wizard retries
        m_retryTimer->start();
      }
    }
  });
}

void MainWindow::forceQuit() {
  qDebug() << "Force Quit Triggered";
  m_forceQuit = true;
  // Triggers the close event but with the forceQuit flag set
  close();
}

void MainWindow::forceRefresh() const {
  qInfo() << "Forced UI refresh";
  // Note: clearHttpCache() is intentionally NOT called here because it can
  // interfere with QWebEngineProfile's cookie state, causing session cookies
  // to be lost on page reload. The daemon's cache headers already handle asset
  // freshness correctly (no-cache for index.html, immutable for hashed assets).
  m_uiLoadingStopped = false;
  m_view->load(getDaemonUrl());
}

void MainWindow::closeEvent(QCloseEvent* event) {
  if (!m_forceQuit && m_startup && !m_uiLoadingStopped && !m_webLoadFinished) {
    // Killing the app during initialization can cause a crash
    event->ignore();
    return;
  }
  if (isVisible()) {
    m_ipc->saveWindowGeometry(saveGeometry());
  }
  if (m_ipc->getCloseToTray() && !m_forceQuit) {
    delay(100);
    hide();
    event->ignore();
    return;
  }
  m_forceQuit = true;    // keeps from triggering disconnect notification
  m_retryTimer->stop();  // stop reconnecting if running
  emit dropConnections();
  m_ipc->syncSettings();
  event->accept();
  m_page->deleteLater();
  delay(200);
  QApplication::quit();
}

void MainWindow::hideEvent(QHideEvent* event) {
  if (m_startup && !m_webLoadFinished) {
    // opening/closing the window during initialization can cause issues.
    event->ignore();
    return;
  }
  delay(100);
  setTrayActionToShow();
  event->accept();
}

void MainWindow::showEvent(QShowEvent* event) {
  if (m_startup) {
    // opening/closing the window during initialization can cause issues.
    event->ignore();
    return;
  }
  delay(100);
  setTrayActionToHide();
  event->accept();
}

QUrl MainWindow::getDaemonUrl(const bool forceHttp) {
  const QSettings settings;
  const auto host =
      settings.value(SETTING_DAEMON_ADDRESS.data(), DEFAULT_DAEMON_ADDRESS.data()).toString();
  const auto port = settings.value(SETTING_DAEMON_PORT.data(), DEFAULT_DAEMON_PORT).toInt();
  const auto sslEnabled =
      settings.value(SETTING_DAEMON_SSL_ENABLED.data(), DEFAULT_DAEMON_SSL_ENABLED).toBool();
  const auto schema = forceHttp ? tr("http") : sslEnabled ? tr("https") : tr("http");
  QUrl url;
  url.setScheme(schema);
  url.setHost(host);
  url.setPort(port);
  return url;
}

QUrl MainWindow::getEndpointUrl(const QString& endpoint, const bool forceHttp) {
  auto url = getDaemonUrl(forceHttp);
  url.setPath(endpoint);
  // for testing with npm dev server:
  // url.setPort(DEFAULT_DAEMON_PORT);
  return url;
}

void MainWindow::displayAddressWizard() const {
  if (m_wizard->isVisible()) {
    return;
  }
  m_wizard->open();
}

void MainWindow::handleStartInTray() {
  restoreGeometry(m_ipc->getWindowGeometry());
  setZoomFactor(m_ipc->getZoomFactor());
  if (m_ipc->getStartInTray()) {
    setAttribute(Qt::WidgetAttribute::WA_DontShowOnScreen, true);
    show();  // this triggers browser engine rendering - which we want for startup&login
    connect(
        m_ipc, &IPC::webLoadFinished, this,
        [this]() {
          delay(300);  // small pause to let web engine breath before suspending.
          m_webLoadFinished = true;
          hide();
          setAttribute(Qt::WidgetAttribute::WA_DontShowOnScreen, false);
          qInfo() << "Initialized closed to system tray.";
        },
        Qt::SingleShotConnection);
  } else {
    show();
    connect(
        m_ipc, &IPC::webLoadFinished, this,
        [this]() {
          m_webLoadFinished = true;
          qInfo() << "Initialized open window.";
        },
        Qt::SingleShotConnection);
  }
}

void MainWindow::setZoomFactor(const double zoomFactor) const { m_view->setZoomFactor(zoomFactor); }

void MainWindow::delay(const int millisecondsWait) {
  QEventLoop loop;
  QTimer t;
  connect(&t, &QTimer::timeout, &loop, &QEventLoop::quit);
  t.start(millisecondsWait);
  loop.exec();
}

void MainWindow::setTrayActionToShow() const { m_showAction->setText(tr("&Show")); }

void MainWindow::setTrayActionToHide() const { m_showAction->setText(tr("&Hide")); }

void MainWindow::showVersionMismatchDialog(const QString& daemonVersion) const {
  const auto appVersion = QString::fromStdString(COOLER_CONTROL_VERSION);
  QMessageBox dialog;
  dialog.setWindowTitle(tr("Version Mismatch"));
  dialog.setIcon(QMessageBox::Warning);
  dialog.setText(tr("The desktop app version (%1) does not match the daemon version (%2).")
                     .arg(appVersion, daemonVersion));
  dialog.setInformativeText(
      tr("Please restart the desktop app to load the correct interface version."));
  const auto quitButton = dialog.addButton(tr("&Quit App"), QMessageBox::AcceptRole);
  dialog.addButton(tr("Continue Anyway"), QMessageBox::RejectRole);
  dialog.setDefaultButton(quitButton);
  dialog.exec();
  if (dialog.clickedButton() == quitButton) {
    m_forceQuit = true;
    QApplication::quit();
  }
}

void MainWindow::notifyDaemonConnectionError() {
  // Qt has some issues around message icons, and we now use DBus notifications
  // now directly to handle the important ones better.
  Notifier::send("Daemon Connection Error", "Connection with the daemon could not be established",
                 1);
}

void MainWindow::notifyDaemonErrors() {
  Notifier::send("Daemon Errors", "The daemon logs contain errors. You should investigate.", 4);
}

void MainWindow::notifyDaemonDisconnected() {
  Notifier::send("Daemon Disconnected", "Connection with the daemon has been lost", 1);
}

void MainWindow::notifyDaemonConnectionRestored() {
  Notifier::send("Daemon Connection Restored", "Connection with the daemon has been restored.", 2);
}

QIcon MainWindow::createIconWithNotificationBadge(const QIcon& baseIcon, const bool redColor) {
  // This doesn't work very well with newer Gnome and symbolic icons. Using actual icons.
  constexpr int iconSize = 64;
  QPixmap pixmap = baseIcon.pixmap(iconSize, iconSize);
  QPainter painter(&pixmap);
  painter.setRenderHint(QPainter::Antialiasing);

  constexpr int dotSize = 20;
  constexpr int x = (iconSize / 2) - (dotSize / 2);
  constexpr int y = iconSize - (dotSize + 6);

  // Bootstrap danger red or warning yellow
  painter.setBrush(redColor ? QColor(220, 53, 69) : QColor(255, 193, 7));
  painter.setPen(QPen(redColor ? QColor(180, 40, 50) : QColor(215, 165, 0), 2));
  painter.drawEllipse(x, y, dotSize, dotSize);

  return (pixmap);
}

void MainWindow::applyTrayIconNotificationBadge(const bool forceBadge) const {
  if (forceBadge || m_daemonHasErrors || m_daemonHasWarnings || m_alertCount > 0) {
    m_sysTrayIcon->setIcon(QIcon::fromTheme(
        APP_ID_SYMBOLIC_ALERT.data(),
        QIcon::fromTheme(APP_ID_ALERT.data(),
                         QIcon(":/icons/org.coolercontrol.CoolerControl-symbolic-alert.svg"))));
  } else {
    m_sysTrayIcon->setIcon(QIcon::fromTheme(
        APP_ID_SYMBOLIC.data(),
        QIcon::fromTheme(APP_ID.data(),
                         QIcon(":/icons/org.coolercontrol.CoolerControl-symbolic.svg"))));
  }
}

void MainWindow::requestDaemonErrors() const {
  QNetworkRequest healthRequest;
  healthRequest.setTransferTimeout(DEFAULT_CONNECTION_TIMEOUT_MS);
  healthRequest.setUrl(getEndpointUrl(ENDPOINT_HEALTH.data()));
  const auto healthReply = m_manager->get(healthRequest);
  connect(healthReply, &QNetworkReply::sslErrors, healthReply,
          qOverload<>(&QNetworkReply::ignoreSslErrors));
  connect(healthReply, &QNetworkReply::readyRead, [healthReply, this]() {
    const auto status = healthReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    if (status == 401) {
      qDebug() << "Health endpoint returned 401 - not yet authenticated.";
      healthReply->deleteLater();
      return;
    }
    const QString replyText = healthReply->readAll();
    qDebug() << "Health Endpoint Response Status: " << status << "; Body: " << replyText;
    const QJsonObject rootObj = QJsonDocument::fromJson(replyText.toUtf8()).object();
    const auto daemonVersion = rootObj.value("details").toObject().value("version").toString();
    if (daemonVersion.isEmpty()) {
      qWarning() << "Health version response is empty - must NOT be connected to the daemon API.";
    } else {
      const auto appVersion = QString::fromStdString(COOLER_CONTROL_VERSION);
      if (daemonVersion != appVersion) {
        showVersionMismatchDialog(daemonVersion);
      }
    }
    if (const auto errors = rootObj.value("details").toObject().value("errors").toInt();
        errors > 0) {
      m_daemonHasErrors = true;
      notifyDaemonErrors();
    }
    if (const auto warnings = rootObj.value("details").toObject().value("warnings").toInt();
        warnings > 0) {
      m_daemonHasWarnings = true;
    }
    applyTrayIconNotificationBadge();
    healthReply->deleteLater();
  });
  connect(healthReply, &QNetworkReply::errorOccurred,
          [healthReply](const QNetworkReply::NetworkError code) {
            const auto status =
                healthReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
            qWarning() << "Error occurred connecting to Daemon Health endpoint. Status: " << status
                       << " QtErrorCode: " << code;
            healthReply->deleteLater();
          });
}

void MainWindow::acknowledgeDaemonErrors() const {
  m_daemonHasErrors = false;
  m_daemonHasWarnings = false;
  applyTrayIconNotificationBadge();
}

// requestAllAlerts - and set the tray icon notification badge if any are active
void MainWindow::requestAllAlerts() const {
  QNetworkRequest alertsRequest;
  alertsRequest.setTransferTimeout(DEFAULT_CONNECTION_TIMEOUT_MS);
  alertsRequest.setUrl(getEndpointUrl(ENDPOINT_ALERTS.data()));
  const auto alertsReply = m_manager->get(alertsRequest);
  connect(alertsReply, &QNetworkReply::sslErrors, alertsReply,
          qOverload<>(&QNetworkReply::ignoreSslErrors));
  connect(alertsReply, &QNetworkReply::readyRead, [alertsReply, this]() {
    const auto status = alertsReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    const QString replyText = alertsReply->readAll();
    qDebug() << "Alerts Endpoint Response Status: " << status << "; Body: " << replyText;
    const QJsonObject rootObj = QJsonDocument::fromJson(replyText.toUtf8()).object();
    const auto alerts = rootObj.value("alerts").toArray();
    foreach (const auto alert, alerts) {
      if (alert.toObject().value("state") == "Active") {
        m_alertCount++;
      }
    }
    applyTrayIconNotificationBadge();
    alertsReply->deleteLater();
  });
  connect(alertsReply, &QNetworkReply::errorOccurred,
          [alertsReply](const QNetworkReply::NetworkError code) {
            const auto status =
                alertsReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
            qWarning() << "Error occurred connecting to Daemon Alerts endpoint. Status: " << status
                       << " QtErrorCode: " << code;
            alertsReply->deleteLater();
          });
}

void MainWindow::requestAllModes() const {
  QNetworkRequest modesRequest;
  modesRequest.setTransferTimeout(DEFAULT_CONNECTION_TIMEOUT_MS);
  modesRequest.setUrl(getEndpointUrl(ENDPOINT_MODES.data()));
  const auto modesReply = m_manager->get(modesRequest);
  connect(modesReply, &QNetworkReply::sslErrors, modesReply,
          qOverload<>(&QNetworkReply::ignoreSslErrors));
  connect(modesReply, &QNetworkReply::finished, [modesReply, this]() {
    const auto status = modesReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    const QString modesJson = modesReply->readAll();
    qDebug() << "Modes Endpoint Response Status: " << status << "; Body: " << modesJson;
    setTrayMenuModes(modesJson);
    modesReply->deleteLater();
  });
  connect(modesReply, &QNetworkReply::errorOccurred,
          [modesReply](const QNetworkReply::NetworkError code) {
            const auto status =
                modesReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
            qWarning() << "Error occurred connecting to Daemon Modes endpoint. Status: " << status
                       << " QtErrorCode: " << code;
            modesReply->deleteLater();
          });
}

void MainWindow::setTrayMenuModes(const QString& modesJson) const {
  const QJsonObject rootObj = QJsonDocument::fromJson(modesJson.toUtf8()).object();
  const auto modesArray = rootObj.value("modes").toArray();
  m_modesTrayMenu->setDisabled(modesArray.isEmpty());
  m_modesTrayMenu->clear();
  foreach (QJsonValue value, modesArray) {
    const auto modeName = value.toObject().value("name").toString();
    const auto modeUID = value.toObject().value("uid").toString();
    const auto modeAction = new QAction(modeName);
    modeAction->setStatusTip(modeUID);  // We use the statusTip to store UID
    modeAction->setCheckable(true);
    modeAction->setChecked(modeUID == m_activeModeUID);
    connect(modeAction, &QAction::triggered, [this, modeUID]() {
      QNetworkRequest setModeRequest;
      setModeRequest.setTransferTimeout(DEFAULT_CONNECTION_TIMEOUT_MS);
      auto url = getEndpointUrl(ENDPOINT_MODES_ACTIVE.data());
      url.setPath(url.path() + "/" + modeUID);
      setModeRequest.setUrl(url);
      const auto setModeReply = m_manager->post(setModeRequest, QByteArray());
      connect(setModeReply, &QNetworkReply::finished, [setModeReply, this]() {
        const auto status =
            setModeReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
        if (status == 401) {
          m_view->showNormal();  // show window if we have login credentials error
          qWarning() << "Authentication no longer valid when trying to apply Mode. Please login.";
        }
        if (status >= 300) {
          qWarning() << "Error trying to apply Mode. Response Status: " << status;
          setActiveMode(m_activeModeUID);
        }
        setModeReply->deleteLater();
      });
    });
    m_modesTrayMenu->addAction(modeAction);
  }
}

void MainWindow::setActiveMode(const QString& modeUID) const {
  m_activeModeUID = modeUID;
  foreach (QAction* action, m_modesTrayMenu->actions()) {
    if (action->statusTip() == m_activeModeUID) {
      action->setChecked(true);
    } else {
      action->setChecked(false);
    }
  }
}

void MainWindow::requestActiveMode() const {
  QNetworkRequest modesActiveRequest;
  modesActiveRequest.setTransferTimeout(DEFAULT_CONNECTION_TIMEOUT_MS);
  modesActiveRequest.setUrl(getEndpointUrl(ENDPOINT_MODES_ACTIVE.data()));
  const auto modesActiveReply = m_manager->get(modesActiveRequest);
  connect(modesActiveReply, &QNetworkReply::sslErrors, modesActiveReply,
          qOverload<>(&QNetworkReply::ignoreSslErrors));
  connect(modesActiveReply, &QNetworkReply::finished, [modesActiveReply, this]() {
    const auto status =
        modesActiveReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    const QString replyText = modesActiveReply->readAll();
    qDebug() << "ModesActive Endpoint Response Status: " << status << "; Body: " << replyText;
    const QJsonObject rootObj = QJsonDocument::fromJson(replyText.toUtf8()).object();
    setActiveMode(rootObj.value("current_mode_uid").toString());
    modesActiveReply->deleteLater();
  });
}

void MainWindow::startWatchingSSE() const {
  watchConnectionAndLogs();
  watchModeActivation();
  watchAlerts();
  watchNotifications();
}

void MainWindow::watchConnectionAndLogs() const {
  QNetworkRequest sseLogsRequest;
  sseLogsRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
                              QNetworkRequest::AlwaysNetwork);
  sseLogsRequest.setUrl(getEndpointUrl(ENDPOINT_SSE_LOGS.data(), false));
  const auto sseLogsReply = m_manager->get(sseLogsRequest);
  connect(sseLogsReply, &QNetworkReply::sslErrors, sseLogsReply,
          qOverload<>(&QNetworkReply::ignoreSslErrors));
  connect(this, &MainWindow::dropConnections, sseLogsReply, &QNetworkReply::abort,
          Qt::DirectConnection);
  connect(sseLogsReply, &QNetworkReply::readyRead, [sseLogsReply, this]() {
    // This is also called for keepAlive ticks - but with semi-filled message
    const QString log = sseLogsReply->readAll();
    if (const auto logContainsErrors = log.contains("ERROR");
        logContainsErrors && !m_daemonHasErrors) {
      m_daemonHasErrors = true;
      notifyDaemonErrors();
    }
    if (const auto logContainsWarnings = log.contains("WARN");
        logContainsWarnings && !m_daemonHasWarnings) {
      m_daemonHasWarnings = true;
    }
  });
  connect(sseLogsReply, &QNetworkReply::finished, [this, sseLogsReply]() {
    const auto status = sseLogsReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    qDebug() << "Log Watch SSE closed with status: " << status;
    if (status == 401) {
      qDebug() << "Log Watch SSE returned 401 - will retry after re-authentication.";
      sseLogsReply->deleteLater();
      return;
    }
    // on error or dropped connection will be re-connected once connection is re-established.
    if (!m_forceQuit && !m_changeAddress) {
      notifyDaemonDisconnected();
      applyTrayIconNotificationBadge(true);
      emit daemonConnectionLost();
      qInfo() << "Connection to the Daemon Lost";
    }
    sseLogsReply->deleteLater();
  });
}

void MainWindow::reestablishDaemonConnection() const {
  if (m_changeAddress) {
    return;
  }
  emit dropConnections();
  m_retryTimer->start();
}

void MainWindow::tryDaemonConnection() {
  QNetworkRequest healthRequest;
  healthRequest.setTransferTimeout(DEFAULT_CONNECTION_TIMEOUT_MS);
  healthRequest.setUrl(getEndpointUrl(ENDPOINT_HEALTH.data()));
  const auto healthReply = m_manager->get(healthRequest);
  connect(healthReply, &QNetworkReply::sslErrors, healthReply,
          qOverload<>(&QNetworkReply::ignoreSslErrors));
  qDebug() << "Attempting to establish connection to the daemon...";
  connect(healthReply, &QNetworkReply::readyRead, [this, healthReply]() {
    const auto status = healthReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    if (status == 401) {
      qDebug() << "Daemon connection returned 401 - waiting for authentication...";
      setAttribute(Qt::WidgetAttribute::WA_DontShowOnScreen, false);
      if (!m_loginWindowShown) {
        showNormal();  // show window once, if we have login credentials error
        m_loginWindowShown = true;
      }
      healthReply->deleteLater();
      return;
    }
    m_retryTimer->stop();
    if (m_startup) {
      requestDaemonErrors();
      requestAllAlerts();
      requestAllModes();
      requestActiveMode();
      emit watchForSSE();
      qInfo() << "Successfully connected to the Daemon";
      m_startup = false;
    } else {
      qInfo() << "Connection to the Daemon Reestablished";
      notifyDaemonConnectionRestored();
      // reset states on reconnection
      m_alertCount = 0;
      m_daemonHasErrors = false;
      m_daemonHasWarnings = false;
      m_loginWindowShown = false;
      if (isHidden()) {
        // if the window was closed/hidden/suspended, this will refresh the app
        // This is particularly helpful when closed to tray and the daemon reconnects
        m_view->load(getDaemonUrl());
      }
      // systray badge update will be handled by re-checking for Alerts and Errors:
      requestAllAlerts();
      requestDaemonErrors();
      emit watchForSSE();
    }
    healthReply->deleteLater();
  });
  connect(healthReply, &QNetworkReply::errorOccurred,
          [healthReply](const QNetworkReply::NetworkError code) {
            const auto status =
                healthReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
            qDebug() << "Error occurred establishing connection to Daemon. Status: " << status
                     << " QtErrorCode: " << code;
            healthReply->deleteLater();
          });
}

void MainWindow::watchModeActivation() const {
  QNetworkRequest sseModesRequest;
  sseModesRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
                               QNetworkRequest::AlwaysNetwork);
  sseModesRequest.setUrl(getEndpointUrl(ENDPOINT_SSE_MODES.data(), false));
  const auto sseModesReply = m_manager->get(sseModesRequest);
  connect(sseModesReply, &QNetworkReply::sslErrors, sseModesReply,
          qOverload<>(&QNetworkReply::ignoreSslErrors));
  connect(this, &MainWindow::dropConnections, sseModesReply, &QNetworkReply::abort,
          Qt::DirectConnection);
  connect(sseModesReply, &QNetworkReply::readyRead, [sseModesReply, this]() {
    const QString modeActivated =
        QString(sseModesReply->readAll()).simplified().replace("event: mode data: ", "");
    const QJsonObject rootObj = QJsonDocument::fromJson(modeActivated.toUtf8()).object();
    if (rootObj.isEmpty()) {
      // This is also called for keepAlive ticks - but semi-empty message
      return;
    }
    const auto currentModeUID = rootObj.value("uid").toString();
    const auto currentModeName = rootObj.value("name").toString();
    const auto modeAlreadyActive = currentModeUID == m_activeModeUID;
    setActiveMode(currentModeUID);
    if (m_activeModeUID.isEmpty()) {
      // This will happen if there is currently no active Mode (null)
      // - such as when applying a setting.
      return;
    }
    const auto msgTitle = modeAlreadyActive ? QString("Mode %1 Already Active").arg(currentModeName)
                                            : QString("Mode %1 Activated").arg(currentModeName);
    Notifier::send(msgTitle, "", 4);
  });
  connect(sseModesReply, &QNetworkReply::finished, [sseModesReply]() {
    // on error or dropped connection will be re-connected once connection is re-established.
    const auto status = sseModesReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    qDebug() << "Modes SSE closed with status: " << status;
    sseModesReply->deleteLater();
  });
}

void MainWindow::watchNotifications() const {
  QNetworkRequest notifyRequest;
  notifyRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
                             QNetworkRequest::AlwaysNetwork);
  notifyRequest.setUrl(getEndpointUrl(ENDPOINT_SSE_NOTIFICATIONS.data(), false));
  const auto notifyReply = m_manager->get(notifyRequest);
  connect(notifyReply, &QNetworkReply::sslErrors, notifyReply,
          qOverload<>(&QNetworkReply::ignoreSslErrors));
  connect(this, &MainWindow::dropConnections, notifyReply, &QNetworkReply::abort,
          Qt::DirectConnection);
  connect(notifyReply, &QNetworkReply::readyRead, [notifyReply]() {
    const QString raw =
        QString(notifyReply->readAll()).simplified().replace("event: notification data: ", "");
    const QJsonObject obj = QJsonDocument::fromJson(raw.toUtf8()).object();
    if (obj.isEmpty()) {
      // Keep-alive ticks produce semi-empty messages.
      return;
    }
    const auto title = obj.value("title").toString();
    const auto body = obj.value("body").toString();
    const auto iconStr = obj.value("icon").toString();
    const auto audio = obj.value("audio").toBool();
    const auto urgency = obj.value("urgency").toInt(1);
    // Map icon string to u8 matching NotificationIcon enum values.
    int iconNum = 4;  // default: info
    if (iconStr == "triggered") {
      iconNum = 1;
    } else if (iconStr == "resolved") {
      iconNum = 2;
    } else if (iconStr == "error") {
      iconNum = 3;
    } else if (iconStr == "shutdown") {
      iconNum = 5;
    }
    Notifier::send(title, body, iconNum, audio, urgency);
  });
  connect(notifyReply, &QNetworkReply::finished, [notifyReply]() {
    const auto status = notifyReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    qDebug() << "Notifications SSE closed with status: " << status;
    notifyReply->deleteLater();
  });
}

void MainWindow::watchAlerts() const {
  QNetworkRequest alertsRequest;
  alertsRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
                             QNetworkRequest::AlwaysNetwork);
  alertsRequest.setUrl(getEndpointUrl(ENDPOINT_SSE_ALERTS.data(), false));
  const auto alertsReply = m_manager->get(alertsRequest);
  connect(alertsReply, &QNetworkReply::sslErrors, alertsReply,
          qOverload<>(&QNetworkReply::ignoreSslErrors));
  connect(this, &MainWindow::dropConnections, alertsReply, &QNetworkReply::abort,
          Qt::DirectConnection);
  connect(alertsReply, &QNetworkReply::readyRead, [alertsReply, this]() {
    const QString alert =
        QString(alertsReply->readAll()).simplified().replace("event: alert data: ", "");
    const QJsonObject rootObj = QJsonDocument::fromJson(alert.toUtf8()).object();
    if (rootObj.isEmpty()) {
      // This is also called for keepAlive ticks - but semi-empty message
      return;
    }
    const auto alertState = rootObj.value("state").toString();
    const auto isActive = alertState == tr("Active");
    if (!isActive && m_alertCount > 0) {
      m_alertCount--;
    } else if (isActive) {
      m_alertCount++;
    }
    applyTrayIconNotificationBadge();
  });
  connect(alertsReply, &QNetworkReply::finished, [alertsReply]() {
    const auto status = alertsReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    qDebug() << "Alerts SSE closed with status: " << status;
    // on error or dropped connection will be re-connected once connection is re-established.
    alertsReply->deleteLater();
  });
}
