/*
 * iDescriptor: A free and open-source idevice management tool.
 *
 * Copyright (C) 2025 Uncore <https://github.com/uncor3>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

#include "mainwindow.h"
#include "appswidget.h"
#include "devicemanagerwidget.h"
#include "ifusediskunmountbutton.h"
#include "ifusemanager.h"
#include "jailbrokenwidget.h"
#include "releasechangelogdialog.h"
#include "toolboxwidget.h"
#include "welcomewidget.h"
#include <unistd.h>

#include "appcontext.h"
#include "networkdeviceswidget.h"
#include "settingsmanager.h"
#include "statusballoon.h"

#ifdef WIN32
#include "platform/windows/win_common.h"
#endif

#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
void handleCallbackRecovery(const irecv_device_event_t *event, void *userData)
{

    switch (event->type) {
    case IRECV_DEVICE_ADD:
        qDebug() << "Recovery device added: ";
        QMetaObject::invokeMethod(AppContext::sharedInstance(),
                                  "addRecoveryDevice", Qt::QueuedConnection,
                                  Q_ARG(uint64_t, event->device_info->ecid));
        break;
    case IRECV_DEVICE_REMOVE:
        qDebug() << "Recovery device removed: ";
        QMetaObject::invokeMethod(AppContext::sharedInstance(),
                                  "removeRecoveryDevice", Qt::QueuedConnection,
                                  Q_ARG(uint64_t, event->device_info->ecid));
        break;
    default:
        printf("Unhandled recovery event: %d\n", event->type);
    }
}
irecv_device_event_context_t context;
#endif

void handleCallback(int code, const QString &udid, const QString &info)
{
    qDebug() << "Device event: "
             << (code == 1   ? "Connected"
                 : code == 2 ? "Disconnected"
                 : code == 3 ? "Pairing Pending"
                 : code == 4 ? "Pairing Failed"
                             : "Unknown")
             << ", UDID: " << udid;
    AddType addType;

    switch (code) {
    case 1: { // fully connected
        addType = AddType::Regular;
        break;
    }
    case 2: { // disconnected
        QMetaObject::invokeMethod(
            AppContext::sharedInstance(), "removeDevice", Qt::QueuedConnection,
            Q_ARG(iDescriptor::Uniq, iDescriptor::Uniq(udid)));
        return;
    }
    case 3: { // pairing pending
        addType = AddType::Pairing;
        break;
    }
    case 4: { // pairing failed
        addType = AddType::FailedToPair;
        break;
    }
    }

    QMetaObject::invokeMethod(
        AppContext::sharedInstance(), "addDevice", Qt::QueuedConnection,
        Q_ARG(iDescriptor::Uniq, iDescriptor::Uniq(udid)),
        Q_ARG(iDescriptor::IdeviceConnectionType,
              static_cast<iDescriptor::IdeviceConnectionType>(
                  iDescriptor::CONNECTION_USB)),
        Q_ARG(AddType, addType), Q_ARG(QString, info));
}

MainWindow *MainWindow::sharedInstance()
{
    static MainWindow instance;
    return &instance;
}

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
{
    setMinimumSize(MIN_MAIN_WINDOW_SIZE);
    resize(MIN_MAIN_WINDOW_SIZE);
    setContentsMargins(0, 0, 0, 0);

    QWidget *centralWidget = new QWidget(this);
    setCentralWidget(centralWidget);
    auto mainLayout = new QVBoxLayout(centralWidget);
    mainLayout->setContentsMargins(0, 0, 0, 0);

    auto core = AppContext::sharedInstance()->core;

    connect(core, &CXX::Core::device_event, this,
            [this](uint32_t code, const QString &udid, const QString &info) {
                handleCallback(code, udid, info);
            });

    connect(core, &CXX::Core::sleepy_time_detected, this,
            &MainWindow::handleShowSleepyDeviceWarning);

    AppContext::sharedInstance()->core->init();

    m_ZTabWidget = new ZTabWidget(this);
#ifdef __APPLE__
    setupMacOSWindow(this);
    setAttribute(Qt::WA_ContentsMarginsRespectsSafeArea, false);
#endif
    mainLayout->addWidget(m_ZTabWidget);
#ifdef WIN32
    setupWinWindow(this);
#endif

    m_mainStackedWidget = new QStackedWidget();
    WelcomeWidget *welcomePage = new WelcomeWidget(this);
    m_deviceManager = new DeviceManagerWidget(this);

    m_mainStackedWidget->addWidget(welcomePage);
    m_mainStackedWidget->addWidget(m_deviceManager);

    connect(m_deviceManager, &DeviceManagerWidget::updateNoDevicesConnected,
            this, &MainWindow::updateNoDevicesConnected);

    m_ZTabWidget->addTab(m_mainStackedWidget, "iDevice");
    auto *appsWidgetTab =
        m_ZTabWidget->addTab(AppsWidget::sharedInstance(), "Apps");
    m_ZTabWidget->addTab(ToolboxWidget::sharedInstance(), "Toolbox");
    auto *jailbrokenWidget = new JailbrokenWidget(this);
    m_ZTabWidget->addTab(jailbrokenWidget, "Jailbroken");
    m_ZTabWidget->finalizeStyles();

    connect(
        appsWidgetTab, &ZTab::clicked, this,
        [this](int index) { AppsWidget::sharedInstance()->init(); },
        Qt::SingleShotConnection);

    // settings button
    ZIconWidget *settingsButton = new ZIconWidget(
        QIcon(":/resources/icons/MingcuteSettings7Line.png"), "Settings");
    settingsButton->setCursor(Qt::PointingHandCursor);
    connect(settingsButton, &ZIconWidget::clicked, this, [this]() {
        SettingsManager::sharedInstance()->showSettingsDialog();
    });

    ZIconWidget *githubButton = new ZIconWidget(
        QIcon(":/resources/icons/MdiGithub.png"), "iDescriptor on GitHub");
    githubButton->setCursor(Qt::PointingHandCursor);
    connect(githubButton, &ZIconWidget::clicked, this,
            []() { QDesktopServices::openUrl(QUrl(REPO_URL)); });

    m_connectedDeviceCountLabel = new QLabel("iDescriptor: no devices");
    m_connectedDeviceCountLabel->setContentsMargins(0, 0, 0, 0);
    m_connectedDeviceCountLabel->setStyleSheet(
        "QLabel:hover { background-color : #13131319; }");

    QWidget *statusbar = new QWidget();
    QHBoxLayout *statusLayout = new QHBoxLayout(statusbar);
    statusLayout->setContentsMargins(10, 0, 10, 0);
    statusLayout->setSpacing(1);
    statusbar->setObjectName("StatusBar");
    statusbar->setStyleSheet(
        "QWidget#StatusBar { background-color: transparent; }");
    statusLayout->addWidget(m_connectedDeviceCountLabel);

    StatusBalloon *statusBalloon = StatusBalloon::sharedInstance();

    statusLayout->addWidget(statusBalloon->getButton());

    ZIconWidget *welcomeMenuSwitch = new ZIconWidget(
        QIcon(":/resources/icons/LetsIconsHorizontalDownLeftMainLight.png"),
        "Switch to Welcome Menu");
    connect(welcomeMenuSwitch, &ZIconWidget::clicked, this,
            [this, welcomeMenuSwitch]() {
                if (m_mainStackedWidget->currentIndex() != 0) {
                    welcomeMenuSwitch->setToolTip(
                        "Switch to Connected Devices");
                    return showWelcomeTab();
                }
                welcomeMenuSwitch->setToolTip("Switch to Welcome Menu");
                showConnectedDevicesTab();
            });

    connect(m_ZTabWidget, &ZTabWidget::currentChanged, this,
            [welcomeMenuSwitch](int index) {
                if (index != 0) {
                    return welcomeMenuSwitch->hide();
                }

                welcomeMenuSwitch->show();
            });

    statusLayout->addWidget(welcomeMenuSwitch);
    statusLayout->addStretch(1);

    QLabel *appVersionLabel = new QLabel(QString("v%1").arg(APP_VERSION));
    appVersionLabel->setContentsMargins(5, 0, 5, 0);
    appVersionLabel->setStyleSheet(
        "QLabel:hover { background-color : #13131319; }");
    statusLayout->addWidget(appVersionLabel);
    statusLayout->addWidget(githubButton);
    statusLayout->addWidget(settingsButton);

    mainLayout->addWidget(statusbar);

#ifdef __linux__
    QList<QString> mounted_iFusePaths = iFuseManager::getMountPoints();

    for (const QString &path : mounted_iFusePaths) {
        auto *p = new iFuseDiskUnmountButton(path);

        statusLayout->addWidget(p);
        connect(p, &iFuseDiskUnmountButton::clicked, this,
                [this, p, path, statusLayout]() {
                    bool ok = iFuseManager::linuxUnmount(path);
                    if (!ok) {
                        QMessageBox::warning(nullptr, "Unmount Failed",
                                             "Failed to unmount iFuse at " +
                                                 path + ". Please try again.");
                        return;
                    }
                    statusLayout->removeWidget(p);
                    p->deleteLater();
                });
    }
#endif

#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
    irecv_error_t res_recovery =
        irecv_device_event_subscribe(&context, handleCallbackRecovery, nullptr);

    if (res_recovery != IRECV_E_SUCCESS) {
        qDebug() << "ERROR: Unable to subscribe to recovery device events. "
                    "Error code:"
                 << res_recovery;
    }
    qDebug() << "Subscribed to recovery device events successfully.";
#endif

    createMenus();

    UpdateProcedure updateProcedure;
    bool packageManagerManaged = false;
    bool isPortable = false;
    bool skipPrerelease = true;
#ifdef WIN32
    isPortable = !is_iDescriptorInstalled();
    qDebug() << "isPortable=" << isPortable;
#endif

    /*
    struct UpdateProcedure {
        bool openFile;
        bool openFileDir;
        bool quitApp;
        QString boxInformativeText;
        QString boxText;
    };
    */
    switch (ZUpdater::detectPlatform()) {
    case Platform::Windows:
        updateProcedure = UpdateProcedure{
            !isPortable,
            isPortable,
            !isPortable,
            isPortable ? "New portable version downloaded, app location will "
                         "be shown after this message"
                       : "The application will now quit to install the update.",
            isPortable ? "New portable version downloaded"
                       : "Do you want to install the downloaded update now?",
        };
        break;
        // todo: adjust for pkg managers
    case Platform::MacOS:
        updateProcedure = UpdateProcedure{
            true,
            false,
            true,
            "The application will now quit and open .dmg file downloaded to "
            "\"Downloads\" from there you can drag it to Applications to "
            "install.",
            "Update downloaded would you like to quit and install the update?",
        };
        break;
    case Platform::Linux:
        // currently only on linux (arch aur) is enabled
#ifdef PACKAGE_MANAGER_MANAGED
        packageManagerManaged = true;
#endif
        updateProcedure = UpdateProcedure{
            true,
            false,
            true,
            "AppImages we ship are not updateable. New version is downloaded "
            "to "
            "\"Downloads\". You can start using the new version by launching "
            "it "
            "from there. You can delete this AppImage version if you like.",
            "Update downloaded would you like to quit and open the new "
            "version?",
        };
        break;
    default:
        updateProcedure = UpdateProcedure{
            false, false, false, "", "",
        };
    }

    m_updater = new ZUpdater("iDescriptor/iDescriptor", APP_VERSION,
                             "iDescriptor", updateProcedure, isPortable,
                             packageManagerManaged, skipPrerelease, this);
#if defined(PACKAGE_MANAGER_MANAGED) && defined(__linux__)
    m_updater->setPackageManagerManagedMessage(
        QString(
            "You seem to have installed iDescriptor using a package manager. "
            "Please use %1 to update it.")
            .arg(PACKAGE_MANAGER_HINT));
#endif

    QString lastAppVersion = SettingsManager::sharedInstance()->appVersion();
    bool shouldShowReleaseChangelog = lastAppVersion != APP_VERSION;
    SettingsManager::sharedInstance()->setAppVersion(APP_VERSION);

    if (shouldShowReleaseChangelog) {
        connect(
            m_updater, &ZUpdater::dataAvailable, this,
            [this](const QJsonDocument data, bool isUpdateAvailable) {
                if (!isUpdateAvailable) {
                    ReleaseChangelogDialog dialog(data, this);
                    dialog.exec();
                }
            },
            Qt::SingleShotConnection);
    }

    SettingsManager::sharedInstance()->doIfEnabled(
        SettingsManager::Setting::AutoCheckUpdates, [this]() {
            qDebug() << "Checking for updates...";
            m_updater->checkForUpdates();
        });

    // ═══════════════════════════════════════════════════════════════════════
    //  Upgrade to wireless when a "WIRED" device is removed
    // ═══════════════════════════════════════════════════════════════════════
    connect(
        AppContext::sharedInstance(), &AppContext::deviceRemoved, this,
        [](const QString &udid, const std::string &wifiMacAddress,
           bool wasWireless) {
            if (wasWireless)
                return;
            QString wifiMac = QString::fromStdString(wifiMacAddress);
            NetworkDevice dev =
                NetworkDeviceProvider::sharedInstance()->getNetworkDeviceByMac(
                    wifiMac);
            if (!dev.isValid()) {
                qDebug() << "No valid network device found for UDID" << udid
                         << "with Wi-Fi MAC" << wifiMac
                         << "Not trying to upgrade to wireless connection.";
                return;
            }

            qDebug()
                << "Trying to upgrade device to wireless connection for UDID"
                << udid;
            // FIXME: maybe ignore iOS 15 and lower?
            AppContext::sharedInstance()->tryToConnectToNetworkDevice(dev);
        });

    // ═══════════════════════════════════════════════════════════════════════
    //  Add a wireless device
    // ═══════════════════════════════════════════════════════════════════════
    connect(
        NetworkDeviceProvider::sharedInstance(),
        &NetworkDeviceProvider::deviceAdded, this,
        [this](const NetworkDevice &device) {
            if (!SettingsManager::sharedInstance()
                     ->autoConnectWirelessDevices())
                return;
            if (auto existingDevice =
                    AppContext::sharedInstance()->getDeviceByMacAddress(
                        device.macAddress)) {
                if (existingDevice->deviceInfo.isWireless) {
                    qDebug()
                        << "Ignoring wireless device with MAC:"
                        << device.macAddress << "as it's already initialized";

                } else {
                    qDebug() << "Prefering wired connection on device with MAC:"
                             << device.macAddress;
                }

                return;
            }
            AppContext::sharedInstance()->tryToConnectToNetworkDevice(device);
        });
}

void MainWindow::createMenus()
{
#ifdef Q_OS_MAC
    QMenu *actionsMenu = menuBar()->addMenu("&Actions");

    QAction *aboutAct = new QAction("&About iDescriptor", this);
    connect(aboutAct, &QAction::triggered, this, [this]() {
        QMessageBox::about(this, "iDescriptor",
                           "A free, open-source, and cross-platform "
                           "iDevice management tool.");
    });
    actionsMenu->addAction(aboutAct);
#endif
}

void MainWindow::updateNoDevicesConnected()
{
    qDebug() << "No devices connected? "
             << AppContext::sharedInstance()->noDevicesConnected();
    if (AppContext::sharedInstance()->noDevicesConnected()) {

        m_connectedDeviceCountLabel->setText("iDescriptor: no devices");
        return m_mainStackedWidget->setCurrentIndex(0); // Show Welcome page
    }
    int deviceCount = AppContext::sharedInstance()->getConnectedDeviceCount();
    m_connectedDeviceCountLabel->setText(
        "iDescriptor: " + QString::number(deviceCount) +
        (deviceCount == 1 ? " device" : " devices") + " connected");
    m_mainStackedWidget->setCurrentIndex(1); // Show device list page
}

void MainWindow::closeEvent(QCloseEvent *event)
{
    if (StatusBalloon::sharedInstance()->hasActiveProcesses()) {
        auto reply = QMessageBox::question(
            this, tr("Transfers in Progress"),
            tr("There are import/export operations in progress.\n"
               "Do you really want to quit? This will cancel them."),
            QMessageBox::Yes | QMessageBox::No, QMessageBox::No);

        if (reply == QMessageBox::No) {
            event->ignore();
            return;
        }
        IOManagerClient::sharedInstance()->cancelAllJobs();
    }

    QMainWindow::closeEvent(event);
}

void MainWindow::raiseDeviceTab()
{
    m_ZTabWidget->setCurrentIndex(0);
    raise();
    activateWindow();
}

void MainWindow::showConnectedDevicesTab()
{
    m_mainStackedWidget->setCurrentIndex(1);
}

void MainWindow::showWelcomeTab() { m_mainStackedWidget->setCurrentIndex(0); }

MainWindow::~MainWindow()
{
#ifdef ENABLE_RECOVERY_DEVICE_SUPPORT
    irecv_device_event_unsubscribe(context);
#endif
    delete m_updater;
    // sleep(2);
}

void MainWindow::handleShowSleepyDeviceWarning()
{
    static bool widgetAlreadyVisible = false;
    /* one minute cooldown to prevent spamming */
    static const int kMinIntervalMs = 60 * 1000;

    static QDateTime lastShown;
    const auto now = QDateTime::currentDateTimeUtc();

    if (lastShown.isValid() && lastShown.msecsTo(now) < kMinIntervalMs) {
        qDebug() << "Ignoring sleepy device warning dialog.";
        return;
    }

    lastShown = now;

    if (SettingsManager::sharedInstance()->isSleepyDeviceWarningDismissed() ||
        widgetAlreadyVisible) {
        return;
    }

    widgetAlreadyVisible = true;
    DeviceSleepWarningWidget(this).exec();
    widgetAlreadyVisible = false;
}