#include "./syncthingnotifier.h"
#include "./syncthingconnection.h"
#include "./syncthingprocess.h"
#include "./utils.h"

#include "resources/config.h"

#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD
#include "./syncthingservice.h"
#endif

#include <c++utilities/chrono/datetime.h>
#include <c++utilities/io/ansiescapecodes.h>

#include <iostream>

using namespace CppUtilities;

namespace Data {

/*!
 * \class SyncthingNotifier
 * \brief The SyncthingNotifier class emits high-level notification for a given SyncthingConnection.
 *
 * In contrast to the signals provided by the SyncthingConnection class, these signals take further apply
 * further logic and take additional information into account (previous status, service status if known, ...).
 */

/*!
 * \brief Constructs a new SyncthingNotifier instance for the specified \a connection.
 * \remarks Use setEnabledNotifications() to enable notifications (only statusChanged() is always emitted).
 */
SyncthingNotifier::SyncthingNotifier(const SyncthingConnection &connection, QObject *parent)
    : QObject(parent)
    , m_connection(connection)
#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD
    , m_service(SyncthingService::mainInstance())
#endif
    , m_process(SyncthingProcess::mainInstance())
    , m_enabledNotifications(SyncthingHighLevelNotification::None)
    , m_consideredIntegrations(SyncthingStartupIntegration::None)
    , m_previousStatus(SyncthingStatus::Disconnected)
    , m_ignoreInavailabilityAfterStart(15)
    , m_initialized(false)
    , m_logOnStderr(
          qEnvironmentVariableIntValue(PROJECT_VARNAME_UPPER "_LOG_ALL") || qEnvironmentVariableIntValue(PROJECT_VARNAME_UPPER "_LOG_NOTIFICATIONS"))
{
    connect(&connection, &SyncthingConnection::statusChanged, this, &SyncthingNotifier::handleStatusChangedEvent);
    connect(&connection, &SyncthingConnection::dirCompleted, this, &SyncthingNotifier::emitSyncComplete);
    connect(&connection, &SyncthingConnection::newDevAvailable, this, &SyncthingNotifier::handleNewDevEvent);
    connect(&connection, &SyncthingConnection::newDirAvailable, this, &SyncthingNotifier::handleNewDirEvent);
    if (m_process) {
        connect(m_process, &SyncthingProcess::errorOccurred, this, &SyncthingNotifier::handleSyncthingProcessError);
    }
}

void SyncthingNotifier::handleStatusChangedEvent(SyncthingStatus newStatus)
{
    // skip redundant status updates
    if (m_initialized && m_previousStatus == newStatus) {
        return;
    }

    // emit signals
    emit statusChanged(m_previousStatus, newStatus);
    emitConnectedAndDisconnected(newStatus);

    // update status variables
    m_initialized = true;
    m_previousStatus = newStatus;
}

void SyncthingNotifier::handleNewDevEvent(DateTime when, const QString &devId, const QString &address)
{
    CPP_UTILITIES_UNUSED(when)

    // ignore if not enabled
    if (!(m_enabledNotifications && SyncthingHighLevelNotification::NewDevice)) {
        return;
    }

    emit newDevice(devId, log(tr("Device %1 (%2) wants to connect.").arg(devId, address)));
}

void SyncthingNotifier::handleNewDirEvent(DateTime when, const QString &devId, const SyncthingDev *dev, const QString &dirId, const QString &dirLabel)
{
    CPP_UTILITIES_UNUSED(when)

    // ignore if not enabled
    if (!(m_enabledNotifications && SyncthingHighLevelNotification::NewDir)) {
        return;
    }

    // format message
    const auto message([&devId, dev, &dirId, &dirLabel] {
        const auto devPrefix(dev ? (tr("Device ") + dev->displayName()) : (tr("Unknown device ") + devId));
        if (dirLabel.isEmpty()) {
            return devPrefix + tr(" wants to share folder %1.").arg(dirId);
        } else {
            return devPrefix + tr(" wants to share folder %1 (%2).").arg(dirLabel, dirId);
        }
    }());
    emit newDir(devId, dirId, dirLabel, log(message));
}

void SyncthingNotifier::handleSyncthingProcessError(QProcess::ProcessError processError)
{
    if (!(m_enabledNotifications && SyncthingHighLevelNotification::SyncthingProcessError)) {
        return;
    }

    const auto error = m_process->errorString();
    switch (processError) {
    case QProcess::FailedToStart:
        emit syncthingProcessError(tr("Failed to start Syncthing"),
            error.isEmpty() ? tr("Maybe the configured binary path is wrong or the binary is not marked as executable.") : error);
        break;
    case QProcess::Crashed:
        emit syncthingProcessError(tr("Syncthing crashed"), error);
        break;
    default:
        emit syncthingProcessError(tr("Syncthing launcher error occurred"), error);
    }
}

/*!
 * \brief Returns whether a "disconnected" notification should be shown.
 */
bool SyncthingNotifier::isDisconnectRelevant() const
{
    // skip disconnect if not initialized
    if (!m_initialized) {
        return false;
    }

    // skip further considerations if connection is remote
    if (!m_connection.isLocal()) {
        return true;
    }

    // consider process/launcher or systemd unit status
    if ((m_consideredIntegrations && SyncthingStartupIntegration::Process) && m_process && m_process->isManuallyStopped()) {
        return false;
    }
#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD
    if ((m_consideredIntegrations && SyncthingStartupIntegration::Service) && m_service && m_service->isManuallyStopped()) {
        return false;
    }
#endif

    // ignore inavailability after start or standby-wakeup
    if (m_ignoreInavailabilityAfterStart) {
        if (((m_consideredIntegrations && SyncthingStartupIntegration::Process) && m_process && m_process->isRunning())
#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD
            && (((m_consideredIntegrations && SyncthingStartupIntegration::Service) && m_service && m_service->isSystemdAvailable()
                    && !m_service->isActiveWithoutSleepFor(m_process->activeSince(), m_ignoreInavailabilityAfterStart))
                || !m_process->isActiveWithoutSleepFor(m_ignoreInavailabilityAfterStart))
#else
            && !m_process->isActiveWithoutSleepFor(m_ignoreInavailabilityAfterStart)
#endif
        ) {
            return false;
        }
#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD
        if ((m_consideredIntegrations && SyncthingStartupIntegration::Service) && m_service && m_service->isRunning()
            && !m_service->isActiveWithoutSleepFor(m_ignoreInavailabilityAfterStart)) {
            return false;
        }
#endif
    }

    return true;
}

/*!
 * \brief Emits the connected() or disconnected() signal.
 */
void SyncthingNotifier::emitConnectedAndDisconnected(SyncthingStatus newStatus)
{
    // discard event if not enabled
    if (!(m_enabledNotifications && SyncthingHighLevelNotification::ConnectedDisconnected)) {
        return;
    }

    switch (newStatus) {
    case SyncthingStatus::Disconnected:
        if (isDisconnectRelevant()) {
            emit disconnected();
        }
        break;
    default:
        switch (m_previousStatus) {
        case SyncthingStatus::Disconnected:
        case SyncthingStatus::Reconnecting:
            emit connected();
            break;
        default:;
        }
    }
}

/*!
 * \brief Emits the syncComplete() signal.
 */
void SyncthingNotifier::emitSyncComplete(CppUtilities::DateTime when, const SyncthingDir &dir, int index, const SyncthingDev *remoteDev)
{
    CPP_UTILITIES_UNUSED(when)
    CPP_UTILITIES_UNUSED(index)

    // discard event for paused directories/devices
    if (dir.paused || (remoteDev && remoteDev->paused)) {
        return;
    }

    // discard event if not enabled
    if (!m_initialized || (!remoteDev && !(m_enabledNotifications && SyncthingHighLevelNotification::LocalSyncComplete))
        || (remoteDev && !(m_enabledNotifications && SyncthingHighLevelNotification::RemoteSyncComplete))) {
        return;
    }

    // format the notification message
    const auto message(syncCompleteString(std::vector<const SyncthingDir *>{ &dir }, remoteDev));
    if (!message.isEmpty()) {
        emit syncComplete(log(message));
    }
}

const QString &SyncthingNotifier::log(const QString &message)
{
    if (m_logOnStderr) {
        std::cerr << EscapeCodes::Phrases::Info << message.toStdString() << EscapeCodes::Phrases::End;
    }
    return message;
}

} // namespace Data
