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

#include <QAbstractSocket>
#include <QByteArray>
#include <QDebug>
#include <QProcessEnvironment>
#include <QRandomGenerator>
#include <QTcpServer>
#include <QTcpSocket>

namespace
{

constexpr int POLL_INTERVAL_MS = 50;
constexpr int STARTUP_DEADLINE_MS = 15000;
constexpr int SHUTDOWN_GRACE_MS = 10000;

int get_free_port()
{
    QTcpServer server;
    if (!server.listen(QHostAddress::LocalHost, 0))
    {
        return 0;
    }
    return server.serverPort();
}

QString make_password()
{
    quint32 words[8];
    QRandomGenerator::system()->fillRange(words);
    return QString::fromLatin1(
        QByteArray(reinterpret_cast<char const*>(words), sizeof(words)).toHex());
}

}  // namespace

RcloneDaemon::RcloneDaemon(QObject* parent)
    : QObject(parent), user_(QStringLiteral("storage-provider-rclone"))
{
    password_ = make_password();
    port_ = get_free_port();
    if (port_ == 0)
    {
        set_failed(QStringLiteral("Could not allocate a local port for rclone rcd"));
        return;
    }
    base_url_ = QUrl(QStringLiteral("http://127.0.0.1:%1/").arg(port_));

    QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
    env.insert(QStringLiteral("RCLONE_RC_PASS"), password_);
    process_.setProcessEnvironment(env);
    process_.setProcessChannelMode(QProcess::ForwardedChannels);

    connect(&process_,
            static_cast<void (QProcess::*)(QProcess::ProcessError)>(&QProcess::errorOccurred),
            this, &RcloneDaemon::on_process_error);
    connect(&process_,
            static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
            this, &RcloneDaemon::on_process_finished);

    QStringList args{
        QStringLiteral("rcd"),
        QStringLiteral("--rc-user"), user_,
        QStringLiteral("--rc-addr"), QStringLiteral("127.0.0.1:%1").arg(port_),
        QStringLiteral("--rc-serve"),
        QStringLiteral("--use-server-modtime"),
        QStringLiteral("--config"), QString(),
    };
    process_.start(QStringLiteral("rclone"), args);

    poll_timer_.setInterval(POLL_INTERVAL_MS);
    connect(&poll_timer_, &QTimer::timeout, this, &RcloneDaemon::poll_port);
    poll_timer_.start();

    deadline_timer_.setSingleShot(true);
    connect(&deadline_timer_, &QTimer::timeout, this, [this] {
        if (state_ == State::Starting)
            set_failed(QStringLiteral("Timed out waiting for rclone rcd to become ready"));
    });
    deadline_timer_.start(STARTUP_DEADLINE_MS);
}

RcloneDaemon::~RcloneDaemon()
{
    poll_timer_.stop();
    deadline_timer_.stop();

    disconnect(&process_, nullptr, this, nullptr);

    if (process_.state() != QProcess::NotRunning)
    {
        process_.terminate();
        if (!process_.waitForFinished(SHUTDOWN_GRACE_MS))
        {
            process_.kill();
            process_.waitForFinished();
        }
    }
}

void RcloneDaemon::poll_port()
{
    if (state_ != State::Starting || listening_emitted_) return;

    QTcpSocket sock;
    sock.connectToHost(QHostAddress::LocalHost, port_);
    if (sock.waitForConnected(POLL_INTERVAL_MS))
    {
        sock.disconnectFromHost();
        listening_emitted_ = true;
        poll_timer_.stop();
        Q_EMIT listening();
    }
}

void RcloneDaemon::on_process_error(QProcess::ProcessError error)
{
    Q_UNUSED(error);
    if (state_ == State::Starting)
        set_failed(QStringLiteral("rclone rcd failed to start: %1")
                 .arg(process_.errorString()));
}

void RcloneDaemon::on_process_finished(int exit_code, QProcess::ExitStatus status)
{
    Q_UNUSED(status);
    if (state_ == State::Starting)
        set_failed(QStringLiteral("rclone rcd exited early with code %1").arg(exit_code));
    else if (state_ == State::Ready)
        set_failed(QStringLiteral("rclone rcd exited with code %1").arg(exit_code));
}

void RcloneDaemon::set_ready()
{
    if (state_ != State::Starting) return;
    state_ = State::Ready;
    deadline_timer_.stop();
    Q_EMIT ready();
}

void RcloneDaemon::set_failed(QString const& reason)
{
    if (state_ == State::Failed) return;
    state_ = State::Failed;
    error_ = reason;
    poll_timer_.stop();
    deadline_timer_.stop();
    qWarning() << "rclone rcd:" << reason;
    Q_EMIT failed(reason);
}

void RcloneDaemon::set_failed(boost::exception_ptr const& error)
{
    try 
        { boost::rethrow_exception(error); }
    catch (std::exception const& e)
        { set_failed(QString::fromUtf8(e.what())); }
    catch (...)
        { set_failed(QStringLiteral("unknown error")); }
}