#include <set>
#include <sys/mount.h>
#include <sys/sysmacros.h>
#include <libgen.h>

#include <base.hpp>
#include <flags.h>
#include <consts.hpp>

#include "init.hpp"

using namespace std;

struct devinfo {
    int major;
    int minor;
    char devname[32];
    char partname[32];
    char dmname[32];
    char devpath[PATH_MAX];
};

static vector<devinfo> dev_list;

// When this boolean is set, this means we are currently
// running magiskinit on legacy SAR AVD emulator
bool avd_hack = false;

static void parse_device(devinfo *dev, const char *uevent) {
    dev->partname[0] = '\0';
    dev->devpath[0] = '\0';
    dev->dmname[0] = '\0';
    dev->devname[0] = '\0';
    parse_prop_file(uevent, [=](string_view key, string_view value) -> bool {
        if (key == "MAJOR")
            dev->major = parse_int(value.data());
        else if (key == "MINOR")
            dev->minor = parse_int(value.data());
        else if (key == "DEVNAME")
            strscpy(dev->devname, value.data(), sizeof(dev->devname));
        else if (key == "PARTNAME")
            strscpy(dev->partname, value.data(), sizeof(dev->devname));

        return true;
    });
}

void MagiskInit::collect_devices() {
    char path[PATH_MAX];
    devinfo dev{};
    if (auto dir = xopen_dir("/sys/dev/block"); dir) {
        for (dirent *entry; (entry = readdir(dir.get()));) {
            if (entry->d_name == "."sv || entry->d_name == ".."sv)
                continue;
            sprintf(path, "/sys/dev/block/%s/uevent", entry->d_name);
            parse_device(&dev, path);
            sprintf(path, "/sys/dev/block/%s/dm/name", entry->d_name);
            if (access(path, F_OK) == 0) {
                auto name = rtrim(full_read(path));
                strscpy(dev.dmname, name.data(), sizeof(dev.dmname));
            }
            if (auto it = std::ranges::find_if(config.partition_map, [&](const auto &i) {
                return i.first == dev.devname;
            }); dev.partname[0] == '\0' && it != config.partition_map.end()) {
                // use androidboot.partition_map as partname fallback.
                strscpy(dev.partname, it->second.data(), sizeof(dev.partname));
            }
            sprintf(path, "/sys/dev/block/%s", entry->d_name);
            xrealpath(path, dev.devpath, sizeof(dev.devpath));
            dev_list.push_back(dev);
        }
    }
}

dev_t MagiskInit::find_block(const char *partname) {
    if (dev_list.empty())
        collect_devices();

    for (int tries = 0; tries < 3; ++tries) {
        for (auto &dev : dev_list) {
            const char *name;
            if (strcasecmp(dev.partname, partname) == 0)
                name = dev.partname;
            else if (strcasecmp(dev.dmname, partname) == 0)
                name = dev.dmname;
            else if (strcasecmp(dev.devname, partname) == 0)
                name = dev.devname;
            else if (std::string_view(dev.devpath).ends_with("/"s + partname))
                name = dev.devpath;
            else
                continue;

            LOGD("Found %s: [%s] (%d, %d)\n", name, dev.devname, dev.major, dev.minor);
            return makedev(dev.major, dev.minor);
        }
        // Wait 10ms and try again
        usleep(10000);
        dev_list.clear();
        collect_devices();
    }

    // The requested partname does not exist
    return 0;
}

void MagiskInit::mount_preinit_dir() {
    if (preinit_dev.empty()) return;
    auto dev = find_block(preinit_dev.data());
    if (dev == 0) {
        LOGE("Cannot find preinit %s, abort!\n", preinit_dev.data());
        return;
    }
    xmknod(PREINITDEV, S_IFBLK | 0600, dev);
    xmkdir(MIRRDIR, 0);
    bool mounted = false;
    // First, find if it is already mounted
    std::string mnt_point;
    if (rust::is_device_mounted(dev, mnt_point)) {
        // Already mounted, just bind mount
        xmount(mnt_point.data(), MIRRDIR, nullptr, MS_BIND, nullptr);
        mounted = true;
    }

    // Since we are mounting the block device directly, make sure to ONLY mount the partitions
    // as read-only, or else the kernel might crash due to crappy drivers.
    // After the device boots up, magiskd will properly bind mount the correct partition
    // on to PREINITMIRR as writable. For more details, check bootstages.cpp
    if (mounted || mount(PREINITDEV, MIRRDIR, "ext4", MS_RDONLY, nullptr) == 0 ||
        mount(PREINITDEV, MIRRDIR, "f2fs", MS_RDONLY, nullptr) == 0) {
        string preinit_dir = resolve_preinit_dir(MIRRDIR);
        // Create bind mount
        xmkdirs(PREINITMIRR, 0);
        if (access(preinit_dir.data(), F_OK)) {
            LOGW("empty preinit: %s\n", preinit_dir.data());
        } else {
            LOGD("preinit: %s\n", preinit_dir.data());
            xmount(preinit_dir.data(), PREINITMIRR, nullptr, MS_BIND, nullptr);
        }
        xumount2(MIRRDIR, MNT_DETACH);
    } else {
        PLOGE("Failed to mount preinit %s\n", preinit_dev.data());
        unlink(PREINITDEV);
    }
}

bool MagiskInit::mount_system_root() {
    LOGD("Mounting system_root\n");

    // there's no /dev in stub cpio
    xmkdir("/dev", 0777);

    dev_t dev;
    do {
        // Try legacy SAR dm-verity
        dev = find_block("vroot");
        if (dev > 0)
            goto mount_root;

        // Try NVIDIA naming scheme
        dev = find_block("APP");
        if (dev > 0)
            goto mount_root;

        // Try normal partname
        char sys_part[32];
        sprintf(sys_part, "system%s", config.slot);
        dev = find_block(sys_part);
        if (dev > 0)
            goto mount_root;

        // Poll forever if rootwait was given in cmdline
    } while (config.rootwait);

    // We don't really know what to do at this point...
    LOGE("Cannot find root partition, abort\n");
    exit(1);

mount_root:
    xmknod("/dev/root", S_IFBLK | 0600, dev);
    xmkdir("/system_root", 0755);

    if (xmount("/dev/root", "/system_root", "ext4", MS_RDONLY, nullptr)) {
        if (xmount("/dev/root", "/system_root", "erofs", MS_RDONLY, nullptr)) {
            // We don't really know what to do at this point...
            LOGE("Cannot mount root partition, abort\n");
            exit(1);
        }
    }

    rust::switch_root("/system_root");

    // Make dev writable
    xmount("tmpfs", "/dev", "tmpfs", 0, "mode=755");
    mount_list.emplace_back("/dev");

    bool is_two_stage = access("/system/bin/init", F_OK) == 0;
    LOGD("is_two_stage: [%d]\n", is_two_stage);

    // For API 28 AVD, it uses legacy SAR setup that requires
    // special hacks in magiskinit to work properly.
    if (!is_two_stage && config.emulator) {
        avd_hack = true;
        // These values are hardcoded for API 28 AVD
        auto vendor_dev = find_block("vendor");
        xmkdir("/dev/block", 0755);
        xmknod("/dev/block/vde1", S_IFBLK | 0600, vendor_dev);
        xmount("/dev/block/vde1", "/vendor", "ext4", MS_RDONLY, nullptr);
    }

    return is_two_stage;
}

void MagiskInit::exec_init() {
    // Unmount in reverse order
    for (auto &p : reversed(mount_list)) {
        if (xumount2(p.data(), MNT_DETACH) == 0)
            LOGD("Unmount [%s]\n", p.data());
    }
    execve("/init", argv, environ);
    exit(1);
}

void MagiskInit::prepare_data() {
    LOGD("Setup data tmp\n");
    xmkdir("/data", 0755);
    xmount("magisk", "/data", "tmpfs", 0, "mode=755");

    cp_afc("/init", REDIR_PATH);
    cp_afc("/.backup", "/data/.backup");
    cp_afc("/overlay.d", "/data/overlay.d");
}

void MagiskInit::setup_tmp(const char *path) {
    LOGD("Setup Magisk tmp at %s\n", path);
    chdir("/data");

    xmkdir(INTLROOT, 0711);
    xmkdir(DEVICEDIR, 0711);
    xmkdir(WORKERDIR, 0);

    mount_preinit_dir();

    cp_afc(".backup/.magisk", MAIN_CONFIG);
    rm_rf(".backup");

    // Create applet symlinks
    for (int i = 0; applet_names[i]; ++i)
        xsymlink("./magisk", applet_names[i]);
    xsymlink("./magiskpolicy", "supolicy");

    xmount(".", path, nullptr, MS_BIND, nullptr);

    chdir(path);

    // Prepare worker
    xmount("magisk", WORKERDIR, "tmpfs", 0, "mode=755");

    // Use isolated devpts if kernel support
    if (access("/dev/pts/ptmx", F_OK) == 0) {
        xmkdirs(SHELLPTS, 0755);
        xmount("devpts", SHELLPTS, "devpts", MS_NOSUID | MS_NOEXEC, "newinstance");
        xmount(nullptr, SHELLPTS, nullptr, MS_PRIVATE, nullptr);
        if (access(SHELLPTS "/ptmx", F_OK)) {
            umount2(SHELLPTS, MNT_DETACH);
            rmdir(SHELLPTS);
        }
    }

    chdir("/");
}
