/*
 * 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 "RcloneDownloadJob.h"

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

#include <QJsonObject>
#include <QNetworkRequest>

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

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

namespace
{

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

constexpr int CHUNK_SIZE = 64 * 1024;

std::exception_ptr to_std(boost::exception_ptr const& bep)
{
    try
    {
        boost::rethrow_exception(bep);
    }
    catch (...)
    {
        return std::current_exception();
    }
    return nullptr;
}

}  // namespace

RcloneDownloadJob::RcloneDownloadJob(shared_ptr<RcloneClient> const& client,
                                     string const& fs, string const& item_id,
                                     string const& match_etag)
    : QObject(), DownloadJob(make_download_id()), client_(client), fs_(fs),
      item_id_(item_id), match_etag_(match_etag)
{
    int fd = dup(write_socket());
    if (fd < 0)
    {
        throw ResourceException(
            "Could not duplicate download socket: " + string(strerror(errno)),
            errno);
    }
    writer_.setSocketDescriptor(
        fd, QLocalSocket::ConnectedState, QIODevice::WriteOnly);
    connect(&writer_, &QIODevice::bytesWritten,
            this, &RcloneDownloadJob::onSocketBytesWritten);

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

        if (!match_etag_.empty())
            verify_etag(client_, fs_, id_to_remote_path(item_id_), item_id_,
                        match_etag_,
                        [this, alive](QJsonObject const&,
                                      boost::exception_ptr const& stat_err) {
                if (!*alive || cancelled_) return;
                if (stat_err) { handle_error(to_std(stat_err)); return; }
                start_download();
            });

        else start_download();
    });
}

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

void RcloneDownloadJob::start_download()
{
    if (cancelled_ || is_error_) return;

    QUrl url = client_->serve_url(fs_, id_to_remote_path(item_id_));
    QNetworkRequest request = client_->authenticated(url);
    reply_.reset(client_->network().get(request));
    reply_->setReadBufferSize(CHUNK_SIZE);
    connect(reply_.get(), &QNetworkReply::finished,
            this, &RcloneDownloadJob::onReplyFinished);
    connect(reply_.get(), &QIODevice::readyRead,
            this, &RcloneDownloadJob::onReplyReadyRead);
    connect(reply_.get(), &QIODevice::readChannelFinished,
            this, &RcloneDownloadJob::onReplyReadChannelFinished);
}

void RcloneDownloadJob::checkStatus()
{
    if (!seen_status_)
    {
        seen_status_ = true;
        auto status = reply_->attribute(
            QNetworkRequest::HttpStatusCodeAttribute).toInt();
        if (status != 200 && status != 206)
        {
            is_error_ = true;
        }
    }
}

void RcloneDownloadJob::onReplyFinished()
{
    if (cancelled_) return;

    checkStatus();
    if (is_error_)
    {
        handle_error(to_std(
            translate_rclone_error(reply_.get(), error_body_, item_id_)));
    }
    else if (reply_->error() != QNetworkReply::NoError)
    {
        handle_error(RemoteCommsException(
            "Download failed: " + reply_->errorString().toStdString()));
    }
}

void RcloneDownloadJob::onReplyReadyRead()
{
    checkStatus();

    if (is_error_)
    {
        if (error_body_.size() < MAX_ERROR_BODY_LENGTH)
        {
            error_body_.append(reply_->readAll());
        }
        else
        {
            reply_->close();
        }
    }
    else
    {
        maybe_send_chunk();
    }
}

void RcloneDownloadJob::onReplyReadChannelFinished()
{
    if (cancelled_) return;

    checkStatus();
    if (is_error_)
    {
        return;
    }

    read_channel_finished_ = true;
    maybe_send_chunk();
}

void RcloneDownloadJob::onSocketBytesWritten(int64_t bytes)
{
    if (is_error_)
    {
        return;
    }

    bytes_written_ += bytes;
    maybe_send_chunk();
}

void RcloneDownloadJob::maybe_send_chunk()
{
    assert(bytes_written_ <= bytes_read_);
    // If there are outstanding writes, do nothing.
    if (bytes_written_ < bytes_read_)
    {
        return;
    }
    // If there are no bytes available, return.
    if (reply_->bytesAvailable() == 0)
    {
        // If we've reached the end of the input, and all data has been
        // written out, signal completion.
        if (read_channel_finished_ && bytes_written_ == bytes_read_)
        {
            writer_.close();
            report_complete();
        }
        return;
    }

    char buffer[CHUNK_SIZE];
    int n_read = reply_->read(buffer, CHUNK_SIZE);
    if (n_read < 0)
    {
        handle_error(RemoteCommsException("Failed to read from server: " +
                                          reply_->errorString().toStdString()));
        return;
    }
    bytes_read_ += n_read;

    int n_written = writer_.write(buffer, n_read);
    if (n_written < 0)
    {
        handle_error(ResourceException(
                         "Error writing to socket: "
                         + writer_.errorString().toStdString(), 0));
        return;
    }
}

void RcloneDownloadJob::handle_error(StorageException const& exc)
{
    handle_error(std::make_exception_ptr(exc));
}

void RcloneDownloadJob::handle_error(std::exception_ptr ep)
{
    is_error_ = true;
    if (reply_) reply_->close();
    writer_.close();
    report_error(ep);
}

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

boost::future<void> RcloneDownloadJob::finish()
{
    return boost::make_exceptional_future<void>(
        LogicException("finish called before all data sent"));
}
