/* EkosLive Node

    SPDX-FileCopyrightText: 2023 Jasem Mutlaq <mutlaqja@ikarustech.com>

    Node Manager

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "nodemanager.h"
#include "version.h"
#include "../../auxiliary/ksutils.h"
#include "ekos_debug.h"

#include <QWebSocket>
#include <QDateTime>
#include <QUrlQuery>
#include <QTimer>
#include <QJsonDocument>
#include <QStandardPaths>
#include <QDir>
#include <QFile>
#include <QSaveFile>

#include <KActionCollection>
#include <KLocalizedString>
#include <basedevice.h>
#include <QUuid>

namespace EkosLive
{

///////////////////////////////////////////////////////////////////////////////////////////
///
///////////////////////////////////////////////////////////////////////////////////////////
NodeManager::NodeManager(uint32_t mask)
{
    m_NetworkManager = new QNetworkAccessManager(this);
    connect(m_NetworkManager, &QNetworkAccessManager::finished, this, &NodeManager::onResult);

    // Configure nodes
    if (mask & Message)
        m_Nodes[Message] = new Node("message");
    if (mask & Media)
        m_Nodes[Media] = new Node("media");
    if (mask & Cloud)
    {
        m_Nodes[Cloud] = new Node("cloud");
        // Must always be true
        m_Nodes[Cloud]->setClientState(true);
    }

    for (auto &node : m_Nodes)
    {
        connect(node, &Node::connected, this, &NodeManager::setConnected);
        connect(node, &Node::disconnected, this, &NodeManager::setDisconnected);
    }
}

///////////////////////////////////////////////////////////////////////////////////////////
///
///////////////////////////////////////////////////////////////////////////////////////////
void NodeManager::setURLs(const QUrl &service, const QUrl &websocket)
{
    m_ServiceURL = service;
    m_WebsocketURL = websocket;
    for (auto &node : m_Nodes)
        node->setProperty("url", m_WebsocketURL);
}

///////////////////////////////////////////////////////////////////////////////////////////
///
///////////////////////////////////////////////////////////////////////////////////////////
bool NodeManager::isConnected() const
{
    return std::all_of(m_Nodes.begin(), m_Nodes.end(), [](const auto & node)
    {
        return node->isConnected();
    });
}

///////////////////////////////////////////////////////////////////////////////////////////
///
///////////////////////////////////////////////////////////////////////////////////////////
void NodeManager::setConnected()
{
    auto node = qobject_cast<Node*>(sender());
    if (!node)
        return;

    auto isConnected = std::all_of(m_Nodes.begin(), m_Nodes.end(), [](const auto & node)
    {
        return node->isConnected();
    });

    // Only emit once all nodes are connected.
    if (isConnected)
    {
        // If we were re-authenticating, mark it as complete now that all nodes are connected.
        if (m_isReauthenticating)
        {
            qCInfo(KSTARS_EKOS) << "NodeManager for URL" << m_ServiceURL.toDisplayString() <<
                                   "successfully re-authenticated and connected all nodes.";
            setIsReauthenticating(false);
        }
        Q_EMIT connected();
    }
}

///////////////////////////////////////////////////////////////////////////////////////////
///
///////////////////////////////////////////////////////////////////////////////////////////
void NodeManager::setDisconnected()
{
    auto node = qobject_cast<Node *>(sender());
    if (!node)
        return;

    // Only emit once all nodes are disconnected.
    if (isConnected() == false)
    {
        Q_EMIT disconnected();
    }
}

///////////////////////////////////////////////////////////////////////////////////////////
///
///////////////////////////////////////////////////////////////////////////////////////////
void NodeManager::disconnectNodes()
{
    for (auto &node : m_Nodes)
        node->disconnectServer();
}

///////////////////////////////////////////////////////////////////////////////////////////
///
///////////////////////////////////////////////////////////////////////////////////////////
void NodeManager::setCredentials(const QString &username, const QString &password)
{
    m_Username = username;
    m_Password = password;
}

///////////////////////////////////////////////////////////////////////////////////////////
///
///////////////////////////////////////////////////////////////////////////////////////////
void NodeManager::authenticate()
{
    qCDebug(KSTARS_EKOS) << "NodeManager(" << m_ServiceURL.toDisplayString() <<
                            "): Entering authenticate(). Current re-auth status:" << m_isReauthenticating;
    if (m_isReauthenticating) // Check if already in progress
    {
        // Already trying to authenticate this manager, prevent stacking requests.
        qCInfo(KSTARS_EKOS) << "NodeManager::authenticate called while already in progress for URL:" <<
                            m_ServiceURL.toDisplayString() << ". Ignoring.";
        return;
    }

    m_isReauthenticating = true; // Set the flag at the start of a new authentication attempt.

    QNetworkRequest request;
    request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");

    QUrl authURL(m_ServiceURL);
    authURL.setPath("/api/authenticate");

    request.setUrl(authURL);

    QJsonObject json = {{"username", m_Username}, {"machine_id", KSUtils::getMachineID()}};

    if (!m_DeviceToken.isEmpty())
        json["device_token"] = m_DeviceToken;
    else
        json["password"] = m_Password;

    auto postData = QJsonDocument(json).toJson(QJsonDocument::Compact);

    m_NetworkManager->post(request, postData);
}

///////////////////////////////////////////////////////////////////////////////////////////
///
///////////////////////////////////////////////////////////////////////////////////////////
bool NodeManager::isTokenNearlyExpired(int bufferSeconds) const
{
    if (m_AuthResponse.isEmpty() || m_tokenExpiryTimestamp == 0)
    {
        qCWarning(KSTARS_EKOS) << "NodeManager: Token is considered expired (empty auth response or no expiry timestamp).";
        return true; // No token or expiry unknown, assume needs auth
    }

    qlonglong currentTime = QDateTime::currentSecsSinceEpoch();
    bool expired = currentTime >= (m_tokenExpiryTimestamp - bufferSeconds);
    if (expired)
    {
        qCWarning(KSTARS_EKOS) << "NodeManager: Token is nearly expired. CurrentTime:" << currentTime
                               << "Expiry:" << m_tokenExpiryTimestamp << "Buffer:" << bufferSeconds;
    }
    return expired;
}

///////////////////////////////////////////////////////////////////////////////////////////
///
///////////////////////////////////////////////////////////////////////////////////////////
void NodeManager::clearAuthentication()
{
    qCInfo(KSTARS_EKOS) << "NodeManager: Clearing authentication data.";
    m_AuthResponse = QJsonObject();
    m_tokenExpiryTimestamp = 0;
    // Also disconnect nodes if they are connected with old auth
    // disconnectNodes(); // Caller (EkosLiveClient) should manage this if needed before re-auth.
}
///////////////////////////////////////////////////////////////////////////////////////////
///
///////////////////////////////////////////////////////////////////////////////////////////
void NodeManager::onResult(QNetworkReply *reply)
{
    // This slot is connected to the shared QNetworkAccessManager::finished, so it fires for EVERY
    // request on m_NetworkManager. Only the authentication reply belongs here — other requests
    // (notably /api/offlinetrainer/train) have their own finished handlers. Processing a non-auth
    // reply here would overwrite m_AuthResponse with the wrong body, blank the token, reconnect every
    // node with empty credentials, and tear the session down — exactly what happened on each Train click.
    if (reply->url().path() != QLatin1String("/api/authenticate"))
        return;

    qCDebug(KSTARS_EKOS) << "NodeManager(" << m_ServiceURL.toDisplayString() << "): Entering onResult(). Reply error:" <<
                         reply->error() << "String:" << reply->errorString();
    if (reply->error() != QNetworkReply::NoError)
    {
        // If connection refused or server error, retry up to 3 times
        if ((reply->error() == QNetworkReply::ConnectionRefusedError || reply->error() == QNetworkReply::UnknownServerError)
                && m_ReconnectTries++ < RECONNECT_MAX_TRIES)
        {
            qCInfo(KSTARS_EKOS) << "NodeManager for" << m_ServiceURL.toDisplayString() << "failed to connect. Retrying in" <<
                                RECONNECT_INTERVAL << "ms. Attempt" << m_ReconnectTries;
            reply->deleteLater();
            QTimer::singleShot(RECONNECT_INTERVAL, this, &NodeManager::retryAuthentication);
            return;
        }

        m_ReconnectTries = 0;
        // Reset flag on authentication error
        setIsReauthenticating(false);
        Q_EMIT authenticationError(i18n("Error authenticating with EkosLive server: %1", reply->errorString()));
        reply->deleteLater();
        return;
    }

    m_ReconnectTries = 0;
    QJsonParseError error;
    auto response = QJsonDocument::fromJson(reply->readAll(), &error);

    if (error.error != QJsonParseError::NoError)
    {
        // Reset flag on authentication error (parse error)
        setIsReauthenticating(false);
        Q_EMIT authenticationError(i18n("Error parsing server response: %1", error.errorString()));
        reply->deleteLater();
        return;
    }

    m_AuthResponse = response.object();

    if (m_AuthResponse["success"].toBool() == false)
    {
        // Reset flag on authentication error (server denied)
        m_tokenExpiryTimestamp = 0; // Clear any old expiry on auth failure
        setIsReauthenticating(false);
        Q_EMIT authenticationError(m_AuthResponse["message"].toString());
        reply->deleteLater();
        return;
    }

    // Store token expiry
    m_tokenExpiryTimestamp = KSUtils::getJwtExpiryTimestamp(m_AuthResponse["token"].toString());
    if (m_tokenExpiryTimestamp > 0)
    {
        qCInfo(KSTARS_EKOS) << "NodeManager for URL" << m_ServiceURL.toDisplayString() << "authenticated. Token expiry:"
                            << QDateTime::fromSecsSinceEpoch(m_tokenExpiryTimestamp).toString(Qt::ISODate);
    }
    else
    {
        qCWarning(KSTARS_EKOS) << "NodeManager for URL" << m_ServiceURL.toDisplayString() <<
                                  "authenticated, but failed to parse token expiry.";
        // Decide if this is a critical failure. For now, proceed but token checks might fail.
    }

    // If we were re-authenticating, NodeManager::setConnected will reset the flag once all nodes connect.
    // If not re-authenticating, this is a fresh authentication.

    qCDebug(KSTARS_EKOS) << "NodeManager(" << m_ServiceURL.toDisplayString() << "): Authenticated. Processing" <<
                         m_Nodes.count() << "nodes for reconnection.";
    for (auto &node : m_Nodes)
    {
        // To get node name here, Node class would need a public name() getter.
        // For now, let's log its pointer. The actual name will be logged from Node::connectServer().
        qCDebug(KSTARS_EKOS) << "NodeManager(" << m_ServiceURL.toDisplayString() << "): Processing node with pointer" << node <<
                                "- Calling setAuthResponse and connectServer.";
        node->setAuthResponse(m_AuthResponse);
        node->connectServer();
    }

    reply->deleteLater();
}

///////////////////////////////////////////////////////////////////////////////////////////
///
///////////////////////////////////////////////////////////////////////////////////////////
void NodeManager::retryAuthentication()
{
    // This is called by a timer after a failed connection attempt.
    // Reset the re-authenticating flag to allow the new attempt to proceed.
    setIsReauthenticating(false);
    authenticate();
}

///////////////////////////////////////////////////////////////////////////////////////////
///
///////////////////////////////////////////////////////////////////////////////////////////
void NodeManager::trainSession(const QJsonObject &sysidData)
{
    if (!isConnected())
    {
        QJsonObject errorResult;
        errorResult["message"] = QStringLiteral("Online server is not connected");
        Q_EMIT trainSessionResult(false, errorResult);
        return;
    }

    const QString token = m_AuthResponse["token"].toString();
    if (token.isEmpty())
    {
        QJsonObject errorResult;
        errorResult["message"] = QStringLiteral("No authentication token available");
        Q_EMIT trainSessionResult(false, errorResult);
        return;
    }

    QNetworkRequest request;
    request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");

    QUrl trainURL(m_ServiceURL);
    trainURL.setPath("/api/offlinetrainer/train");

    QUrlQuery query;
    query.addQueryItem("username", m_Username);
    query.addQueryItem("token", token);
    trainURL.setQuery(query);

    request.setUrl(trainURL);

    QJsonObject body;
    body["sysid_data"] = sysidData;
    auto postData = QJsonDocument(body).toJson(QJsonDocument::Compact);

    QNetworkReply *reply = m_NetworkManager->post(request, postData);
    connect(reply, &QNetworkReply::finished, this, [this, reply]()
    {
        reply->deleteLater();

        if (reply->error() != QNetworkReply::NoError)
        {
            QJsonObject errorResult;
            errorResult["message"] = reply->errorString();
            Q_EMIT trainSessionResult(false, errorResult);
            return;
        }

        QJsonParseError parseError;
        auto response = QJsonDocument::fromJson(reply->readAll(), &parseError);
        if (parseError.error != QJsonParseError::NoError)
        {
            QJsonObject errorResult;
            errorResult["message"] = parseError.errorString();
            Q_EMIT trainSessionResult(false, errorResult);
            return;
        }

        const QJsonObject responseObj = response.object();
        const bool success = responseObj["success"].toBool(false);
        Q_EMIT trainSessionResult(success, responseObj);
    });
}

///////////////////////////////////////////////////////////////////////////////////////////
///
///////////////////////////////////////////////////////////////////////////////////////////
QString NodeManager::ekosLiveDataPath()
{
    return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/ekoslive";
}

}
