/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *                                                                         *
 *  Copyright (C) 2017-2018 Simon Stürz <simon.stuerz@guh.io               *
 *                                                                         *
 *  This file is part of nymea.                                            *
 *                                                                         *
 *  This library is free software; you can redistribute it and/or          *
 *  modify it under the terms of the GNU Lesser General Public             *
 *  License as published by the Free Software Foundation; either           *
 *  version 2.1 of the License, or (at your option) any later version.     *
 *                                                                         *
 *  This library 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      *
 *  Lesser General Public License for more details.                        *
 *                                                                         *
 *  You should have received a copy of the GNU Lesser General Public       *
 *  License along with this library; If not, see                           *
 *  <http://www.gnu.org/licenses/>.                                        *
 *                                                                         *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "snapdcontrol.h"
#include "extern-plugininfo.h"

#include <QFileInfo>
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonParseError>

SnapdControl::SnapdControl(Device *device, QObject *parent) :
    QObject(parent),
    m_device(device),
    m_snapdSocketPath("/run/snapd.socket")
{
    // If a change is one of following kind, the plugin will recognize it as update running
    m_updateChangeKinds.append("install-snap");
    m_updateChangeKinds.append("remove-snap");
    m_updateChangeKinds.append("refresh-snap");
    m_updateChangeKinds.append("revert-snap");

    m_snapConnection = new SnapdConnection(this);
    connect(m_snapConnection, &SnapdConnection::connectedChanged, this, &SnapdControl::onConnectedChanged);
}

Device *SnapdControl::device()
{
    return m_device;
}

bool SnapdControl::available() const
{
    QFileInfo fileInfo(m_snapdSocketPath);
    if (!fileInfo.exists()) {
        qCDebug(dcSnapd()) << "The socket descriptor" << m_snapdSocketPath << "does not exist";
        return false;
    }

    if (!fileInfo.isReadable()) {
        qCDebug(dcSnapd()) << "The socket descriptor" << m_snapdSocketPath << "is not readable";
        return false;
    }

    if (!fileInfo.isWritable()) {
        qCDebug(dcSnapd()) << "The socket descriptor" << m_snapdSocketPath << "is not writable";
        return false;
    }

    return true;
}

bool SnapdControl::connected() const
{
    return m_snapConnection->isConnected();
}

bool SnapdControl::enabled() const
{
    return m_enabled;
}

bool SnapdControl::timerBasedSchedule() const
{
    return m_timerBasedSchedule;
}

void SnapdControl::loadSystemInfo()
{
    if (!m_snapConnection)
        return;

    if (!m_snapConnection->isConnected())
        return;

    SnapdReply *reply = m_snapConnection->get("/v2/system-info", this);
    connect(reply, &SnapdReply::finished, this, &SnapdControl::onLoadSystemInfoFinished);
}

void SnapdControl::loadSnapList()
{
    if (!m_snapConnection)
        return;

    if (!m_snapConnection->isConnected())
        return;

    SnapdReply *reply = m_snapConnection->get("/v2/snaps", this);
    connect(reply, &SnapdReply::finished, this, &SnapdControl::onLoadSnapListFinished);
}

void SnapdControl::loadRunningChanges()
{
    if (!m_snapConnection)
        return;

    if (!m_snapConnection->isConnected())
        return;

    SnapdReply *reply = m_snapConnection->get("/v2/changes", this);
    connect(reply, &SnapdReply::finished, this, &SnapdControl::onLoadRunningChangesFinished);
}

void SnapdControl::configureRefreshSchedule()
{
    if (!m_snapConnection)
        return;

    if (!m_snapConnection->isConnected())
        return;

    QVariantMap configuration; QVariantMap configMap;
    configMap.insert("timer", m_preferredRefreshSchedule);
    configMap.insert("schedule", m_preferredRefreshSchedule);
    configuration.insert("refresh", configMap);

    qCDebug(dcSnapd()) << "Configure refresh schedule from" << m_currentRefreshSchedule << "-->" << m_preferredRefreshSchedule;

    SnapdReply *reply = m_snapConnection->put(QString("/v2/snaps/core/conf"), QJsonDocument::fromVariant(configuration).toJson(QJsonDocument::Compact), this);
    connect(reply, &SnapdReply::finished, this, &SnapdControl::onConfigureRefreshScheduleFinished);
}

bool SnapdControl::validAsyncResponse(const QVariantMap &responseMap)
{
    if (!responseMap.contains("type"))
        return false;

    if (responseMap.value("type") == "error")
        return false;

    return true;
}

void SnapdControl::onConnectedChanged(const bool &connected)
{
    if (connected) {
        device()->setStateValue(snapdControlSnapdAvailableStateTypeId, true);
        update();
    } else {
        device()->setStateValue(snapdControlSnapdAvailableStateTypeId, false);
    }
}

void SnapdControl::onLoadSystemInfoFinished()
{
    SnapdReply *reply = static_cast<SnapdReply *>(sender());
    if (!reply->isValid()) {
        qCDebug(dcSnapd()) << "Load system info request finished with error" << reply->requestPath();
        reply->deleteLater();
        return;
    }

    QVariantMap result = reply->dataMap().value("result").toMap();
    QDateTime lastRefreshTime = QDateTime::fromString(result.value("refresh").toMap().value("last").toString(), Qt::ISODate);
    QDateTime nextRefreshTime = QDateTime::fromString(result.value("refresh").toMap().value("next").toString(), Qt::ISODate);

    // Set update time information
    device()->setStateValue(snapdControlLastUpdateTimeStateTypeId, lastRefreshTime.toTime_t());
    device()->setStateValue(snapdControlNextUpdateTimeStateTypeId, nextRefreshTime.toTime_t());

    // Check if we are working on refresh timer or refresh schedule
    if (result.value("refresh").toMap().contains("schedule")) {
        // Schedule based core snap
        m_timerBasedSchedule = false;
        m_currentRefreshSchedule = result.value("refresh").toMap().value("schedule").toString();
    } else if (result.value("refresh").toMap().contains("timer")) {
        // Timer based core snap: snapd >= 2.31
        m_timerBasedSchedule = true;
        m_currentRefreshSchedule = result.value("refresh").toMap().value("timer").toString();
    }

    reply->deleteLater();

    // Check if the refresh schedule should be updated
    if (m_currentRefreshSchedule != m_preferredRefreshSchedule) {
        configureRefreshSchedule();
    }
}

void SnapdControl::onLoadSnapListFinished()
{
    SnapdReply *reply = static_cast<SnapdReply *>(sender());
    if (!reply->isValid()) {
        qCDebug(dcSnapd()) << "Load system info request finished with error" << reply->requestPath();
        reply->deleteLater();
        return;
    }

    emit snapListUpdated(reply->dataMap().value("result").toList());
    reply->deleteLater();
}

void SnapdControl::onLoadRunningChangesFinished()
{
    SnapdReply *reply = static_cast<SnapdReply *>(sender());
    if (!reply->isValid()) {
        qCDebug(dcSnapd()) << "Load running changes request finished with error" << reply->requestPath();
        reply->deleteLater();
        return;
    }

    // Load changes list
    QVariantList changes = reply->dataMap().value("result").toList();
    reply->deleteLater();

    // If there are no running changes, update is not running
    if (changes.isEmpty()) {
        // Update not running any more
        device()->setStateValue(snapdControlUpdateRunningStateTypeId, false);
        device()->setStateValue(snapdControlStatusStateTypeId, "-");
        return;
    }

    bool updateRunning = false;
    QString updateStatus = "-";

    // Verifiy if a change is running and which one is currently doing something
    foreach (const QVariant &changeVariant, changes) {
        QVariantMap changeMap = changeVariant.toMap();

        int changeId = changeMap.value("id").toInt();
        bool changeReady = changeMap.value("ready").toBool();
        QString changeKind = changeMap.value("kind").toString();
        QString changeStatus = changeMap.value("status").toString();
        QString changeSummary = changeMap.value("summary").toString();

        // If there is a change kind "Doing" or "Do"
        if ( (changeStatus == "Doing" || changeStatus == "Do") && m_updateChangeKinds.contains(changeKind)) {
            // Set the status of the current running change
            updateRunning = true;
            if (changeStatus == "Doing") {
                updateStatus = changeSummary;
                qCDebug(dcSnapd()).noquote() << "Current change:" << changeId << (changeReady ? "ready" : "not ready") << changeStatus << changeKind << changeSummary;

                // TODO: calculate the overall percentage of the update process (Do + Doing / Done)
            }
        }
    }

    device()->setStateValue(snapdControlUpdateRunningStateTypeId, updateRunning);
    device()->setStateValue(snapdControlStatusStateTypeId, updateStatus);

    // If currently an update is running, lets force update available to false if set to true
    // keep this below the setStateValue for updateRunning or we might have intermediate states indicating
    // no update running and none available
    if (updateRunning && device()->stateValue(snapdControlUpdateAvailableStateTypeId).toBool()) {
        device()->setStateValue(snapdControlUpdateAvailableStateTypeId, false);
    }

}

void SnapdControl::onConfigureRefreshScheduleFinished()
{
    SnapdReply *reply = static_cast<SnapdReply *>(sender());
    if (!reply->isValid()) {
        qCDebug(dcSnapd()) << "Set refresh schedule request finished with error" << reply->requestPath();
        reply->deleteLater();
        return;
    }

    if (!validAsyncResponse(reply->dataMap())) {
        qCWarning(dcSnapd()) << "Async refresh configuration request finished with error" << reply->dataMap().value("status").toString() << reply->dataMap().value("status-code").toInt();
        reply->deleteLater();
        return;
    }

    qCDebug(dcSnapd()) << "Configure refresh schedule finished successfully";
    reply->deleteLater();
}

void SnapdControl::onSnapRefreshFinished()
{
    SnapdReply *reply = static_cast<SnapdReply *>(sender());
    if (!reply->isValid()) {
        qCDebug(dcSnapd()) << "Snap refresh request finished with error" << reply->requestPath();
        reply->deleteLater();
        return;
    }

    if (!validAsyncResponse(reply->dataMap())) {
        qCWarning(dcSnapd()) << "Async refresh request finished with error" << reply->dataMap().value("status").toString() << reply->dataMap().value("status-code").toInt();
    } else {
        loadRunningChanges();
    }

    reply->deleteLater();
}

void SnapdControl::onSnapRevertFinished()
{
    SnapdReply *reply = static_cast<SnapdReply *>(sender());
    if (!reply->isValid()) {
        qCDebug(dcSnapd()) << "Snap revert request finished with error" << reply->requestPath();
        reply->deleteLater();
        return;
    }

    if (!validAsyncResponse(reply->dataMap())) {
        qCWarning(dcSnapd()) << "Async change request finished with error" << reply->dataMap().value("status").toString() << reply->dataMap().value("status-code").toInt();;
    } else {
        loadRunningChanges();
    }

    reply->deleteLater();
}

void SnapdControl::onCheckForUpdatesFinished()
{
    SnapdReply *reply = static_cast<SnapdReply *>(sender());
    if (!reply->isValid()) {
        qCDebug(dcSnapd()) << "Check for snap updates request finished with error" << reply->requestPath();
        reply->deleteLater();
        return;
    }

    qCDebug(dcSnapd()) << "Check for available snap updates finished.";
    if (reply->dataMap().value("result").toList().isEmpty()) {
        qCDebug(dcSnapd()) << "There are no snap updates available.";
        device()->setStateValue(snapdControlUpdateAvailableStateTypeId, false);
    } else {
        // Print available snap updates
        qCDebug(dcSnapd()) << "Following snaps can be updated:";
        foreach (const QVariant &resultVariant, reply->dataMap().value("result").toList()) {
            QVariantMap resultMap = resultVariant.toMap();
            qCDebug(dcSnapd()) << "    -->" << resultMap.value("name").toString() << resultMap.value("version").toString();
        }

        device()->setStateValue(snapdControlUpdateAvailableStateTypeId, true);
    }

    reply->deleteLater();
}

void SnapdControl::onChangeSnapChannelFinished()
{
    SnapdReply *reply = static_cast<SnapdReply *>(sender());
    if (!reply->isValid()) {
        qCDebug(dcSnapd()) << "Change snap channel request finished with error" << reply->requestPath();
        reply->deleteLater();
        return;
    }

    if (!validAsyncResponse(reply->dataMap())) {
        qCWarning(dcSnapd()) << "Async change request finished with error" << reply->dataMap().value("status").toString() << reply->dataMap().value("status-code").toInt();
    } else {
        loadRunningChanges();
    }

    reply->deleteLater();
}

void SnapdControl::enable()
{
    m_enabled = true;
    update();
}

void SnapdControl::disable()
{
    m_enabled = false;

    if (m_snapConnection) {
        m_snapConnection->close();
    }
}

void SnapdControl::update()
{
    if (!m_snapConnection)
        return;

    if (!available())
        return;

    if (!enabled())
        return;

    // Try to reconnect if unconnected
    if (m_snapConnection->state() == QLocalSocket::UnconnectedState) {
        m_snapConnection->connectToServer(m_snapdSocketPath, QLocalSocket::ReadWrite);
        return;
    }

    // Note: this makes sure the state is really connected (including connection initialisation stuff)
    if (!m_snapConnection->isConnected())
        return;

    // Update information
    if (device()->stateValue(snapdControlUpdateRunningStateTypeId).toBool()) {
        // Note: if an update is running, just load the changes to save system resources
        loadRunningChanges();
    } else {
        // Normal update
        loadSystemInfo();
        loadSnapList();
        loadRunningChanges();
    }
}

void SnapdControl::snapRefresh()
{
    if (!m_snapConnection)
        return;

    if (!m_snapConnection->isConnected())
        return;

    QVariantMap request;
    request.insert("action", "refresh");

    qCDebug(dcSnapd()) << "Refresh all snaps";
    SnapdReply *reply = m_snapConnection->post("/v2/snaps", QJsonDocument::fromVariant(request).toJson(QJsonDocument::Compact), this);
    connect(reply, &SnapdReply::finished, this, &SnapdControl::onSnapRefreshFinished);
}

void SnapdControl::changeSnapChannel(const QString &snapName, const QString &channel)
{
    if (!m_snapConnection)
        return;

    if (!m_snapConnection->isConnected())
        return;

    QVariantMap request;
    request.insert("action", "refresh");
    request.insert("channel", channel);

    qCDebug(dcSnapd()) << "Refresh snap" << snapName << "to channel" << channel;
    SnapdReply *reply = m_snapConnection->post(QString("/v2/snaps/%1").arg(snapName), QJsonDocument::fromVariant(request).toJson(QJsonDocument::Compact), this);
    connect(reply, &SnapdReply::finished, this, &SnapdControl::onChangeSnapChannelFinished);
}

void SnapdControl::checkForUpdates()
{
    if (!m_snapConnection)
        return;

    if (!m_snapConnection->isConnected())
        return;

    qCDebug(dcSnapd()) << "Checking for available snap updates";
    SnapdReply *reply = m_snapConnection->get("/v2/find?select=refresh", this);
    connect(reply, &SnapdReply::finished, this, &SnapdControl::onCheckForUpdatesFinished);
}

void SnapdControl::setPreferredRefreshTime(int startTime)
{
    // Schedule the refresh between startTime and startTime + 59 minutes
    QTime start(startTime, 0, 0);
    QTime end = start.addSecs(3540);
    m_preferredRefreshSchedule  = QString("%1-%2").arg(start.toString("h:mm")).arg(end.toString("h:mm"));
    qCDebug(dcSnapd()) << "Set preferred refresh schedule to " << m_preferredRefreshSchedule;
}

void SnapdControl::snapRevert(const QString &snapName)
{
    if (!m_snapConnection)
        return;

    if (!m_snapConnection->isConnected())
        return;

    QVariantMap request;
    request.insert("action", "revert");

    qCDebug(dcSnapd()) << "Revert snap" << snapName;
    SnapdReply *reply = m_snapConnection->post(QString("/v2/snaps/%1").arg(snapName), QJsonDocument::fromVariant(request).toJson(QJsonDocument::Compact), this);
    connect(reply, &SnapdReply::finished, this, &SnapdControl::onSnapRevertFinished);
}
