#include "seasonalpackdownloader.h"

#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSaveFile>
#include <QTimer>

namespace {

// Пак большой, но идёт с нашего же сервера: минуты простоя без единого байта
// означают, что соединение умерло, а не что файл тяжёлый.
constexpr int kTransferTimeoutMs = 60000;

// Столько же, сколько у остальных установщиков: сигналы прогресса приходят
// пачками, а перерисовывать панель чаще десяти раз в секунду незачем.
constexpr int kProgressIntervalMs = 100;

} // namespace

SeasonalPackDownloader::SeasonalPackDownloader(QObject *parent)
    : QObject(parent)
    , m_network(new QNetworkAccessManager(this))
    , m_progressTimer(new QTimer(this))
{
    m_progressTimer->setInterval(kProgressIntervalMs);
    connect(m_progressTimer, &QTimer::timeout, this, &SeasonalPackDownloader::flushProgress);
}

SeasonalPackDownloader::~SeasonalPackDownloader()
{
    if (m_reply)
        m_reply->abort();
}

double SeasonalPackDownloader::fraction() const
{
    if (m_bytesTotal <= 0)
        return -1.0;
    return qBound(0.0, double(m_bytesDone) / double(m_bytesTotal), 1.0);
}

void SeasonalPackDownloader::download(const QUrl &url,
                                      const QString &targetPath,
                                      const QString &sha256,
                                      qint64 expectedSize,
                                      const QString &label)
{
    if (m_running) {
        emit failed(label, tr("Загрузка сборки уже идёт"));
        return;
    }

    m_running = true;
    m_canceling = false;
    m_targetPath = targetPath;
    m_sha256 = sha256;
    m_label = label;
    m_bytesDone = 0;
    m_bytesTotal = expectedSize;
    m_stage = tr("Загрузка файлов сборки");
    m_currentFile = QFileInfo(targetPath).fileName();

    if (!QDir().mkpath(QFileInfo(targetPath).absolutePath())) {
        fail(tr("Не удалось создать %1")
                 .arg(QDir::toNativeSeparators(QFileInfo(targetPath).absolutePath())));
        return;
    }

    m_file = std::make_unique<QSaveFile>(targetPath);
    if (!m_file->open(QIODevice::WriteOnly)) {
        fail(tr("Не удалось открыть на запись %1").arg(QDir::toNativeSeparators(targetPath)));
        return;
    }
    m_hash.reset();

    emit started(m_label);

    QNetworkRequest request{url};
    request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
                         QNetworkRequest::NoLessSafeRedirectPolicy);
    request.setTransferTimeout(kTransferTimeoutMs);

    m_reply = m_network->get(request);
    connect(m_reply, &QNetworkReply::readyRead, this, [this] {
        const QByteArray chunk = m_reply->readAll();
        if (chunk.isEmpty())
            return;
        m_hash.addData(chunk);
        // Диск кончился или файл забрали из-под нас: продолжать качать незачем,
        // ошибку разберёт обработчик finished.
        if (m_file->write(chunk) != chunk.size())
            m_reply->abort();
    });
    connect(m_reply, &QNetworkReply::downloadProgress, this,
            [this](qint64 received, qint64 total) {
                m_bytesDone = received;
                // Размер из каталога может разойтись с реальным — верим серверу.
                if (total > 0)
                    m_bytesTotal = total;
                scheduleProgress();
            });
    connect(m_reply, &QNetworkReply::finished, this, [this] {
        QNetworkReply *reply = m_reply;
        m_reply = nullptr;
        reply->deleteLater();

        if (m_canceling) {
            m_file->cancelWriting();
            m_file.reset();
            const QString label = m_label;
            reset();
            emit canceled(label);
            return;
        }

        if (reply->error() != QNetworkReply::NoError) {
            m_file->cancelWriting();
            m_file.reset();
            fail(tr("Не удалось скачать сборку: %1").arg(reply->errorString()));
            return;
        }

        const QString actual = QString::fromLatin1(m_hash.result().toHex());
        if (actual.compare(m_sha256, Qt::CaseInsensitive) != 0) {
            m_file->cancelWriting();
            m_file.reset();
            fail(tr("Контрольная сумма архива сборки не совпала — загрузка повреждена"));
            return;
        }

        if (!m_file->commit()) {
            m_file.reset();
            fail(tr("Не удалось сохранить %1").arg(QDir::toNativeSeparators(m_targetPath)));
            return;
        }
        m_file.reset();

        const QString path = m_targetPath;
        reset();
        emit finished(path);
    });

    scheduleProgress();
}

void SeasonalPackDownloader::cancel()
{
    if (!m_running || m_canceling)
        return;

    m_canceling = true;
    if (m_reply) {
        m_reply->abort();
        return;
    }

    // Запрос ещё не создан — обрывать нечего, отвечаем сразу.
    if (m_file)
        m_file->cancelWriting();
    m_file.reset();
    const QString label = m_label;
    reset();
    emit canceled(label);
}

void SeasonalPackDownloader::fail(const QString &message)
{
    const QString label = m_label;
    if (m_file)
        m_file->cancelWriting();
    m_file.reset();
    // Обрезанный пак нельзя ни распаковать, ни докачать: следующая попытка
    // начнётся с чистого файла.
    if (!m_targetPath.isEmpty())
        QFile::remove(m_targetPath);
    reset();
    emit failed(label, message);
}

void SeasonalPackDownloader::reset()
{
    m_running = false;
    m_canceling = false;
    m_progressTimer->stop();
    m_progressDirty = false;
    m_file.reset();
    m_targetPath.clear();
    m_sha256.clear();
    m_label.clear();
    m_stage.clear();
    m_currentFile.clear();
    m_bytesDone = 0;
    m_bytesTotal = 0;
    emit progressChanged();
}

void SeasonalPackDownloader::scheduleProgress()
{
    m_progressDirty = true;
    if (!m_progressTimer->isActive())
        m_progressTimer->start();
}

void SeasonalPackDownloader::flushProgress()
{
    if (!m_progressDirty) {
        m_progressTimer->stop();
        return;
    }
    m_progressDirty = false;
    emit progressChanged();
}
