/**
 * ./src/classes/pxe.ts
 * penguins-eggs v.25.7.x / ecmascript 2020
 * author: Piero Proietti
 * email: piero.proietti@gmail.com
 * license: MIT
 */
import express from 'express';
import fs from 'node:fs';
import path from 'node:path';
// @ts-ignore
import tftp from 'tftp';
import { startSimpleProxy } from '../dhcpd-proxy/simple-proxy.js';
import { exec } from '../lib/utils.js';
import Distro from './distro.js';
import Diversions from './diversions.js';
import Settings from './settings.js';
import Utils from './utils.js';
// _dirname
const __dirname = path.dirname(new URL(import.meta.url).pathname);
/**
 * Pxe:
 */
export default class Pxe {
    bootLabel = '';
    distro = {};
    echo = {};
    eggRoot = '';
    initrdImg = '';
    isos = []; // cuckoo's eggs
    nest = '';
    pxeRoot = '';
    settings = {};
    vmlinuz = '';
    /**
     * constructor
     * @param nest
     * @param pxeRoot
     */
    constructor(nest = '', pxeRoot = '', verbose = false) {
        this.nest = nest;
        this.pxeRoot = pxeRoot;
        this.echo = Utils.setEcho(verbose);
    }
    /**
     * build
     */
    async build() {
        const echoYes = Utils.setEcho(true);
        // pxeRoot erase
        if (fs.existsSync(this.pxeRoot)) {
            await exec(`rm ${this.pxeRoot} -rf`, this.echo);
        }
        // Struttura
        await exec(`mkdir ${this.pxeRoot} -p`, this.echo);
        await exec(`ln -s ${path.join(this.eggRoot, 'live')} ${path.join(this.pxeRoot, 'live')}`, this.echo);
        await exec(`ln -s ${this.nest}.disk ${path.join(this.pxeRoot, '.disk')}`, this.echo);
        // Link supplementari distro
        if (this.distro.familyId === 'archlinux') {
            let filesystemName = `arch/x86_64/airootfs.sfs`;
            if (Diversions.isManjaroBased(this.settings.distro.distroId)) {
                filesystemName = `manjaro/x86_64/livefs.sfs`;
            }
            await exec(`mkdir ${path.join(this.pxeRoot, path.dirname(filesystemName))} -p`, this.echo);
            await exec(`ln -s ${path.join(this.eggRoot, 'live/filesystem.squashfs')} ${path.join(this.pxeRoot, filesystemName)}`, this.echo);
        }
        // Firewall per fedora
        if (this.distro.familyId === 'fedora' || this.distro.familyId === 'opensuse') {
            await exec(`firewall-cmd --add-service=dhcp --permanent`, this.echo);
            await exec(`firewall-cmd --add-service=tftp --permanent`, this.echo);
            await exec(`firewall-cmd --add-service=http --permanent`, this.echo);
            await exec(`firewall-cmd --reload`, this.echo);
        }
        await this.grubCfg();
        await this.bios();
        await this.uefi();
        await this.http();
    }
    /**
     *
     * @param dhcpOptions
     */
    dhcpdStart(dhcpOptions) {
        startSimpleProxy(dhcpOptions);
    }
    /**
     * fertilization()
     *
     * cuckoo's nest
     */
    async fertilization() {
        this.settings = new Settings();
        this.settings.load();
        this.distro = new Distro();
        if (Utils.isLive()) {
            this.eggRoot = this.distro.liveMediumPath;
        }
        else {
            this.eggRoot = path.join(this.settings.config.snapshot_dir, 'mnt/iso');
        }
        if (!Utils.isLive() && !fs.existsSync(this.settings.config.snapshot_dir)) {
            console.log('no image available, build an image with: sudo eggs produce');
            process.exit();
        }
        /**
         * se pxeRoot non esiste viene creato
         */
        if (!fs.existsSync(this.pxeRoot)) {
            await exec(`mkdir ${this.pxeRoot} -p`);
        }
        /**
         * Ricerca delle ISOs
         * installed: /home/eggs/mnt/iso/live
         * live: this.iso/live
         */
        const isos = [];
        const pathFiles = path.join(this.eggRoot, 'live');
        const files = fs.readdirSync(pathFiles);
        // rieva nome vmlinuz e initrdImg
        for (const file of files) {
            if (path.basename(file).slice(0, 7) === 'vmlinuz') {
                this.vmlinuz = path.basename(file);
            }
            if (path.basename(file).slice(0, 4) === 'init') {
                this.initrdImg = path.basename(file);
            }
        }
        /**
         * bootLabel
         */
        this.bootLabel = 'not found';
        if (fs.existsSync(path.join(this.eggRoot, '.disk/mkisofs'))) {
            const a = fs.readFileSync(path.join(this.eggRoot, '.disk/mkisofs'), 'utf8');
            const b = a.slice(Math.max(0, a.indexOf('-o ') + 3));
            const c = b.slice(0, Math.max(0, b.indexOf(' ')));
            this.bootLabel = c.slice(Math.max(0, c.lastIndexOf('/') + 1));
        }
        console.log(`eggRoot: ${this.eggRoot}`);
        console.log(`bootLabel: ${this.bootLabel}`);
        console.log(`vmlinuz: ${this.vmlinuz}`);
        console.log(`initrd: ${this.initrdImg}`);
    }
    /**
     * start http server for images
     */
    async httpStart() {
        const port = 80;
        const httpRoot = this.pxeRoot;
        // 1. Crea un'applicazione Express
        const app = express();
        // 2. Usa il middleware di Express per servire i file statici.
        app.use(express.static(httpRoot));
        // 3. Avvia il server
        app.listen(port, () => {
            console.log(`HTTP server (Express) listening on 0.0.0.0:${port}`);
            console.log(`Serving files from ${httpRoot}`);
        });
    }
    /**
     * start tftp
     */
    async tftpStart(tftpOptions) {
        const tftpServer = tftp.createServer(tftpOptions);
        console.log('tftp listening: ' + tftpOptions.host + ':' + tftpOptions.port);
        tftpServer.on('error', (error) => {
            // Errors from the main socket
            // The current transfers are not aborted
            console.error(error);
        });
        tftpServer.on('request', (req, res) => {
            req.on('error', (error) => {
                // Error from the request
                // The connection is already closed
                console.error('[' + req.stats.remoteAddress + ':' + req.stats.remotePort + '] (' + req.file + ') ' + error.message);
            });
        });
        tftpServer.listen();
    }
    /**
     * Metodo helper per ottenere i parametri corretti per GRUB
     * in base alla famiglia della distribuzione.
     */
    _getKernelParameters() {
        let lp = '';
        // .replaceAll(/\s\s+/g, ' ')
        switch (this.distro.familyId) {
            case 'alpine': {
                lp = `alpine_repo=http://${Utils.address()}/live/filesystem.squashfs \
               modules=loop,squashfs,sd-mod,usb-storage,virtio-net,e1000e \
               acpi=off \
               ip=dhcp`;
                break;
            }
            case 'archlinux': {
                let basedir = 'archisobasedir=arch';
                let hook = 'archiso_http_srv';
                if (Diversions.isManjaroBased(this.distro.distroId)) {
                    basedir = '';
                    hook = 'miso_http_srv';
                }
                lp = `${hook}=http://${Utils.address()}/ \
              ${basedir} \
              ip=dhcp \
              copytoram=n`;
                break;
            }
            case 'debian': {
                lp = `fetch=http://${Utils.address()}/live/filesystem.squashfs \
                boot=live \
                config \
                noswap \
                noprompt \
                ip=dhcp`;
                break;
            }
            case 'fedora':
            case 'openmamba':
            case 'opensuse': {
                lp = `initrd=http://${Utils.address()}/live/${path.basename(this.initrdImg)} \
               root=live:http://${Utils.address()}/live/filesystem.squashfs \
               rootfstype=auto \
               ro \
               rd.live.image \
               rd.luks=0 \
               rd.md=0 \
               rd.dm=0\n`;
                break;
            }
            default: {
                console.warn(`Attenzione: famiglia distro '${this.distro.familyId}' non riconosciuta per GRUB.`);
            }
        }
        return lp.replaceAll(/\s\s+/g, ' ');
    }
    /**
     * configure PXE bios
     */
    async bios() {
        const bootloaders = Diversions.bootloaders(this.distro.familyId);
        await exec(`cp ${path.join(__dirname, '../../addons/eggs/theme/livecd/isolinux.theme.cfg')} ${path.join(this.pxeRoot, 'isolinux.theme.cfg')}`, this.echo);
        await exec(`cp ${path.join(__dirname, '../../addons/eggs/theme/livecd/splash.png')} ${path.join(this.pxeRoot, 'splash.png')}`, this.echo);
        // ipxe.pxe
        await exec(`ln -s ${path.join(bootloaders, 'ipxe/ipxe.pxe')} ${path.join(this.pxeRoot, 'ipxe.pxe')}`, this.echo);
        // snponly.efi
        await exec(`ln -s ${path.join(bootloaders, 'ipxe/snponly.efi')} ${path.join(this.pxeRoot, 'snponly.efi')}`, this.echo);
        // pxe
        await exec(`cp ${path.join(bootloaders, 'PXELINUX/pxelinux.0')} ${path.join(this.pxeRoot, 'pxelinux.0')}`, this.echo);
        await exec(`cp ${path.join(bootloaders, 'PXELINUX/lpxelinux.0')} ${path.join(this.pxeRoot, 'lpxelinux.0')}`, this.echo);
        // syslinux
        await exec(`ln -s ${path.join(bootloaders, 'syslinux/modules/bios/ldlinux.c32')} ${path.join(this.pxeRoot, 'ldlinux.c32')}`, this.echo);
        await exec(`ln -s ${path.join(bootloaders, 'syslinux/modules/bios/vesamenu.c32')} ${path.join(this.pxeRoot, 'vesamenu.c32')}`, this.echo);
        await exec(`ln -s ${path.join(bootloaders, 'syslinux/modules/bios/libcom32.c32')} ${path.join(this.pxeRoot, 'libcom32.c32')}`, this.echo);
        await exec(`ln -s ${path.join(bootloaders, 'syslinux/modules/bios/libutil.c32')} ${path.join(this.pxeRoot, 'libutil.c32')}`, this.echo);
        await exec(`ln -s ${path.join(bootloaders, 'syslinux/memdisk')} ${path.join(this.pxeRoot, 'memdisk')}`, this.echo);
        await exec(`mkdir ${path.join(this.pxeRoot, 'pxelinux.cfg')}`, this.echo);
        let content = '';
        content += '# eggs: pxelinux.cfg/default\n';
        content += '# search path for the c32 support libraries (libcom32, libutil etc.)\n';
        content += `path /\n`;
        content += 'include isolinux.theme.cfg\n';
        content += 'UI vesamenu.c32\n';
        content += '\n';
        content += `menu title cuckoo: when you need a flying PXE server! ${Utils.address()}\n`;
        content += 'PROMPT 0\n';
        content += 'TIMEOUT 200\n';
        content += '\n';
        content += `label ${this.distro.distroId}\n`;
        content += `menu label ${this.bootLabel.replace('.iso', '')}\n`;
        content += `kernel http://${Utils.address()}/live/${path.basename(this.vmlinuz)}\n`;
        const kernelParams = this._getKernelParameters();
        content += `append initrd=http://${Utils.address()}/live/${path.basename(this.initrdImg)} ${kernelParams}\n`;
        const file = path.join(this.pxeRoot, 'pxelinux.cfg/default');
        fs.writeFileSync(file, content);
    }
    /**
     * grubCfg
     * @param familyId
     */
    async grubCfg() {
        const bootloaders = Diversions.bootloaders(this.distro.familyId);
        const echoYes = Utils.setEcho(true);
        /**
         * On Debian bookworm:
         */
        await exec(`mkdir -p ${path.join(this.pxeRoot, 'grub')}`, this.echo);
        if (this.distro.familyId === 'debian') {
            switch (process.arch) {
                case 'arm64': {
                    await exec(`cp ${path.join(bootloaders, '/grub/arm64-efi-signed/grubnetaa64.efi.signed')} ${path.join(this.pxeRoot, 'grub.efi')}`, this.echo);
                    await exec(`cp -r ${path.join(bootloaders, '/grub/arm64-efi')} ${path.join(this.pxeRoot, 'grub')}`, this.echo);
                    break;
                }
                case 'ia32': {
                    await exec(`cp ${path.join(bootloaders, '/grub/i386-efi-signed/grubnetia32.efi.signed')} ${path.join(this.pxeRoot, 'grub.efi')}`, this.echo);
                    await exec(`cp -r ${path.join(bootloaders, '/grub/i386-efi')} ${path.join(this.pxeRoot, 'grub')}`, this.echo);
                    break;
                }
                case 'x64': {
                    await exec(`cp ${path.join(bootloaders, '/grub/x86_64-efi-signed/grubnetx64.efi.signed')} ${path.join(this.pxeRoot, 'grub.efi')}`, this.echo);
                    await exec(`cp -r ${path.join(bootloaders, '/grub/x86_64-efi')} ${path.join(this.pxeRoot, 'grub')}`, this.echo);
                    break;
                }
                // No default
            }
        }
        else {
            /**
             * le altre distribuzione not signed
             */
            await exec(`cp ${path.join(bootloaders, 'grub/x86_64-efi/monolithic/grubnetx64.efi')} ${path.join(this.pxeRoot, 'grub.efi')}`, this.echo);
            await exec(`cp -r ${path.join(bootloaders, '/grub/x86_64-efi')} ${path.join(this.pxeRoot, 'grub')}`, this.echo);
        }
        // Genera il file grub.cfg
        const grubName = path.join(this.pxeRoot, 'grub/grub.cfg'); // Il file deve chiamarsi grub.cfg
        let grubContent = '';
        grubContent += `set timeout=10\n`;
        grubContent += `set default=0\n\n`;
        // Titolo del menu dinamico
        grubContent += `menuentry "${this.bootLabel.replace('.iso', '')} via PXE" {\n`;
        grubContent += `  echo "Booting ${this.bootLabel.replace('.iso', '')}..."\n`;
        grubContent += `  echo "Loading Linux Kernel..."\n`;
        const kernelParams = this._getKernelParameters();
        grubContent += `  linux (http,${Utils.address()})/live/${path.basename(this.vmlinuz)} ${kernelParams}\n`;
        grubContent += `  echo "Loading Initial Ramdisk..."\n`;
        grubContent += `  initrd (http,${Utils.address()})/live/${path.basename(this.initrdImg)}\n`;
        grubContent += `}\n`;
        fs.writeFileSync(grubName, grubContent, 'utf-8');
    }
    /**
     * configure PXE http server
     */
    async http() {
        const file = path.join(this.pxeRoot, 'index.html');
        let content = '';
        content += "<html><title>Penguin's eggs PXE server</title>";
        content += '<div style="background-image:url(\'/splash.png\');background-repeat:no-repeat;width: 640;height:480;padding:5px;border:1px solid black;">';
        content += '<h1>Cuckoo PXE server</h1>';
        content += `<body>address: <a href=http://${Utils.address()}>${Utils.address()}</a><br/>`;
        if (Utils.isLive()) {
            content += 'started from live iso image<br/>';
        }
        else {
            content += 'Serving:<li>';
            for (const iso of this.isos) {
                content += `<ul><a href='http://${Utils.address()}/${iso}'>${iso}</a></ul>`;
            }
            content += '</li>';
        }
        content += "source: <a href='https://github.com/pieroproietti/penguins-eggs'>https://github.com/pieroproietti/penguins-eggs</a><br/>";
        content += "manual: <a href='https://penguins-eggs.net/book/italiano9.2.html'>italiano</a>, <a href='https://penguins--eggs-net.translate.goog/book/italiano9.2?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en'>translated</a><br/>";
        content += "discuss: <a href='https://t.me/penguins_eggs'>Telegram group<br/></body</html>";
        fs.writeFileSync(file, content);
    }
    /**
     * uefi: uso ipxe solo per chainload di grub
     */
    async uefi() {
        let content = '#!ipxe\n';
        content += 'dhcp\n';
        content += `chain http://${Utils.address()}/grub.efi\n`;
        const file = path.join(this.pxeRoot, 'autoexec.ipxe');
        fs.writeFileSync(file, content);
    }
}
