use error_stack::{bail, report, Context, IntoReport, Result, ResultExt};
use fatfs::{FileSystem, FormatVolumeOptions, FsOptions, ReadWriteSeek};
use nix::fcntl::FallocateFlags;
use std::{fmt::Display, fs::File, io, os::fd::AsRawFd, path::Path, process::Command};

fn main() -> Result<(), Error> {
    env_logger::init();
    let mut args = std::env::args();
    args.next();

    match args.next().as_deref() {
        Some("build" | "b") => build(
            args.next()
                .map(|x| x == "-r" || x == "--release")
                .unwrap_or_default(),
        )
        .change_context(Error::Build),
        Some("run" | "r") => {
            build(
                args.next()
                    .map(|x| x == "-r" || x == "--release")
                    .unwrap_or_default(),
            )?;
            run()
        }
        Some("help" | "h") => {
            println!(concat!(
                "AbleOS RepBuild\n",
                "Subcommands:\n",
                "  build (b): Build a bootable disk image\n",
                "   help (h): Print this message\n",
                "    run (r): Build and run AbleOS in QEMU\n\n",
                "Options for build and run:\n",
                "  -r: build in release mode",
            ),);
            Ok(())
        }
        _ => Err(report!(Error::InvalidSubCom)),
    }
}

fn get_fs() -> Result<FileSystem<impl ReadWriteSeek>, io::Error> {
    let path = Path::new("target/disk.img");

    match std::fs::metadata(path) {
        Err(e) if e.kind() == io::ErrorKind::NotFound => (),
        Err(e) => bail!(e),
        Ok(_) => {
            return FileSystem::new(
                File::options().read(true).write(true).open(path)?,
                FsOptions::new(),
            )
            .into_report()
        }
    }

    let mut img = File::options()
        .read(true)
        .write(true)
        .create(true)
        .open(path)?;

    img.set_len(1024 * 1024 * 64)?;

    fatfs::format_volume(&mut img, FormatVolumeOptions::new())?;

    let fs = FileSystem::new(img, FsOptions::new())?;
    let bootdir = fs.root_dir().create_dir("efi")?.create_dir("boot")?;

    io::copy(
        &mut File::open("limine/BOOTX64.EFI")?,
        &mut bootdir.create_file("bootx64.efi")?,
    )?;

    io::copy(
        &mut File::open(Path::new("repbuild/limine.cfg"))?,
        &mut fs.root_dir().create_file("limine.cfg")?,
    )?;

    drop(bootdir);
    Ok(fs)
}

fn build(release: bool) -> Result<(), Error> {
    let fs = get_fs().change_context(Error::Io)?;
    let mut com = Command::new("cargo");
    com.current_dir("kernel");
    com.args(["b"]);
    if release {
        com.arg("-r");
    }

    com.status().into_report().change_context(Error::Build)?;

    (|| -> std::io::Result<_> {
        io::copy(
            &mut File::open(
                Path::new("target/x86_64-ableos")
                    .join(if release { "release" } else { "debug" })
                    .join("kernel"),
            )?,
            &mut fs.root_dir().create_file("kernel")?,
        )
        .map(|_| ())
    })()
    .into_report()
    .change_context(Error::Io)
}

fn run() -> Result<(), Error> {
    let mut com = Command::new("qemu-system-x86_64");

    #[rustfmt::skip]
    com.args([
        "-bios", "/usr/share/OVMF/OVMF_CODE.fd",
        "-drive", "file=target/disk.img,format=raw",
        "-m", "4G",
        "-serial", "stdio",
        "-smp", "cores=2",
    ]);

    #[cfg(target_os = "linux")]
    {
        com.args(["-enable-kvm", "-cpu", "host"]);
    }

    match com
        .status()
        .into_report()
        .change_context(Error::ProcessSpawn)?
    {
        s if s.success() => Ok(()),
        s => Err(report!(Error::Qemu(s.code()))),
    }
}

#[derive(Debug)]
enum Error {
    Build,
    InvalidSubCom,
    Io,
    ProcessSpawn,
    Qemu(Option<i32>),
}

impl Context for Error {}
impl Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Build => f.write_str("failed to build the kernel"),
            Self::InvalidSubCom => {
                f.write_str("missing or invalid subcommand (available: build, run)")
            }
            Self::Io => f.write_str("IO error"),
            Self::ProcessSpawn => f.write_str("failed to spawn a process"),
            Self::Qemu(Some(c)) => write!(f, "QEMU Error: {c}"),
            Self::Qemu(None) => write!(f, "QEMU Error: interrupted by signal"),
        }
    }
}