// Sane Break is a gentle break reminder that helps you avoid mindlessly skipping breaks
// Copyright (C) 2024-2026 Sane Break developers
// SPDX-License-Identifier: GPL-3.0-or-later

#include "app.h"

#include <qglobal.h>
#include <qlogging.h>

#include <QApplication>
#include <QDateTime>
#include <QMessageBox>
#include <QObject>
#include <QSqlDatabase>
#include <QString>
#include <QStyleHints>
#include <QTimer>
#include <QWindow>
#include <Qt>
#include <optional>

#include "app/break-windows.h"
#include "app/pref-window.h"
#include "app/tray.h"
#include "app/widgets/language-select.h"
#include "core/app-states.h"
#include "core/app.h"
#include "core/db.h"
#include "core/preferences.h"
#include "focus-window.h"
#include "idle/factory.h"
#include "lib/screen-lock.h"
#include "lib/system-monitor.h"
#include "lib/timer.h"
#include "meeting-prompt.h"
#include "meeting-window.h"
#include "postpone-window.h"
#include "stats-window.h"

#if (defined(Q_OS_LINUX) or defined(Q_OS_MACOS))
#include "app/unix/signal-handler.h"
#else
#include "app/windows/signal-handler.h"
#endif

#ifdef Q_OS_MACOS
#include "lib/macos/workspace.h"
#endif

SaneBreakApp::SaneBreakApp(const AppDependencies& deps, QObject* parent)
    : AbstractApp(deps, parent) {
  prefWindow = new PreferenceWindow(preferences);
  tray = StatusTrayWindow::createTrayOrWindow(preferences, this);

  connect(this, &SaneBreakApp::trayDataUpdated, tray, &StatusTrayWindow::update);
  connect(tray, &StatusTrayWindow::nextBreakRequested, this, &SaneBreakApp::breakNow);
  connect(tray, &StatusTrayWindow::nextBigBreakRequested, this,
          &SaneBreakApp::bigBreakNow);
  connect(tray, &StatusTrayWindow::smallBreakInsteadRequested, this,
          &SaneBreakApp::smallBreakInstead);
  connect(tray, &StatusTrayWindow::postponeRequested, this,
          [this]() { openPostponeWindow(std::nullopt); });
  connect(tray, &StatusTrayWindow::postponePresetRequested, this,
          [this](int minutes) { openPostponeWindow(minutes); });
  connect(tray, &StatusTrayWindow::meetingRequested, this,
          [this]() { openMeetingWindow(std::nullopt); });
  connect(tray, &StatusTrayWindow::meetingPresetRequested, this,
          [this](int minutes) { openMeetingWindow(minutes); });
  connect(tray, &StatusTrayWindow::focusRequested, this,
          [this]() { openFocusWindow(std::nullopt); });
  connect(tray, &StatusTrayWindow::focusPresetRequested, this,
          [this](int minutes) { openFocusWindow(minutes); });
  connect(tray, &StatusTrayWindow::endFocusRequested, this, &SaneBreakApp::endFocus);
  connect(tray, &StatusTrayWindow::endMeetingRequested, this,
          [this]() { endMeetingBreakLater(preferences->smallEvery->get()); });
  connect(tray, &StatusTrayWindow::extendMeetingRequested, this,
          &SaneBreakApp::extendMeeting);
  connect(tray, &StatusTrayWindow::preferenceWindowRequested, this,
          &SaneBreakApp::showPreferences);
  connect(tray, &StatusTrayWindow::statsRequested, this,
          &SaneBreakApp::openStatsWindow);
  connect(tray, &StatusTrayWindow::enableBreakRequested, this,
          &SaneBreakApp::enableBreak);
  connect(tray, &StatusTrayWindow::quitRequested, this, &SaneBreakApp::confirmQuit);
  connect(preferences->language, &SettingWithSignal::changed, this,
          [this]() { LanguageSelect::setLanguage(preferences->language->get()); });

  connect(this, &SaneBreakApp::quit, qApp, &QApplication::quit, Qt::QueuedConnection);
  connect(qApp, &QCoreApplication::aboutToQuit, this, &SaneBreakApp::onExit);
#if (defined(Q_OS_LINUX) or defined(Q_OS_MACOS))
  auto signalHandler = new SignalHandler(this);
  signalHandler->setup();
  connect(signalHandler, &SignalHandler::exitRequested, this, &QCoreApplication::quit);
#endif
#ifdef Q_OS_WINDOWS
  qApp->installNativeEventFilter(new SignalHandler());
#endif
}

SaneBreakApp* SaneBreakApp::create(SanePreferences* preferences, QObject* parent) {
  AppDependencies deps = {
      .preferences = preferences,
      .db = new BreakDatabase(QSqlDatabase::addDatabase("QSQLITE")),
      .countDownTimer = new Timer(),
      .screenLockTimer = new Timer(),
      .idleTimer = createIdleTimer(parent),
      .systemMonitor = new SystemMonitor(preferences),
      .breakWindows = new BreakWindows(),
      .meetingPrompt = new MeetingPrompt(parent, preferences),
  };
  return new SaneBreakApp(deps, parent);
}

void SaneBreakApp::start() {
  AbstractApp::start();
  tray->show();
}

void SaneBreakApp::doLockScreen() { lockScreen(); }

static void showAndActivate(QWidget* window) {
  window->show();
  window->raise();
  window->activateWindow();
#ifdef Q_OS_MACOS
  macForceActivation();
#endif
}

void SaneBreakApp::showPreferences() { showAndActivate(prefWindow); }

void SaneBreakApp::openStatsWindow() {
  if (!statsWindow) statsWindow = new StatsWindow(db);
  showAndActivate(statsWindow);
}

void SaneBreakApp::openFocusWindow(std::optional<int> presetMinutes) {
  if (data->focus().isActive() || m_currentState->getID() == AppState::Meeting ||
      data->schedule().isPostponing())
    return;
  if (!focusWindow) {
    focusWindow = new FocusWindow(preferences);
    connect(focusWindow, &FocusWindow::focusRequested, this, &SaneBreakApp::startFocus);
  }
  if (presetMinutes.has_value()) {
    focusWindow->setMinutes(presetMinutes.value());
  } else {
    int defaultMinutes = preferences->focusSmallEvery->get() / 60 * 2;
    focusWindow->setMinutes(defaultMinutes);
  }
  showAndActivate(focusWindow);
}

void SaneBreakApp::openPostponeWindow(std::optional<int> presetMinutes) {
  if (data->focus().isActive()) {
    QMessageBox msgBox;
    msgBox.setText(tr("Cannot postpone during focus mode."));
    msgBox.setInformativeText(tr("End focus mode first if you want to postpone."));
    msgBox.setIcon(QMessageBox::Icon::Warning);
    msgBox.addButton(QMessageBox::Ok)->setText(tr("OK"));
    msgBox.exec();
    return;
  }
  if (data->schedule().isPostponing()) {
    QMessageBox msgBox;
    msgBox.setText(tr("You have already postponed this break once."));
    msgBox.setInformativeText(tr("No further postpones are allowed."));
    msgBox.setIcon(QMessageBox::Icon::Warning);
    msgBox.addButton(QMessageBox::Ok)->setText(tr("OK"));
    msgBox.setDefaultButton(QMessageBox::Cancel);
    msgBox.exec();
    return;
  }
  if (!postponeWindow) {
    postponeWindow = new PostponeWindow(preferences, db);
    connect(postponeWindow, &PostponeWindow::postponeRequested, this,
            &SaneBreakApp::postpone);
  }
  postponeWindow->setMinutes(presetMinutes.value_or(0));
  showAndActivate(postponeWindow);
}

void SaneBreakApp::openMeetingWindow(std::optional<int> presetMinutes) {
  if (m_currentState->getID() == AppState::Meeting) return;
  if (!meetingWindow) {
    meetingWindow = new MeetingWindow(preferences, db);
    connect(meetingWindow, &MeetingWindow::meetingRequested, this,
            [this](QTime endTime, QString reason) {
              QDateTime now = QDateTime::currentDateTime();
              auto resolvedEndTime = MeetingTime::resolveEndDateTime(now, endTime);
              if (!resolvedEndTime) return;
              int seconds = now.secsTo(*resolvedEndTime);
              if (seconds > 0) startMeeting(seconds, reason);
            });
  }
  int minutes = presetMinutes.value_or(30);
  QTime endTime = QTime::currentTime().addSecs(minutes * 60);
  meetingWindow->setEndTime(endTime);
  showAndActivate(meetingWindow);
}

void SaneBreakApp::confirmQuit() {
  QMessageBox msgBox;
  msgBox.setText(tr("Are you sure to quit Sane Break?"));
  msgBox.setInformativeText(tr("You can postpone the breaks instead."));
  msgBox.setIcon(QMessageBox::Icon::Question);
  msgBox.addButton(QMessageBox::Cancel)->setText(tr("Cancel"));
  msgBox.addButton(tr("Postpone"), QMessageBox::NoRole);
  msgBox.addButton(QMessageBox::Yes)->setText(tr("Yes"));

  msgBox.setDefaultButton(QMessageBox::Cancel);
  switch (msgBox.exec()) {
    case QMessageBox::Yes:
      emit quit();
      return;
    case QMessageBox::Cancel:
      return;
    default:
      openPostponeWindow();
      return;
  }
}
