/*
 * Copyright (C) 2016 Canonical Ltd.
 * Copyright (C) 2026 UBports Foundation.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "RcloneUploadJob.h"

#include "MultipartDevice.h"
#include "RcloneClient.h"
#include "item_id.h"
#include "rclone_error.h"
#include "rclone_ops.h"

#include <QJsonObject>
#include <QNetworkRequest>
#include <QUrl>
#include <QUrlQuery>
#include <lomiri/storage/provider/Exceptions.h>

#include <unistd.h>
#include <cerrno>
#include <cstring>

using namespace std;
using namespace lomiri::storage::provider;

namespace
{

string make_upload_id()
{
    static int counter = 0;
    return to_string(counter++);
}

constexpr int READ_BUFFER_SIZE = 64 * 1024;

}  // namespace

RcloneUploadJob::RcloneUploadJob(shared_ptr<RcloneClient> const& client,
                                 string const& fs, string const& item_id,
                                 int64_t size, string const& content_type,
                                 bool allow_overwrite, string const& old_etag)
    : QObject(), UploadJob(make_upload_id()), client_(client), fs_(fs),
      item_id_(item_id), size_(size), content_type_(content_type),
      allow_overwrite_(allow_overwrite), old_etag_(old_etag)
{
    int fd = dup(read_socket());
    if (fd < 0)
    {
        throw ResourceException(
            "Could not duplicate upload socket: " + string(strerror(errno)),
            errno);
    }
    reader_.setReadBufferSize(READ_BUFFER_SIZE);
    reader_.setSocketDescriptor(
        fd, QLocalSocket::ConnectedState, QIODevice::ReadOnly);

    client_->when_ready([this, alive = alive_](QJsonObject const&,
                                               boost::exception_ptr const& error) {
        if (!*alive || cancelled_) return;
        if (error) { set_exception(error); return; }

        // create_file with ErrorIfConflict: fail if the target already exists
        if (!allow_overwrite_)
            verify_not_exists(client_, promise_, fs_, id_to_remote_path(item_id_),
                            item_id_, name_of(item_id_),
                            [this, alive] { if (*alive) start_upload(); },
                            ExistsError::Conflict);

        // update with an expected etag: fail on a mismatch
        else if (!old_etag_.empty())
            verify_etag(client_, fs_, id_to_remote_path(item_id_), item_id_,
                        old_etag_,
                        [this, alive](QJsonObject const&, boost::exception_ptr const& error) {
                if (!*alive || cancelled_) return;
                if (error) { set_exception(error); return; }
                start_upload();
            });
        
        else start_upload();
    });
}

RcloneUploadJob::~RcloneUploadJob()
{
    *alive_ = false;
}

void RcloneUploadJob::set_exception(boost::exception_ptr const& error)
{
    if (promise_set_) return;
    promise_->set_exception(error);
    promise_set_ = true;
}

void RcloneUploadJob::start_upload()
{
    if (cancelled_ || promise_set_) return;

    // POST operations/uploadfile?fs=<fs>&remote=<parent dir>
    string dir = id_to_remote_path(parent_of(item_id_));
    string filename = name_of(item_id_);
    QUrl url = client_->base_url().resolved(
        QUrl(QStringLiteral("operations/uploadfile")));
    QUrlQuery query;
    query.addQueryItem(QStringLiteral("fs"), QString::fromStdString(fs_));
    query.addQueryItem(QStringLiteral("remote"), QString::fromStdString(dir));
    url.setQuery(query);
    QNetworkRequest request = client_->authenticated(url);

    multipart_.reset(new MultipartDevice(
        &reader_, size_, QString::fromStdString(filename),
        QString::fromStdString(content_type_)));
    request.setHeader(QNetworkRequest::ContentTypeHeader,
                      multipart_->content_type_header());
    request.setHeader(QNetworkRequest::ContentLengthHeader,
                      QVariant::fromValue(multipart_->total_size()));
    request.setAttribute(QNetworkRequest::DoNotBufferUploadDataAttribute, true);

    reply_.reset(client_->network().post(request, multipart_.get()));
    connect(reply_.get(), &QNetworkReply::finished,
            this, &RcloneUploadJob::onReplyFinished);
}

void RcloneUploadJob::onReplyFinished()
{
    if (promise_set_) return;

    int status = reply_->attribute(
        QNetworkRequest::HttpStatusCodeAttribute).toInt();
    QByteArray body = reply_->readAll();
    if (status != 200)
    {
        set_exception(translate_rclone_error(reply_.get(), body, item_id_));
        return;
    }

    promise_set_ = true;
    fetch_item_metadata(client_, promise_, fs_,
                        id_to_remote_path(item_id_), item_id_);
}

boost::future<void> RcloneUploadJob::cancel()
{
    cancelled_ = true;
    if (reply_) reply_->abort();
    return boost::make_ready_future();
}

boost::future<Item> RcloneUploadJob::finish()
{
    return promise_->get_future();
}
