/**********************************************************************
 *  mainwindow.cpp
 **********************************************************************
 * Copyright (C) 2015-2026 MX Authors
 *
 * Authors: Adrian
 *          MX Linux <http://mxlinux.org>
 *
 * This file is part of mx-repo-manager.
 *
 * mx-repo-manager is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * mx-repo-manager 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 mx-repo-manager.  If not, see <http://www.gnu.org/licenses/>.
 **********************************************************************/
#include "mainwindow.h"
#include "ui_mainwindow.h"

#include <QDebug>
#include <QCloseEvent>
#include <QFile>
#include <QDir>
#include <QMetaEnum>
#include <QNetworkProxyFactory>
#include <QNetworkReply>
#include <QProgressBar>
#include <QRadioButton>
#include <QScreen>
#include <QTemporaryDir>
#include <QTextEdit>
#include <QStringList>
#include <QRegularExpression>

#include "about.h"
#include <chrono>

using namespace std::chrono_literals;

MainWindow::MainWindow(QWidget *parent)
    : QDialog(parent),
      ui(new Ui::MainWindow)
{
    qDebug().noquote() << QApplication::applicationName() << "version:" << QApplication::applicationVersion();
    ui->setupUi(this);
    setConnections();
    setWindowFlags(Qt::Window);
    initializeIcons();
    shell = new Cmd(this);

    connect(shell, &Cmd::started, this, &MainWindow::procStart);
    connect(shell, &Cmd::done, this, &MainWindow::procDone);

    setProgressBar();

    ui->pushOk->setDisabled(true);

    setWindowTitle(tr("MX Repo Manager"));
    ui->tabWidget->setCurrentWidget(ui->tabMX);
    refresh(true);

    restoreWindowGeometry();
}

MainWindow::~MainWindow()
{
    if (sources_changed) {
        progress->setLabelText(tr("Updating package sources..."));
        progress->show();
        QApplication::processEvents();

        shell->procAsRoot("apt-get", {"update"}, nullptr, nullptr, QuietMode::Yes);
    }

    delete ui;
}

void MainWindow::initializeIcons()
{
    setIconIfNull(ui->pushOk, "dialog-ok", ":/icons/dialog-ok.svg");
    setIconIfNull(ui->pushFastestMX, "cursor-arrow", ":/icons/cursor-arrow.svg");
    setIconIfNull(ui->pushFastestDebian, "cursor-arrow", ":/icons/cursor-arrow.svg");
}

void MainWindow::setIconIfNull(QPushButton *button, const QString &themeIcon, const QString &fallbackIcon)
{
    if (button->icon().isNull()) {
        button->setIcon(QIcon::fromTheme(themeIcon, QIcon(fallbackIcon)));
    }
}

void MainWindow::restoreWindowGeometry()
{
    QSize size = this->size();
    if (settings.contains("geometry")) {
        restoreGeometry(settings.value("geometry").toByteArray());
        if (isMaximized()) {
            resize(size);
            centerWindow();
        }
    }
}

void MainWindow::refresh(bool force)
{
    getCurrentRepo(force);
    displayMXRepos(repos, {});
    displayAllRepos(listAptFiles());
    ui->lineSearch->clear();
    ui->lineSearch->setFocus();
    ui->pushOk->setDisabled(true);
    radioSelectionChanged = false;
}

void MainWindow::replaceDebianRepos(const QString &url)
{
    if (!isValidRepositoryUrl(url)) {
        qWarning() << "Invalid repository URL:" << url;
        return;
    }

    const QStringList files {"/etc/apt/sources.list.d/debian.list", "/etc/apt/sources.list.d/debian.sources",
                             "/etc/apt/sources.list.d/debian-stable-updates.list",
                             "/etc/apt/sources.list.d/debian-stable-updates.sources", "/etc/apt/sources.list"};
    const QDir backupDir("/etc/apt/sources.list.d/backups");

    if (!backupDir.exists() && !Cmd().procAsRoot("mkdir", {"-p", backupDir.path()})) {
        qWarning() << "Failed to create backup directory:" << backupDir.path();
        return;
    }

    bool anyChange = false;
    bool failed = false;
    const QString trimmedUrl = url.trimmed().remove(QRegularExpression("/$"));
    QStringList changedFiles;
    QStringList changedFilesBackups;

    for (const QString &filePath : files) {
        if (!QFile::exists(filePath)) {
            continue;
        }

        QFile file(filePath);
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            qWarning() << "Could not open file:" << file.fileName() << file.errorString();
            continue;
        }

        QString content;
        QTextStream in(&file);
        QRegularExpression re {R"(((?:https?|ftp)://\S*?/debian))"};
        bool fileChanged = false;

        while (!in.atEnd()) {
            QString line = in.readLine().trimmed();
            // Don't replace security line since it might not be available on the mirror
            if (!line.contains("security")) {
                QString updatedLine = line;
                updatedLine.replace(re, trimmedUrl);
                if (updatedLine != line) {
                    fileChanged = true;
                    line = updatedLine;
                }
            }
            content.append(line + '\n');
        }
        file.close();

        if (!fileChanged) {
            continue;
        }

        QFileInfo fileInfo(filePath);
        const QString backupFilePath = backupDir.absoluteFilePath(
            fileInfo.fileName() + "." + QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss"));

        if (!Cmd().procAsRoot("cp", {filePath, backupFilePath})) {
            qWarning() << "Failed to backup" << filePath;
            failed = true;
            break;
        }

        // Track for rollback as soon as the backup exists: writeUpdatedFile() can fail after its
        // "mv" step already replaced the file's contents, so a false return doesn't mean filePath
        // is untouched.
        changedFiles << filePath;
        changedFilesBackups << backupFilePath;

        if (!writeUpdatedFile(filePath, content)) {
            failed = true;
            break;
        }

        anyChange = true;
    }

    if (failed) {
        qWarning() << "Rolling back" << changedFiles.size() << "file(s) after a failed repository update";
        bool rollbackFailed = false;
        for (int i = 0; i < changedFiles.size(); ++i) {
            if (!Cmd().procAsRoot("cp", {changedFilesBackups.at(i), changedFiles.at(i)})) {
                qWarning() << "Failed to roll back" << changedFiles.at(i) << "from" << changedFilesBackups.at(i);
                rollbackFailed = true;
            }
        }
        QMessageBox::critical(this, tr("Error"),
                              rollbackFailed ? tr("Failed to update the repository sources, and some changes could "
                                                  "not be automatically rolled back. Check %1 to restore manually.")
                                                    .arg(backupDir.path())
                                            : tr("Failed to update the repository sources. Any changes made have "
                                                 "been rolled back."));
        return;
    }

    if (anyChange) {
        sources_changed = true;
        QMessageBox::information(this, tr("Success"),
                                 tr("Your new selection will take effect the next time sources are updated."));
    } else {
        QMessageBox::information(this, tr("No Changes"),
                                 tr("The selected repository is already configured."));
    }
}

bool MainWindow::writeUpdatedFile(const QString &filePath, const QString &content)
{
    Cmd cmd;
    if (!cmd.installAsRoot(filePath, content.toUtf8())) {
        qWarning() << "Failed to replace the file and update permissions for" << filePath;
        return false;
    }
    return true;
}

QStringList MainWindow::readMXRepos()
{
    QTemporaryDir tmpdir;
    QFile file(tmpdir.path() + "/repos.txt");
    const QString remoteUrl = "https://raw.githubusercontent.com/MX-Linux/mx-repo-list/master/repos.txt";
    const QString localPath = "/usr/share/mx-repo-list/repos.txt";

    if (!downloadFile(remoteUrl, &file, 3s)) {
        qWarning() << "Could not download 'repos.txt' from GitHub, falling back to local file.";
        file.setFileName(localPath);
    }

    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qWarning() << "Could not open file:" << file.fileName() << file.errorString();
        return {};
    }

    QTextStream in(&file);
    QStringList fileContentList = in.readAll().trimmed().split('\n');
    file.close();

    fileContentList.sort(Qt::CaseInsensitive);

    QStringList list;
    std::copy_if(fileContentList.cbegin(), fileContentList.cend(), std::back_inserter(list),
                 [](const QString &line) { return !line.startsWith('#'); });

    extractUrls(list);
    repos = list;
    return list;
}

// Compares by exact host, not substring: "mxrepo.com" must not match "la.mxrepo.com".
bool MainWindow::repoEntryMatchesHost(const QString &repoLine, const QString &host)
{
    const QString url = repoLine.section(" - ", 1, 1).trimmed();
    return QUrl(url).host().compare(host, Qt::CaseInsensitive) == 0;
}

void MainWindow::getCurrentRepo(bool force)
{
    QFile file("/etc/apt/sources.list.d/mx.list");
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        file.setFileName("/etc/apt/sources.list.d/mx.sources");
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            qWarning() << "Could not open either mx.list or mx.sources file:" << file.errorString();
            return;
        }
    }

    QTextStream in(&file);

    QRegularExpression repoRegex {R"(^#?\s?deb\s+.*/repo[/]?)"};
    QRegularExpression urlRegex {"//(.*?)/"};
    QRegularExpression sourcesRegex {R"(URIs:\s*(.*?)(?:\s|$))"};

    QString line;
    while (in.readLineInto(&line)) {
        line = line.trimmed();

        if (file.fileName().endsWith("mx.sources")) {
            QRegularExpressionMatch match = sourcesRegex.match(line);
            if (match.hasMatch()) {
                current_repo = urlRegex.match(match.captured(1)).captured(1);
                break;
            }
        } else {
            if (repoRegex.match(line).hasMatch()) {
                QRegularExpressionMatch match = urlRegex.match(line);
                if (match.hasMatch()) {
                    current_repo = match.captured(1);
                    break;
                }
            }
        }
    }
    file.close();

    if (force || repos.isEmpty()) {
        readMXRepos();
    }

    bool repoFound = std::find_if(repos.cbegin(), repos.cend(),
                                  [this](const QString &item) { return repoEntryMatchesHost(item, current_repo); })
                     != repos.cend();

    if (!repoFound) {
        qDebug() << "Current repo not found in the up-to-date list of repos, it might have been removed as "
                    "non-working; selecting mxrepo.com as default";
        const QString defaultRepo = "http://mxrepo.com";
        if (replaceRepos(defaultRepo, true)) {
            current_repo = defaultRepo;
        }
    }
}

int MainWindow::getDebianVerNum()
{
    QFile file("/etc/debian_version");
    QString line;

    if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        QTextStream in(&file);
        if (!in.atEnd()) {
            line = in.readLine().trimmed();
        }
        file.close();
    } else {
        qCritical() << "Could not open /etc/debian_version:" << file.errorString() << ". Defaulting to Bullseye.";
        return Version::Bullseye;
    }

    if (line.isEmpty()) {
        qCritical() << "/etc/debian_version is empty. Defaulting to Bullseye.";
        return Version::Bullseye;
    }

    static const QHash<QString, Version> versionMap {
        {"bullseye", Version::Bullseye},
        {"bookworm", Version::Bookworm},
        {"trixie", Version::Trixie}
    };

    // Check named versions first
    for (auto it = versionMap.cbegin(); it != versionMap.cend(); ++it) {
        if (line.contains(it.key(), Qt::CaseInsensitive)) {
            return it.value();
        }
    }

    // Try numeric debian_versions such as "13.1" or "12" by extracting the major number and mapping to Version.
    QRegularExpression numberPattern{R"((\d+))"};
    QRegularExpressionMatch numberMatch = numberPattern.match(line);
    if (numberMatch.hasMatch()) {
        bool ok = false;
        int majorVersion = numberMatch.captured(1).toInt(&ok);
        if (ok && majorVersion >= Version::Bullseye && majorVersion < Version::MAX) {
            return static_cast<Version>(majorVersion);
        }
    }

    qCritical() << "Unrecognized or unparseable Debian version:" << line << ". Defaulting to Bullseye.";
    return Version::Bullseye;
}

QString MainWindow::getDebianVerName(int ver)
{
    static const QHash<int, QString> versionNames {
        {Version::Bullseye, "bullseye"}, {Version::Bookworm, "bookworm"}, {Version::Trixie, "trixie"}};

    if (versionNames.contains(ver)) {
        return versionNames.value(ver);
    } else {
        qWarning() << "Error: Invalid Debian version. Defaulting to Bullseye";
        return "bullseye";
    }
}

void MainWindow::displayMXRepos(const QStringList &repos, const QString &filter)
{
    ui->listWidget->clear();
    for (const QString &repo : repos) {
        if (!filter.isEmpty() && !repo.contains(filter, Qt::CaseInsensitive)) {
            continue;
        }
        QString country = repo.section('-', 0, 0).trimmed().section(',', 0, 0);
        auto *item = new QListWidgetItem(ui->listWidget);
        auto *radio = new QRadioButton(repo);
        radio->setIcon(getFlag(country));
        ui->listWidget->setItemWidget(item, radio);
        if (repoEntryMatchesHost(repo, current_repo)) {
            radio->setChecked(true);
            ui->listWidget->scrollToItem(item);
        }
        connect(radio, &QRadioButton::clicked, this, [this]() {
            radioSelectionChanged = true;
            ui->pushOk->setEnabled(true);
        });
    }
}

void MainWindow::displayAllRepos(const QFileInfoList &apt_files)
{
    ui->treeWidget->clear();
    ui->treeWidgetDeb->clear();
    ui->treeWidget->blockSignals(true);
    ui->treeWidgetDeb->blockSignals(true);

    const QStringList columnNames {tr("Lists"), tr("Sources (checked sources are enabled)")};
    ui->treeWidget->setHeaderLabels(columnNames);
    ui->treeWidgetDeb->setHeaderLabels(columnNames);

    auto processEntries = [](QTreeWidgetItem *parentItem, const auto &entries) {
        for (const auto &item : entries) {
            auto *childItem = new QTreeWidgetItem(parentItem, {QString(), item.first});
            childItem->setFlags(childItem->flags() | Qt::ItemIsUserCheckable);
            childItem->setCheckState(1, item.second ? Qt::Checked : Qt::Unchecked);
        }
    };

    for (const QFileInfo &file_info : apt_files) {
        QString file_name = file_info.fileName();
        QString file = file_info.absoluteFilePath();
        auto *topLevelItem = new QTreeWidgetItem(ui->treeWidget, {file_name});
        topLevelItem->setForeground(0, QBrush(Qt::darkGreen));
        QTreeWidgetItem *topLevelItemDeb = nullptr;

        if (file_name.contains("debian") || file_name == "sources.list") {
            topLevelItemDeb = new QTreeWidgetItem(ui->treeWidgetDeb, {file_name});
            topLevelItemDeb->setForeground(0, QBrush(Qt::darkGreen));
        }

        auto entries = loadAptFile(file);
        processEntries(topLevelItem, entries);
        if (topLevelItemDeb) {
            processEntries(topLevelItemDeb, entries);
        }
    }

    for (auto *treeWidget : {ui->treeWidget, ui->treeWidgetDeb}) {
        for (int i = 0; i < treeWidget->columnCount(); ++i) {
            treeWidget->resizeColumnToContents(i);
        }
        treeWidget->expandAll();
        treeWidget->blockSignals(false);
    }
}

QVector<QPair<QString, bool>> MainWindow::loadAptFile(const QString &file)
{
    QFile aptFile(file);
    if (!aptFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qWarning() << "Could not open file: " << file << " - " << aptFile.errorString();
        return {};
    }

    QVector<QPair<QString, bool>> entries;
    QTextStream in(&aptFile);
    bool isSources = QFileInfo(file).suffix() == "sources";
    if (isSources) {
        QString block;
        QRegularExpression enabledRegex("Enabled:\\s*no", QRegularExpression::CaseInsensitiveOption);

        auto processBlock = [&entries, &enabledRegex](const QString &block) {
            if (!block.isEmpty()) {
                bool blockEnabled = !block.contains(enabledRegex);
                entries.append({block.trimmed(), blockEnabled});
            }
        };

        while (!in.atEnd()) {
            QString line = in.readLine();
            if (line.trimmed().isEmpty()) {
                processBlock(block);
                block.clear();
            } else {
                block.append(line + "\n");
            }
        }
        processBlock(block); // Process final block
    } else {
        QRegularExpression re(R"(^\s*(?:#)?\s*deb\s+)");
        while (!in.atEnd()) {
            QString line = in.readLine().trimmed();
            if (re.match(line).hasMatch()) {
                bool enabled = !line.startsWith('#');
                entries.append({line, enabled});
            }
        }
    }

    return entries;
}

void MainWindow::cancelOperation()
{
    auto processId = shell->processId();
    if (processId != 0) {
        Cmd cmd;
        cmd.procAsRoot("kill", {});
        if (!shell->waitForFinished(1000)) {
            cmd.procAsRoot("kill", {"-9"});
        }
    }
    procDone();
}

void MainWindow::centerWindow()
{
    auto screenGeometry = QApplication::primaryScreen()->geometry();
    auto x = (screenGeometry.width() - width()) / 2;
    auto y = (screenGeometry.height() - height()) / 2;
    move(x, y);
}

void MainWindow::closeEvent([[maybe_unused]] QCloseEvent *event)
{
    settings.setValue("geometry", saveGeometry());
    event->accept();
}

void MainWindow::displaySelected(const QString &repo)
{
    for (int row = 0; row < ui->listWidget->count(); ++row) {
        auto *item = ui->listWidget->item(row);
        auto *radio = qobject_cast<QRadioButton *>(ui->listWidget->itemWidget(item));
        if (radio && radio->text().contains(repo, Qt::CaseInsensitive)) {
            radio->setChecked(true);
            ui->listWidget->scrollToItem(item);
            radioSelectionChanged = true;
            ui->pushOk->setEnabled(true);
            break;
        }
    }
}

void MainWindow::extractUrls(const QStringList &repos)
{
    listMXUrls.clear();
    listMXUrls.reserve(repos.size());
    for (const QString &line : repos) {
        QStringList linelist = line.split('-');
        if (linelist.size() > 1) {
            linelist.pop_front();
            QString joinedLine = linelist.join('-').trimmed();
            if (!joinedLine.isEmpty()) {
                listMXUrls.append(joinedLine);
            }
        }
    }
}

void MainWindow::setSelected()
{
    if (!radioSelectionChanged) {
        return;
    }
    for (int row = 0; row < ui->listWidget->count(); ++row) {
        auto *item = ui->listWidget->item(row);
        if (auto *radio = qobject_cast<QRadioButton *>(ui->listWidget->itemWidget(item)); radio && radio->isChecked()) {
            QString url = radio->text().section(" - ", 1, 1).trimmed();
            replaceRepos(url);
            break;
        }
    }
}

void MainWindow::procTime()
{
    bar->setValue((bar->value() + 1) % bar->maximum());
}

void MainWindow::procStart()
{
    bar->setValue(0);
    QApplication::setOverrideCursor(QCursor(Qt::BusyCursor));
    timer.start(100ms);
    connect(&timer, &QTimer::timeout, this, &MainWindow::procTime);
}

void MainWindow::procDone()
{
    bar->setValue(bar->maximum());
    timer.stop();
    timer.disconnect();
    QApplication::restoreOverrideCursor();
}

bool MainWindow::replaceRepos(const QString &url, bool quiet)
{
    if (!isValidRepositoryUrl(url)) {
        qWarning() << "Invalid repository URL:" << url;
        return false;
    }

    const QString trimmedUrl = url.trimmed().remove(QRegularExpression("/$"));
    const QString mx_list {"/etc/apt/sources.list.d/mx.list"};
    const QString mx_sources {"/etc/apt/sources.list.d/mx.sources"};

    enum class UpdateOutcome { NotApplicable, NoChangeNeeded, Changed, Failed };

    auto tryUpdate = [&](const QString &path, bool sourcesFormat) -> UpdateOutcome {
        QFile file(path);
        if (!file.exists()) {
            return UpdateOutcome::NotApplicable;
        }
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            qWarning() << "Could not open file:" << path << file.errorString();
            return UpdateOutcome::Failed;
        }
        QTextStream in(&file);
        QString content = in.readAll();
        file.close();

        QString updated = content;
        if (sourcesFormat) {
            static const QRegularExpression uriPattern(
                R"((URIs:\s*)(\S*)(/mx/(?:[.]?/)*repo/?|/mx/(?:[.]?/)*testrepo/?))",
                QRegularExpression::CaseInsensitiveOption);
            updated.replace(uriPattern, QStringLiteral("\\1%1\\3").arg(trimmedUrl));
        } else {
            static const QRegularExpression repoPattern(
                R"((deb(?:-src)?\s+(?:\[.*?\]\s+)?)(\S*)(/mx/(?:[.]?/)*repo/?|/mx/(?:[.]?/)*testrepo/?))",
                QRegularExpression::CaseInsensitiveOption);
            updated.replace(repoPattern, QStringLiteral("\\1%1\\3").arg(trimmedUrl));
        }
        updated.replace(QRegularExpression(R"([\t ]{2,})"), " ");
        updated.replace(QRegularExpression(R"([\t ]+$)", QRegularExpression::MultilineOption), "");

        if (updated == content) {
            return UpdateOutcome::NoChangeNeeded;
        }
        return writeUpdatedFile(path, updated) ? UpdateOutcome::Changed : UpdateOutcome::Failed;
    };

    // Both formats are examined independently and unconditionally -- nothing enforces that MX
    // installs ship only one of these, so either file could be the genuinely active repo
    // declaration, or both could be (e.g. around a format migration). Checking mx.sources only
    // when mx.list didn't change would leave a real, independently-active mx.sources untouched
    // whenever mx.list also happened to need (and successfully receive) an update.
    const UpdateOutcome listOutcome = tryUpdate(mx_list, false);
    const UpdateOutcome sourcesOutcome = tryUpdate(mx_sources, true);

    const bool anyChanged = listOutcome == UpdateOutcome::Changed || sourcesOutcome == UpdateOutcome::Changed;
    const bool anyFailed = listOutcome == UpdateOutcome::Failed || sourcesOutcome == UpdateOutcome::Failed;
    const bool anyExists = listOutcome != UpdateOutcome::NotApplicable
        || sourcesOutcome != UpdateOutcome::NotApplicable;

    if (anyChanged) {
        sources_changed = true;
    }

    // A failure on either file takes priority over the other succeeding: silently reporting
    // "Success" would hide that one of two active repo declarations was left stale.
    if (anyFailed) {
        if (!quiet) {
            QMessageBox::critical(this, tr("Error"), tr("Could not change the repo."));
        }
        return false;
    }
    if (anyChanged) {
        if (!quiet) {
            QMessageBox::information(this, tr("Success"),
                                     tr("Your new selection will take effect the next time sources are updated."));
        }
        return true;
    }
    if (anyExists) {
        // Already pointing at this mirror -- not a failure, so don't show "Could not change".
        if (!quiet) {
            QMessageBox::information(this, tr("No Changes"),
                                     tr("The selected repository is already configured."));
        }
        return true;
    }
    if (!quiet) {
        QMessageBox::critical(this, tr("Error"), tr("Could not change the repo."));
    }
    return false;
}

void MainWindow::setConnections()
{
    connect(ui->lineSearch, &QLineEdit::textChanged, this, &MainWindow::lineSearch_textChanged);
    connect(ui->pushRestoreSources, &QPushButton::clicked, this, &MainWindow::pushRestoreSources_clicked);
    connect(ui->pushAbout, &QPushButton::clicked, this, &MainWindow::pushAbout_clicked);
    connect(ui->pushCancel, &QPushButton::clicked, this, &MainWindow::close);
    connect(ui->pushFastestDebian, &QPushButton::clicked, this, &MainWindow::pushFastestDebian_clicked);
    connect(ui->pushFastestMX, &QPushButton::clicked, this, &MainWindow::pushFastestMX_clicked);
    connect(ui->pushHelp, &QPushButton::clicked, this, &MainWindow::pushHelp_clicked);
    connect(ui->pushOk, &QPushButton::clicked, this, &MainWindow::pushOk_clicked);
    connect(ui->tabWidget, &QTabWidget::currentChanged, this, &MainWindow::tabWidget_currentChanged);
    connect(ui->treeWidget, &QTreeWidget::itemChanged, this, &MainWindow::treeWidget_itemChanged);
    connect(ui->treeWidgetDeb, &QTreeWidget::itemChanged, this, &MainWindow::treeWidget_itemChanged);
}

void MainWindow::setProgressBar()
{
    progress = new QProgressDialog(this);
    bar = new QProgressBar(progress);
    progCancel = new QPushButton(tr("Cancel"));
    connect(progCancel, &QPushButton::clicked, this, &MainWindow::cancelOperation);
    progress->setWindowModality(Qt::WindowModal);
    progress->setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
                             | Qt::WindowStaysOnTopHint);
    progress->setCancelButton(progCancel);
    progress->setLabelText(tr("Please wait..."));
    progress->setAutoClose(false);
    progress->setBar(bar);
    bar->setTextVisible(false);
    progress->reset();
}

QFileInfoList MainWindow::listAptFiles()
{
    QFileInfoList list;
    const QDir apt_dir("/etc/apt/sources.list.d");
    list = apt_dir.entryInfoList(QStringList({"*.list", "*.sources"}), QDir::Files | QDir::Readable, QDir::Name);

    QFile file("/etc/apt/sources.list");
    if (file.exists() && file.size() > 0 && file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        QTextStream in(&file);
        QRegularExpression re(R"(^\s*deb)");
        while (!in.atEnd()) {
            QString line = in.readLine();
            if (re.match(line).hasMatch()) {
                list << QFileInfo(file);
                break;
            }
        }
        file.close();
    }

    return list;
}

void MainWindow::pushOk_clicked()
{
    bool appliedChanges = false;
    bool anyFailures = false;
    if (!queued_changes.isEmpty()) {
        for (const QStringList &changes : std::as_const(queued_changes)) {
            const QString &text = changes.at(0);
            const QString &new_text = changes.at(1);
            const QString &file_name = changes.at(2);

            QFile file(file_name);
            if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
                qWarning() << "Could not open file:" << file_name;
                anyFailures = true;
                continue;
            }

            QString content;
            QTextStream in(&file);
            content = in.readAll();
            file.close();

            QString updatedContent = content;
            updatedContent.replace(text, new_text);

            if (updatedContent == content) {
                continue;
            }

            if (shell->installAsRoot(file_name, updatedContent.toUtf8())) {
                appliedChanges = true;
            } else {
                qWarning() << "Failed to apply change to" << file_name;
                anyFailures = true;
            }
        }
        queued_changes.clear();
    }
    if (appliedChanges) {
        sources_changed = true;
    }
    setSelected();
    refresh();
    if (anyFailures) {
        QMessageBox::warning(this, tr("Some Changes Not Applied"),
                             tr("One or more repository changes could not be applied. Please check "
                                "the affected sources manually."));
    }
}

void MainWindow::pushAbout_clicked()
{
    hide();
    displayAboutMsgBox(
        tr("About %1").arg(windowTitle()),
        "<p align=\"center\"><b><h2>" + windowTitle() + "</h2></b></p><p align=\"center\">" + tr("Version: ")
            + QApplication::applicationVersion() + "</p><p align=\"center\"><h3>"
            + tr("Program for choosing the default APT repository")
            + R"(</h3></p><p align="center"><a href="http://mxlinux.org">http://mxlinux.org</a><br /></p><p align="center">)"
            + tr("Copyright (c) MX Linux") + "<br /><br /></p>",
        "/usr/share/doc/mx-repo-manager/license.html", tr("%1 License").arg(windowTitle()));
    show();
}

void MainWindow::pushHelp_clicked()
{
    QLocale locale;
    QString lang = locale.bcp47Name();
    QString url = "/usr/share/doc/mx-repo-manager/mx-repo-manager.html";

    if (lang.startsWith("fr")) {
        url = "https://mxlinux.org/wiki/help-files/help-mx-gestionnaire-de-d%C3%A9p%C3%B4ts";
    }

    displayHelpDoc(url, tr("%1 Help").arg(windowTitle()));
}

void MainWindow::treeWidget_itemChanged(QTreeWidgetItem *item, int column)
{
    ui->pushOk->setEnabled(true);
    ui->treeWidget->blockSignals(true);
    ui->treeWidgetDeb->blockSignals(true);

    const QString text = item->text(column);
    const QString file_name = item->parent()->text(0);
    const QString file_path
        = (file_name == "sources.list") ? "/etc/apt/" + file_name : "/etc/apt/sources.list.d/" + file_name;
    QString new_text = text;
    QStringList changes;

    if (text.contains("/mx/testrepo/") && item->checkState(column) == Qt::Checked) {
        QMessageBox::warning(this, tr("Warning"),
                             tr("You have selected MX Test Repo. It's not recommended to leave it enabled or to "
                                "upgrade all the packages from it.")
                                 + "\n\n"
                                 + tr("A safer option is to install packages individually with MX Package "
                                      "Installer."));
    }

    if (file_name.endsWith(".sources")) {
        if (item->checkState(column) == Qt::Checked) {
            new_text.replace(QRegularExpression("Enabled:\\s*no", QRegularExpression::CaseInsensitiveOption),
                             "Enabled: yes");
        } else {
            if (!new_text.contains(QRegularExpression("Enabled:", QRegularExpression::CaseInsensitiveOption))) {
                new_text.append("\nEnabled: no");
            } else {
                new_text.replace(QRegularExpression("Enabled:\\s*yes", QRegularExpression::CaseInsensitiveOption),
                                 "Enabled: no");
            }
        }
    } else {
        if (item->checkState(column) == Qt::Checked) {
            new_text.remove(QRegularExpression("#\\s*"));
        } else {
            new_text.prepend("# ");
        }
    }

    item->setText(column, new_text);
    changes << text << new_text << file_path;
    queued_changes << changes;

    ui->treeWidgetDeb->blockSignals(false);
    ui->treeWidget->blockSignals(false);
}

void MainWindow::tabWidget_currentChanged()
{
    if (ui->tabWidget->currentWidget() == ui->tabMX) {
        ui->label->setText(tr("Select the APT repository that you want to use:"));
    } else {
        ui->label->setText(tr("Select the APT repository and sources that you want to use:"));
    }
}

// Transform "country" name to 2-3 letter ISO 3166 country code and provide the QIcon for it
QIcon MainWindow::getFlag(QString country)
{
    static const QMap<QString, QString> specialCases = {{"The Netherlands", "Netherlands"},
                                                        {"USA", "UnitedStates"},
                                                        {"Anycast", "any"},
                                                        {"Any", "any"},
                                                        {"World", "any"}};

    if (specialCases.contains(country)) {
        if (country == "Anycast" || country == "Any" || country == "World") {
            return QIcon("/usr/share/flags-common/any.png");
        }
        country = specialCases[country];
    }

    QMetaEnum metaEnum = QMetaEnum::fromType<QLocale::Country>();
    int index = metaEnum.keyToValue(country.remove(' ').toUtf8());
    if (index == -1) {
        return {};
    }

    QList<QLocale> locales
        = QLocale::matchingLocales(QLocale::AnyLanguage, QLocale::AnyScript, static_cast<QLocale::Country>(index));
    if (!locales.isEmpty()) {
        QString short_name = locales.at(0).name().section('_', 1, 1).toLower();
        return QIcon("/usr/share/flags-common/" + short_name + ".png");
    }
    return {};
}

// Detect fastest Debian repo
void MainWindow::pushFastestDebian_clicked()
{
    if (!shell->procAsRoot("true", {}, nullptr, nullptr, QuietMode::Yes)) {
        QMessageBox::critical(this, tr("Error"), tr("Could not detect fastest repo."));
        return;
    }
    progress->show();

    QString ver_name = getDebianVerName(getDebianVerNum());
    if (ver_name == "buster" || ver_name == "bullseye") {
        ver_name.clear(); // netselect-apt doesn't like name buster/bullseye for some reason,
                          // maybe it expects "stable"
    }

    QStringList args;
    if (!ver_name.isEmpty()) {
        args << ver_name;
    }

    QString output;
    bool success = shell->procAsRoot("netselect-apt", args, &output);
    progress->hide();

    if (!success) {
        QMessageBox::critical(this, tr("Error"), tr("netselect-apt could not detect fastest repo."));
        return;
    }
    QString repo;
    const QStringList lines = output.split('\n');
    for (const QString &line : lines) {
        if (line.startsWith("deb ")) {
            repo = line.section(' ', 1, 1).trimmed();
            break;
        }
    }
    if (checkRepo(repo)) {
        replaceDebianRepos(repo);
        refresh();
    } else {
        QMessageBox::critical(this, tr("Error"), tr("Could not detect fastest repo."));
    }
}

// Detect and select the fastest MX repo
void MainWindow::pushFastestMX_clicked()
{
    if (!shell->procAsRoot("true", {}, nullptr, nullptr, QuietMode::Yes)) {
        QMessageBox::critical(this, tr("Error"), tr("Could not detect fastest repo."));
        return;
    }
    progress->show();
    QString out;
    bool success = shell->procAsRoot("netselect", QStringList {"-D", "-I"} + listMXUrls, &out);
    out = out.trimmed();
    // netselect's result ("<score> <url>") and its "unknown host" diagnostics for unreachable
    // mirrors both land in this same buffer (stdout and stderr merged), with no guaranteed
    // ordering between the two, so the result line isn't reliably the last one. Identify it by
    // shape instead of position, keeping the last match in case more than one line qualifies.
    static const QRegularExpression resultLine(R"(^\d+\s+(\S+)$)");
    QString selectedRepo;
    const QStringList lines = out.split('\n', Qt::SkipEmptyParts);
    for (const QString &line : lines) {
        const QRegularExpressionMatch match = resultLine.match(line.simplified());
        if (match.hasMatch()) {
            selectedRepo = match.captured(1);
        }
    }
    qDebug() << "FASTEST " << success << out;
    progress->hide();
    if (success && !selectedRepo.isEmpty()) {
        displaySelected(selectedRepo);
        pushOk_clicked();
    } else {
        QMessageBox::critical(this, tr("Error"), tr("Could not detect fastest repo."));
    }
}

void MainWindow::lineSearch_textChanged(const QString &arg1)
{
    displayMXRepos(repos, arg1);
}

void MainWindow::pushRestoreSources_clicked()
{
    // Check if running on antiX/MX
    if (!QFileInfo::exists("/etc/antix-version") && !QFileInfo::exists("/etc/mx-version")) {
        QMessageBox::critical(this, tr("Error"), tr("Can't figure out if this app is running on antiX or MX"));
        return;
    }

    bool ok = true;
    int mx_version = shell->getOut("grep -oP '(?<=DISTRIB_RELEASE=).*' /etc/lsb-release").left(2).toInt(&ok);
    if (!ok || mx_version < 18) {
        QMessageBox::critical(this, tr("Error"),
                              tr("MX version not detected or out of range: ") + QString::number(mx_version));
        return;
    }

    QTemporaryDir tmpdir;
    if (!tmpdir.isValid()) {
        qDebug() << "Could not create temp dir";
        return;
    }
    // Download source files from
    const QString url = QString("https://codeload.github.com/MX-Linux/mx-sources/zip/mx%1").arg(mx_version);
    QFile tofile(tmpdir.path() + "/" + QFileInfo(url).fileName() + ".zip");
    if (!downloadFile(url, &tofile)) {
        QMessageBox::critical(this, tr("Error"), tr("Could not download original APT files."));
        return;
    }

    // Extract master.zip to temp folder
    QString cmd = QString("unzip -q %1 -d %2")
                      .arg(Cmd::shellQuote(tofile.fileName()), Cmd::shellQuote(tmpdir.path()));
    if (!tofile.exists() || !shell->run(cmd)) {
        QMessageBox::critical(this, tr("Error"), tr("Could not unzip downloaded file."));
        return;
    }

    const QString aptUrisOut = shell->getOut("apt-get", {"update", "--print-uris"}, QuietMode::Yes);
    static const QRegularExpression ahsRx(QStringLiteral("/mx/([.]?/)*repo/.*/ahs/binary-amd64/Packages"));
    static const QRegularExpression mxRx(QStringLiteral("/mx/([.]?/)*repo/.*/main/binary-amd64/Packages"));

    bool enable_ahs = false;
    // For newer versions and 64-bit OS check if AHS was enabled
    if (mx_version >= 19 && shell->getOut("uname", {"-m"}, QuietMode::Yes).trimmed() == "x86_64") {
        if (ahsRx.match(aptUrisOut).hasMatch()) {
            enable_ahs = true;
            qDebug() << "AHS repo detected:" << enable_ahs;
        }
    }

    const bool enable_mx = mxRx.match(aptUrisOut).hasMatch();

    QString mx_list = QString("%1/mx-sources-mx%2/mx.list").arg(tmpdir.path(), QString::number(mx_version));
    QString mx_sources = QString("%1/mx-sources-mx%2/mx.sources").arg(tmpdir.path(), QString::number(mx_version));

    auto enableAHS = [&](const QString &file) {
        if (QFile::exists(file)) {
            if (file.endsWith(".list")) {
                cmd = QString("sed -i -r 's/^[[:space:]]*#[[:space:]#]*(deb.*[[:space:]]ahs)[[:space:]]*/\\1/' %1")
                          .arg(Cmd::shellQuote(file));
            } else {
                cmd = QString("sed -i -r "
                              "'/Components:.*ahs/!s/^[[:space:]]*Components:[[:space:]]*\\[([^]]*?)\\][[:space:]]*$/"
                              "Components: [\\1 ahs]/' %1")
                          .arg(Cmd::shellQuote(file));
            }
            shell->run(cmd);
        }
    };

    QString &file = QFile::exists(mx_list) ? mx_list : mx_sources;

    if (enable_ahs) {
        enableAHS(file);
    } else if (mx_version >= 19 && shell->getOut("uname", {"-m"}, QuietMode::Yes).trimmed() == "x86_64" && !enable_mx) {
        if (QMessageBox::Yes
            == QMessageBox::question(this, tr("Enabling AHS"), tr("Do you use AHS (Advanced Hardware Stack) repo?"))) {
            enableAHS(file);
        }
    }

    // Move the sources list files from the temporary directory to /etc/apt/sources.list.d/
    const QDir restoredSourcesDir(QString("%1/mx-sources-mx%2").arg(tmpdir.path(), QString::number(mx_version)));
    const QFileInfoList restoredFiles
        = restoredSourcesDir.entryInfoList({"*.list", "*.sources"}, QDir::Files | QDir::Readable, QDir::Name);

    const QDir backupDir("/etc/apt/sources.list.d/backups");
    if (!backupDir.exists() && !Cmd().procAsRoot("mkdir", {"-p", backupDir.path()})) {
        qWarning() << "Failed to create backup directory:" << backupDir.path();
        QMessageBox::critical(this, tr("Error"), tr("Could not restore the original APT source files."));
        return;
    }

    bool restoreSuccess = !restoredFiles.isEmpty();
    QStringList installedTargets;
    QHash<QString, QString> targetBackups; // only present for targets that existed before this restore

    for (const QFileInfo &sourceInfo : restoredFiles) {
        const QString targetPath = QString("/etc/apt/sources.list.d/%1").arg(sourceInfo.fileName());

        if (QFile::exists(targetPath)) {
            const QString backupFilePath = backupDir.absoluteFilePath(
                sourceInfo.fileName() + "." + QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss"));
            if (!Cmd().procAsRoot("cp", {targetPath, backupFilePath})) {
                qWarning() << "Failed to backup" << targetPath;
                restoreSuccess = false;
                break;
            }
            targetBackups.insert(targetPath, backupFilePath);
        }

        QFile sourceFile(sourceInfo.absoluteFilePath());
        if (!sourceFile.open(QIODevice::ReadOnly)) {
            qWarning() << "Could not read restored file:" << sourceInfo.absoluteFilePath();
            restoreSuccess = false;
            break;
        }
        const QByteArray content = sourceFile.readAll();
        sourceFile.close();

        restoreSuccess = shell->installAsRoot(targetPath, content, true);
        if (!restoreSuccess) {
            break;
        }
        installedTargets << targetPath;
    }

    if (!restoreSuccess && !installedTargets.isEmpty()) {
        qWarning() << "Rolling back" << installedTargets.size() << "restored file(s) after failure";
        for (const QString &targetPath : std::as_const(installedTargets)) {
            const auto backupIt = targetBackups.constFind(targetPath);
            if (backupIt != targetBackups.constEnd()) {
                if (!Cmd().procAsRoot("cp", {backupIt.value(), targetPath})) {
                    qWarning() << "Failed to roll back" << targetPath;
                }
            } else if (!Cmd().procAsRoot("rm", {"-f", targetPath})) {
                qWarning() << "Failed to remove newly-restored file during rollback:" << targetPath;
            }
        }
    }

    if (restoreSuccess) {
        refresh(true);
        QMessageBox::information(this, tr("Success"),
                                 tr("Original APT sources have been restored to the release status. User added "
                                    "source files in /etc/apt/sources.list.d/ have not been touched.")
                                     + "\n\n"
                                     + tr("Your new selection will take effect the next time sources are updated."));
        sources_changed = true;
    } else {
        QMessageBox::critical(this, tr("Error"), tr("Could not restore the original APT source files."));
    }
}

bool MainWindow::isValidRepositoryUrl(const QString &url)
{
    if (url.trimmed().isEmpty()) {
        return false;
    }

    QUrl qurl(url.trimmed());
    if (!qurl.isValid() || qurl.host().isEmpty()) {
        return false;
    }

    // Only allow http, https, and ftp schemes
    const QString scheme = qurl.scheme().toLower();
    if (scheme != "http" && scheme != "https" && scheme != "ftp") {
        return false;
    }

    // Basic length check (reasonable URL length)
    if (url.length() > 2048) {
        return false;
    }

    return true;
}

void MainWindow::setupNetworkProxy(QNetworkAccessManager &manager, const QUrl &url)
{
    QNetworkProxyQuery query {url};
    QList<QNetworkProxy> proxies = QNetworkProxyFactory::systemProxyForQuery(query);
    if (!proxies.isEmpty()) {
        manager.setProxy(proxies.first());
    }
}

bool MainWindow::checkRepo(const QString &repo)
{
    QNetworkRequest request;
    request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
    request.setRawHeader(
        "User-Agent",
        QString("%1/%2 (linux-gnu)").arg(QApplication::applicationName(), QApplication::applicationVersion()).toUtf8());

    QNetworkAccessManager manager;
    setupNetworkProxy(manager, QUrl(repo));

    request.setUrl(QUrl(repo));
    QNetworkReply *reply = manager.head(request);
    QEventLoop loop;
    connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
    connect(reply, &QNetworkReply::errorOccurred, &loop, &QEventLoop::quit);
    manager.setTransferTimeout(5000);
    loop.exec();
    reply->disconnect();

    if (reply->error() == QNetworkReply::NoError) {
        return true;
    }

    qDebug() << "No response from repo:" << reply->url() << reply->error();
    return false;
}

bool MainWindow::downloadFile(const QString &url, QFile *file, std::chrono::seconds timeout)
{
    if (!file->open(QIODevice::WriteOnly)) {
        qWarning() << "Could not open file:" << file->fileName() << file->errorString();
        return false;
    }

    QNetworkAccessManager manager;
    setupNetworkProxy(manager, QUrl(url));

    QNetworkRequest request {QUrl(url)};
    request.setRawHeader(
        "User-Agent",
        QString("%1/%2 (linux-gnu)").arg(QApplication::applicationName(), QApplication::applicationVersion()).toUtf8());

    manager.setTransferTimeout(std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count());

    QNetworkReply *reply = manager.get(request);
    QEventLoop loop;

    connect(reply, &QNetworkReply::readyRead, this, [&file, reply]() {
        if (file->write(reply->readAll()) == -1) {
            qWarning() << "Failed to write data to file:" << file->fileName();
            reply->abort();
            file->close();
            file->remove();
        }
    });
    connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
    connect(reply, &QNetworkReply::errorOccurred, &loop, &QEventLoop::quit);

    loop.exec();
    file->close();

    if (reply->error() != QNetworkReply::NoError) {
        qWarning() << "Failed to download file:" << url << reply->errorString();
        file->remove();
        return false;
    }
    return true;
}
