/*******************************************************************************
 * Copyright (c) 2026, Jan Koester jan.koester@gmx.net
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 * Neither the name of the <organization> nor the
 *      names of its contributors may be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

#include "publishclient.h"

#include <QNetworkReply>
#include <QNetworkRequest>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QUrlQuery>
#include <QRegularExpression>
#include <QHttpMultiPart>

PublishClient::PublishClient(QObject *parent)
    : QObject(parent)
{
    _nam = new QNetworkAccessManager(this);
}

bool PublishClient::isConfigured() const {
    return !_serverUrl.isEmpty();
}

void PublishClient::setServerUrl(const QString &url) {
    _serverUrl = url;
    if (_serverUrl.endsWith('/'))
        _serverUrl.chop(1);
}

void PublishClient::setAuthToken(const QString &token) {
    _authToken = token;
}

void PublishClient::setIgnoreSslErrors(bool ignore) {
    _ignoreSsl = ignore;
}

void PublishClient::abort() {
    if (_activeReply && _activeReply->isRunning()) {
        _activeReply->abort();
    }
}

void PublishClient::testConnection(Callback cb) {
    if (_serverUrl.isEmpty()) {
        cb(false, "Server URL not set");
        return;
    }

    QUrl url(_serverUrl);
    QNetworkRequest req(url);
    req.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);

    QNetworkReply *reply = _nam->get(req);
    _activeReply = reply;
    if (_ignoreSsl)
        connect(reply, &QNetworkReply::sslErrors, reply, qOverload<>(&QNetworkReply::ignoreSslErrors));
    connect(reply, &QNetworkReply::finished, this, [this, reply, cb]() {
        if (_activeReply == reply) _activeReply = nullptr;
        reply->deleteLater();
        if (reply->error() == QNetworkReply::OperationCanceledError) {
            cb(false, "Aborted");
            return;
        }
        /* Any HTTP response (even 401/403) means the server is reachable.
           Only true network errors (DNS, refused, timeout) with no HTTP
           status should be treated as failures. */
        int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
        if (reply->error() == QNetworkReply::NoError || status > 0) {
            cb(true, "Server reachable (HTTP " + QString::number(status) + ")");
        } else {
            cb(false, reply->errorString());
        }
    });
}

void PublishClient::login(const QString &user, const QString &password, Callback cb) {
    QUrl url(_serverUrl + "/api");
    QNetworkRequest req(url);
    req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
    req.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);

    /* blogi API format: [{"login": {"username": "...", "password": "..."}}] */
    QJsonObject loginParams;
    loginParams["username"] = user;
    loginParams["password"] = password;

    QJsonObject loginCmd;
    loginCmd["login"] = loginParams;

    QJsonArray commands;
    commands.append(loginCmd);

    QJsonDocument doc(commands);

    QNetworkReply *reply = _nam->post(req, doc.toJson(QJsonDocument::Compact));
    _activeReply = reply;
    if (_ignoreSsl)
        connect(reply, &QNetworkReply::sslErrors, reply, qOverload<>(&QNetworkReply::ignoreSslErrors));
    connect(reply, &QNetworkReply::finished, this, [this, reply, cb]() {
        if (_activeReply == reply) _activeReply = nullptr;
        reply->deleteLater();
        /* Try to parse JSON body even on auth errors */
        QByteArray body = reply->readAll();
        QJsonDocument respDoc = QJsonDocument::fromJson(body);
        if (respDoc.isArray() && respDoc.array().size() > 0) {
            QJsonObject obj = respDoc.array()[0].toObject();
            if (obj.contains("login_success")) {
                QJsonObject success = obj["login_success"].toObject();
                _authToken = success["authid"].toString();
                cb(true, "Logged in");
            } else if (obj.contains("login_failure")) {
                QJsonObject fail = obj["login_failure"].toObject();
                cb(false, fail["error"].toString("Login failed"));
            } else {
                cb(false, "Unexpected response: " + QString::fromUtf8(body));
            }
        } else if (reply->error() != QNetworkReply::NoError) {
            cb(false, reply->errorString());
        } else {
            cb(false, "Empty response from server");
        }
    });
}

void PublishClient::publish(const QString &html, Callback cb) {
    if (_serverUrl.isEmpty()) {
        cb(false, "Server URL not configured");
        return;
    }

    QUrl url(_serverUrl + "/api");
    QNetworkRequest req(url);
    req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
    req.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);

    QJsonArray commands;

    /* First command: authenticate with authid */
    if (!_authToken.isEmpty()) {
        QJsonObject authCmd;
        authCmd["authid"] = _authToken;
        commands.append(authCmd);
    }

    /* Second command: publish the rendered HTML via webedit */
    QJsonObject pubCmd;
    QJsonObject pubParams;
    pubParams["html"] = html;
    pubCmd["publish"] = pubParams;
    commands.append(pubCmd);

    QJsonDocument doc(commands);

    QNetworkReply *reply = _nam->post(req, doc.toJson(QJsonDocument::Compact));
    _activeReply = reply;
    if (_ignoreSsl)
        connect(reply, &QNetworkReply::sslErrors, reply, qOverload<>(&QNetworkReply::ignoreSslErrors));
    connect(reply, &QNetworkReply::finished, this, [this, reply, cb]() {
        if (_activeReply == reply) _activeReply = nullptr;
        reply->deleteLater();
        if (reply->error() != QNetworkReply::NoError) {
            cb(false, reply->errorString());
            return;
        }
        QJsonDocument respDoc = QJsonDocument::fromJson(reply->readAll());
        if (respDoc.isArray() && respDoc.array().size() > 0) {
            QJsonObject obj = respDoc.array()[0].toObject();
            if (obj["status"].toString() == "success") {
                QString pubUrl = obj["url"].toString();
                cb(true, pubUrl.isEmpty() ? "Published successfully" : pubUrl);
            } else {
                cb(false, obj["message"].toString("Publish failed"));
            }
        } else {
            cb(false, "Unexpected response");
        }
    });
}

void PublishClient::renderOnServer(const QString &html, bool mobile, Callback cb) {
    renderOnServer(html, mobile, QString(), QMap<QString,QString>(), cb);
}

void PublishClient::renderOnServer(const QString &html, bool mobile, const QString &target,
                                   const QMap<QString,QString> &extraParams, Callback cb) {
    if (_serverUrl.isEmpty()) {
        cb(false, "Server URL not configured");
        return;
    }

    QUrl url(_serverUrl + "/render");
    QNetworkRequest req(url);
    req.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);

    QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);

    auto addPart = [multiPart](const QString &name, const QString &value) {
        QHttpPart part;
        part.setHeader(QNetworkRequest::ContentDispositionHeader,
                       QString("form-data; name=\"%1\"").arg(name));
        part.setBody(value.toUtf8());
        multiPart->append(part);
    };

    if (!_authToken.isEmpty())
        addPart("authid", _authToken);
    addPart("html", html);
    addPart("mobile", mobile ? "true" : "false");
    if (!target.isEmpty())
        addPart("target", target);
    for (auto it = extraParams.cbegin(); it != extraParams.cend(); ++it)
        addPart(it.key(), it.value());

    QNetworkReply *reply = _nam->post(req, multiPart);
    multiPart->setParent(reply); // ensure multiPart is deleted with reply
    _activeReply = reply;
    if (_ignoreSsl)
        connect(reply, &QNetworkReply::sslErrors, reply, qOverload<>(&QNetworkReply::ignoreSslErrors));
    connect(reply, &QNetworkReply::finished, this, [this, reply, cb]() {
        if (_activeReply == reply) _activeReply = nullptr;
        reply->deleteLater();
        if (reply->error() == QNetworkReply::OperationCanceledError) {
            cb(false, "Aborted");
            return;
        }
        int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
        QByteArray body = reply->readAll();
        if (status == 200) {
            cb(true, QString::fromUtf8(body));
        } else {
            /* Try to extract a useful message from the HTML error body */
            QString errMsg;
            QString bodyStr = QString::fromUtf8(body);
            /* Look for text inside <span>...</span> which is the blogi error format */
            QRegularExpression rx("<span>(.*?)</span>");
            QRegularExpressionMatch match = rx.match(bodyStr);
            if (match.hasMatch()) {
                errMsg = match.captured(1);
            } else if (reply->error() != QNetworkReply::NoError) {
                errMsg = reply->errorString();
            } else {
                errMsg = "Server returned HTTP " + QString::number(status);
            }
            qWarning() << "renderOnServer failed: HTTP" << status << errMsg;
            cb(false, errMsg);
        }
    });
}

void PublishClient::apiRequest(const QJsonArray &commands, std::function<void(bool, const QJsonArray &)> cb) {
    if (_serverUrl.isEmpty()) {
        cb(false, QJsonArray());
        return;
    }

    /* Prepend authid command for authentication */
    QJsonArray fullCmds;
    if (!_authToken.isEmpty()) {
        QJsonObject authCmd;
        authCmd["authid"] = _authToken;
        fullCmds.append(authCmd);
    }
    for (int i = 0; i < commands.size(); ++i)
        fullCmds.append(commands[i]);

    QUrl url(_serverUrl + "/api");
    QNetworkRequest req(url);
    req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
    req.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);

    QJsonDocument doc(fullCmds);
    QNetworkReply *reply = _nam->post(req, doc.toJson(QJsonDocument::Compact));
    if (_ignoreSsl)
        connect(reply, &QNetworkReply::sslErrors, reply, qOverload<>(&QNetworkReply::ignoreSslErrors));
    connect(reply, &QNetworkReply::finished, this, [reply, cb]() {
        reply->deleteLater();
        QJsonDocument respDoc = QJsonDocument::fromJson(reply->readAll());
        if (respDoc.isArray()) {
            cb(true, respDoc.array());
        } else {
            cb(false, QJsonArray());
        }
    });
}
