/*
 * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "ocsuserstatusconnector.h"
#include "account.h"
#include "userstatusconnector.h"

#include <networkjobs.h>

#include <QDateTime>
#include <QtGlobal>
#include <QJsonDocument>
#include <QJsonValue>
#include <QLoggingCategory>
#include <QString>
#include <QJsonObject>
#include <QJsonArray>
#include <qdatetime.h>
#include <qjsonarray.h>
#include <qjsonobject.h>
#include <qloggingcategory.h>

using namespace Qt::StringLiterals;

namespace {

Q_LOGGING_CATEGORY(lcOcsUserStatusConnector, "nextcloud.gui.ocsuserstatusconnector", QtInfoMsg)

OCC::UserStatus::OnlineStatus stringToUserOnlineStatus(const QString &status)
{
    // it needs to match the Status enum
    const QHash<QString, OCC::UserStatus::OnlineStatus> preDefinedStatus {
        { "online", OCC::UserStatus::OnlineStatus::Online },
        { "dnd", OCC::UserStatus::OnlineStatus::DoNotDisturb },
        { "away", OCC::UserStatus::OnlineStatus::Away },
        { "busy", OCC::UserStatus::OnlineStatus::Busy },
        { "offline", OCC::UserStatus::OnlineStatus::Offline },
        { "invisible", OCC::UserStatus::OnlineStatus::Invisible }
    };

    // api should return invisible, dnd,... toLower() it is to make sure
    // it matches _preDefinedStatus, otherwise the default is online (0)
    return preDefinedStatus.value(status.toLower(), OCC::UserStatus::OnlineStatus::Online);
}

QString onlineStatusToString(OCC::UserStatus::OnlineStatus status)
{
    switch (status) {
    case OCC::UserStatus::OnlineStatus::Online:
        return QStringLiteral("online");
    case OCC::UserStatus::OnlineStatus::DoNotDisturb:
        return QStringLiteral("dnd");
    case OCC::UserStatus::OnlineStatus::Away:
        return QStringLiteral("away");
    case OCC::UserStatus::OnlineStatus::Busy:
        return QStringLiteral("busy");
    case OCC::UserStatus::OnlineStatus::Offline:
        return QStringLiteral("offline");
    case OCC::UserStatus::OnlineStatus::Invisible:
        return QStringLiteral("invisible");
    }
    return QStringLiteral("online");
}

OCC::Optional<OCC::ClearAt> jsonExtractClearAt(const QJsonObject &jsonObject)
{
    OCC::Optional<OCC::ClearAt> clearAt {};
    if (jsonObject.contains("clearAt"_L1) && !jsonObject.value("clearAt"_L1).isNull()) {
        OCC::ClearAt clearAtValue;
        clearAtValue._type = OCC::ClearAtType::Timestamp;
        clearAtValue._timestamp = jsonObject.value("clearAt"_L1).toInt();
        clearAt = clearAtValue;
    }
    return clearAt;
}

OCC::UserStatus jsonExtractUserStatus(const QJsonObject &json)
{
    const auto clearAt = jsonExtractClearAt(json);

    const OCC::UserStatus userStatus(json.value("messageId"_L1).toString(),
        json.value("message"_L1).toString().trimmed(),
        json.value("icon"_L1).toString().trimmed(), stringToUserOnlineStatus(json.value("status"_L1).toString()),
        json.value("messageIsPredefined"_L1).toBool(false), clearAt);

    return userStatus;
}

OCC::UserStatus jsonToUserStatus(const QJsonDocument &json)
{
    const QJsonObject defaultValues {
        { "icon", "" },
        { "message", "" },
        { "status", "online" },
        { "messageIsPredefined", "false" },
        { "statusIsUserDefined", "false" }
    };
    const auto retrievedData = json.object().value("ocs"_L1).toObject().value("data"_L1).toObject(defaultValues);
    return jsonExtractUserStatus(retrievedData);
}

quint64 clearAtEndOfToTimestamp(const OCC::ClearAt &clearAt)
{
    Q_ASSERT(clearAt._type == OCC::ClearAtType::EndOf);

    if (clearAt._endof == "day"_L1) {
        return QDate::currentDate().addDays(1).startOfDay().toSecsSinceEpoch();
    } else if (clearAt._endof == "week"_L1) {
        const auto days = Qt::Sunday - QDate::currentDate().dayOfWeek();
        return QDate::currentDate().addDays(days + 1).startOfDay().toSecsSinceEpoch();
    }
    qCWarning(lcOcsUserStatusConnector) << "Can not handle clear at endof day type" << clearAt._endof;
    return QDateTime::currentDateTime().toSecsSinceEpoch();
}

quint64 clearAtPeriodToTimestamp(const OCC::ClearAt &clearAt)
{
    return QDateTime::currentDateTime().addSecs(clearAt._period).toSecsSinceEpoch();
}

quint64 clearAtToTimestamp(const OCC::ClearAt &clearAt)
{
    switch (clearAt._type) {
    case OCC::ClearAtType::Period: {
        return clearAtPeriodToTimestamp(clearAt);
    }

    case OCC::ClearAtType::EndOf: {
        return clearAtEndOfToTimestamp(clearAt);
    }

    case OCC::ClearAtType::Timestamp: {
        return clearAt._timestamp;
    }
    }

    return 0;
}

quint64 clearAtToTimestamp(const OCC::Optional<OCC::ClearAt> &clearAt)
{
    if (clearAt) {
        return clearAtToTimestamp(*clearAt);
    }
    return 0;
}

OCC::Optional<OCC::ClearAt> jsonToClearAt(QJsonObject jsonObject)
{
    OCC::Optional<OCC::ClearAt> clearAt;

    if (jsonObject.value("clearAt"_L1).isObject() && !jsonObject.value("clearAt"_L1).isNull()) {
        OCC::ClearAt clearAtValue;
        const auto clearAtObject = jsonObject.value("clearAt"_L1).toObject();
        const auto typeValue = clearAtObject.value("type"_L1).toString(u"period"_s);
        if (typeValue == "period"_L1) {
            const auto timeValue = clearAtObject.value("time"_L1).toInt(0);
            clearAtValue._type = OCC::ClearAtType::Period;
            clearAtValue._period = timeValue;
        } else if (typeValue == "end-of"_L1) {
            const auto timeValue = clearAtObject.value("time"_L1).toString(u"day"_s);
            clearAtValue._type = OCC::ClearAtType::EndOf;
            clearAtValue._endof = timeValue;
        } else {
            qCWarning(lcOcsUserStatusConnector) << "Can not handle clear type value" << typeValue;
        }
        clearAt = clearAtValue;
    }

    return clearAt;
}

OCC::UserStatus jsonToUserStatus(QJsonObject jsonObject)
{
    const auto clearAt = jsonToClearAt(jsonObject);

    OCC::UserStatus userStatus(
        jsonObject.value("id"_L1).toString(u"no-id"_s),
        jsonObject.value("message"_L1).toString(u"No message"_s),
        jsonObject.value("icon"_L1).toString(u"no-icon"_s),
        OCC::UserStatus::OnlineStatus::Online,
        true,
        clearAt);

    return userStatus;
}

QVector<OCC::UserStatus> jsonToPredefinedStatuses(QJsonArray jsonDataArray)
{
    QVector<OCC::UserStatus> statuses;
    for (const auto &jsonEntry : jsonDataArray) {
        Q_ASSERT(jsonEntry.isObject());
        if (!jsonEntry.isObject()) {
            continue;
        }
        statuses.append(jsonToUserStatus(jsonEntry.toObject()));
    }

    return statuses;
}


const QString baseUrl("/ocs/v2.php/apps/user_status/api/v1");
const QString userStatusBaseUrl = baseUrl + QStringLiteral("/user_status");
}

namespace OCC {

OcsUserStatusConnector::OcsUserStatusConnector(AccountPtr account, QObject *parent)
    : UserStatusConnector(parent)
    , _account(account)
{
    Q_ASSERT(_account);
    _userStatusSupported = _account->capabilities().userStatus();
    _userStatusEmojisSupported = _account->capabilities().userStatusSupportsEmoji();
    _userStatusBusySupported = _account->capabilities().userStatusSupportsBusy();
}

void OcsUserStatusConnector::fetchUserStatus()
{
    qCDebug(lcOcsUserStatusConnector) << "Try to fetch user status";

    if (!_userStatusSupported) {
        qCDebug(lcOcsUserStatusConnector) << "User status not supported";
        emit error(Error::UserStatusNotSupported);
        return;
    }

    startFetchUserStatusJob();
}

void OcsUserStatusConnector::startFetchUserStatusJob()
{
    if (_getUserStatusJob) {
        qCDebug(lcOcsUserStatusConnector) << "Get user status job is already running.";
        return;
    }

    _getUserStatusJob = new JsonApiJob(_account, userStatusBaseUrl, this);
    connect(_getUserStatusJob, &JsonApiJob::jsonReceived, this, &OcsUserStatusConnector::onUserStatusFetched);
    _getUserStatusJob->start();
}

void OcsUserStatusConnector::onUserStatusFetched(const QJsonDocument &json, int statusCode)
{
    logResponse("user status fetched", json, statusCode);

    if (statusCode != 200) {
        qCInfo(lcOcsUserStatusConnector) << "Slot fetch UserStatus finished with status code" << statusCode;
        emit error(Error::CouldNotFetchUserStatus);
        return;
    }

    const auto oldOnlineState = _userStatus.state();
    _userStatus = jsonToUserStatus(json);

    emit userStatusFetched(_userStatus);

    if (oldOnlineState != _userStatus.state()) {
        emit serverUserStatusChanged();
    }
}

void OcsUserStatusConnector::startFetchPredefinedStatuses()
{
    if (_getPredefinedStausesJob) {
        qCDebug(lcOcsUserStatusConnector) << "Get predefined statuses job is already running";
        return;
    }

    _getPredefinedStausesJob = new JsonApiJob(_account,
        baseUrl + QStringLiteral("/predefined_statuses"), this);
    connect(_getPredefinedStausesJob, &JsonApiJob::jsonReceived, this,
        &OcsUserStatusConnector::onPredefinedStatusesFetched);
    _getPredefinedStausesJob->start();
}

void OcsUserStatusConnector::fetchPredefinedStatuses()
{
    if (!_userStatusSupported) {
        emit error(Error::UserStatusNotSupported);
        return;
    }
    startFetchPredefinedStatuses();
}

void OcsUserStatusConnector::onPredefinedStatusesFetched(const QJsonDocument &json, int statusCode)
{
    logResponse("predefined statuses", json, statusCode);

    if (statusCode != 200) {
        qCInfo(lcOcsUserStatusConnector) << "Slot predefined user statuses finished with status code" << statusCode;
        emit error(Error::CouldNotFetchPredefinedUserStatuses);
        return;
    }
    const auto jsonData = json.object().value("ocs"_L1).toObject().value("data"_L1);
    Q_ASSERT(jsonData.isArray());
    if (!jsonData.isArray()) {
        return;
    }
    const auto statuses = jsonToPredefinedStatuses(jsonData.toArray());
    emit predefinedStatusesFetched(statuses);
}

void OcsUserStatusConnector::logResponse(const QString &message, const QJsonDocument &json, int statusCode)
{
    qCDebug(lcOcsUserStatusConnector) << "Response from:" << message << "Status:" << statusCode << "Json:" << json;
}

void OcsUserStatusConnector::setUserStatusOnlineStatus(UserStatus::OnlineStatus onlineStatus)
{
    _setOnlineStatusJob = new JsonApiJob(_account,
        userStatusBaseUrl + QStringLiteral("/status"), this);
    _setOnlineStatusJob->setVerb(JsonApiJob::Verb::Put);
    // Set body
    QJsonObject dataObject;
    dataObject.insert("statusType", onlineStatusToString(onlineStatus));
    QJsonDocument body;
    body.setObject(dataObject);
    _setOnlineStatusJob->setBody(body);
    connect(_setOnlineStatusJob, &JsonApiJob::jsonReceived, this, &OcsUserStatusConnector::onUserStatusOnlineStatusSet);
    _setOnlineStatusJob->start();
}

void OcsUserStatusConnector::setUserStatusMessagePredefined(const UserStatus &userStatus)
{
    Q_ASSERT(userStatus.messagePredefined());
    if (!userStatus.messagePredefined()) {
        return;
    }

    _setMessageJob = new JsonApiJob(_account, userStatusBaseUrl + QStringLiteral("/message/predefined"), this);
    _setMessageJob->setVerb(JsonApiJob::Verb::Put);
    // Set body
    QJsonObject dataObject;
    dataObject.insert("messageId", userStatus.id());
    if (userStatus.clearAt()) {
        dataObject.insert("clearAt", static_cast<int>(clearAtToTimestamp(userStatus.clearAt())));
    } else {
        dataObject.insert("clearAt", QJsonValue());
    }
    QJsonDocument body;
    body.setObject(dataObject);
    _setMessageJob->setBody(body);
    connect(_setMessageJob, &JsonApiJob::jsonReceived, this, &OcsUserStatusConnector::onUserStatusMessageSet);
    _setMessageJob->start();
}

void OcsUserStatusConnector::setUserStatusMessageCustom(const UserStatus &userStatus)
{
    Q_ASSERT(!userStatus.messagePredefined());
    if (userStatus.messagePredefined()) {
        return;
    }

    if (!_userStatusEmojisSupported) {
        emit error(Error::EmojisNotSupported);
        return;
    }
    _setMessageJob = new JsonApiJob(_account, userStatusBaseUrl + QStringLiteral("/message/custom"), this);
    _setMessageJob->setVerb(JsonApiJob::Verb::Put);
    // Set body
    QJsonObject dataObject;
    QJsonValue statusIcon;
    if (!userStatus.icon().isEmpty()) {
        statusIcon = userStatus.icon();
    }
    dataObject.insert("statusIcon", statusIcon);
    dataObject.insert("message", userStatus.message());
    const auto clearAt = userStatus.clearAt();
    if (clearAt) {
        dataObject.insert("clearAt", static_cast<int>(clearAtToTimestamp(*clearAt)));
    } else {
        dataObject.insert("clearAt", QJsonValue());
    }
    QJsonDocument body;
    body.setObject(dataObject);
    _setMessageJob->setBody(body);
    connect(_setMessageJob, &JsonApiJob::jsonReceived, this, &OcsUserStatusConnector::onUserStatusMessageSet);
    _setMessageJob->start();
}

void OcsUserStatusConnector::setUserStatusMessage(const UserStatus &userStatus)
{
    if (userStatus.messagePredefined()) {
        setUserStatusMessagePredefined(userStatus);
        return;
    }
    setUserStatusMessageCustom(userStatus);
}

void OcsUserStatusConnector::setUserStatus(const UserStatus &userStatus)
{
    if (!_userStatusSupported) {
        emit error(Error::UserStatusNotSupported);
        return;
    }

    if (_setOnlineStatusJob || _setMessageJob) {
        qCDebug(lcOcsUserStatusConnector) << "Set online status job or set message job are already running.";
        return;
    }

    if (userStatus.state() != _userStatus.state()) {
        setUserStatusOnlineStatus(userStatus.state());
    }
    setUserStatusMessage(userStatus);
}

void OcsUserStatusConnector::onUserStatusOnlineStatusSet(const QJsonDocument &json, int statusCode)
{
    logResponse("Online status set", json, statusCode);

    if (statusCode != 200) {
        emit error(Error::CouldNotSetUserStatus);
        return;
    }

    const auto oldOnlineState = _userStatus.state();
    _userStatus.setState(jsonToUserStatus(json).state());

    emit userStatusSet();

    if (oldOnlineState != _userStatus.state()) {
        emit serverUserStatusChanged();
    }
}

void OcsUserStatusConnector::onUserStatusMessageSet(const QJsonDocument &json, int statusCode)
{
    logResponse("Message set", json, statusCode);

    if (statusCode != 200) {
        emit error(Error::CouldNotSetUserStatus);
        return;
    }

    // We fetch the user status again because json does not contain
    // the new message when user status was set from a predefined
    // message
    fetchUserStatus();

    emit userStatusSet();
}

void OcsUserStatusConnector::clearMessage()
{
    _clearMessageJob = new JsonApiJob(_account, userStatusBaseUrl + QStringLiteral("/message"));
    _clearMessageJob->setVerb(JsonApiJob::Verb::Delete);
    connect(_clearMessageJob, &JsonApiJob::jsonReceived, this, &OcsUserStatusConnector::onMessageCleared);
    _clearMessageJob->start();
}

UserStatus OcsUserStatusConnector::userStatus() const
{
    return _userStatus;
}

bool OcsUserStatusConnector::supportsBusyStatus() const
{
    return _userStatusBusySupported;
}

void OcsUserStatusConnector::onMessageCleared(const QJsonDocument &json, int statusCode)
{
    logResponse("Message cleared", json, statusCode);

    if (statusCode != 200) {
        emit error(Error::CouldNotClearMessage);
        return;
    }

    const auto onlineState = _userStatus.state();

    _userStatus = {};
    _userStatus.setState(onlineState);
    emit messageCleared();
}
}
