use std::{
    fs::{self, File},
    io::BufWriter,
    path::Path,
};

use walkdir::WalkDir;

use crate::{
    build::{
        cargo::CargoBuilder,
        ports::{self, Port},
    },
    check::AllOk,
    env::BuildEnv,
    error::Error,
    util,
};

use super::InitrdGenerated;

const PROGRAMS: &[(&str, &str)] = &[
    // init
    ("init", "init"),
    ("rc", "sbin/rc"),
    ("service", "sbin/service"),
    ("logd", "sbin/logd"),
    ("kmod", "sbin/kmod"),
    // shell
    ("shell", "bin/sh"),
    // sysutils
    ("cat", "bin/cat"),
    ("tar", "bin/tar"),
    ("chmod", "bin/chmod"),
    ("chroot", "sbin/chroot"),
    ("date", "bin/date"),
    ("dd", "bin/dd"),
    ("echo", "bin/echo"),
    ("env", "bin/env"),
    ("grep", "bin/grep"),
    ("hexd", "bin/hexd"),
    ("ln", "bin/ln"),
    ("login", "sbin/login"),
    ("ls", "bin/ls"),
    ("lspci", "bin/lspci"),
    ("md2txt", "bin/md2txt"),
    ("mkdir", "bin/mkdir"),
    ("mount", "sbin/mount"),
    ("mv", "bin/mv"),
    ("poweroff", "sbin/poweroff"),
    ("ps", "bin/ps"),
    ("random", "bin/random"),
    ("reboot", "sbin/reboot"),
    ("rm", "bin/rm"),
    ("sha256sum", "bin/sha256sum"),
    ("sleep", "bin/sleep"),
    ("strace", "bin/strace"),
    ("sync", "bin/sync"),
    ("sysmon", "bin/sysmon"),
    ("top", "bin/top"),
    ("touch", "bin/touch"),
    ("tree", "bin/tree"),
    ("tst", "bin/tst"),
    ("view", "bin/view"),
    ("serial", "bin/serial"),
    // netutils
    ("netconf", "sbin/netconf"),
    ("dhcp-client", "sbin/dhcp-client"),
    ("nc", "bin/nc"),
    ("http", "bin/http"),
    ("dnsq", "bin/dnsq"),
    ("ping", "bin/ping"),
    ("ncap", "sbin/ncap"),
    ("ntpc", "bin/ntpc"),
    // colors
    ("colors", "bin/colors"),
    ("colors-bar", "bin/colors-bar"),
    ("term", "bin/term"),
    // red
    ("red", "bin/red"),
    // rdb
    ("rdb", "bin/rdb"),
    // rsh
    ("rsh", "bin/rsh"),
    ("rshd", "sbin/rshd"),
    // crypt
    ("crypt", "bin/crypt"),
    ("dyn-loader", "libexec/dyn-loader"),
    // TODO: proper process for C program builds
];

fn build_userspace(env: &BuildEnv, _: AllOk) -> Result<(), Error> {
    log::info!("Building userspace");
    CargoBuilder::Userspace(env).build(env.workspace_root.join("userspace"))?;
    CargoBuilder::Userspace(env).build(env.workspace_root.join("userspace/dynload-program"))?;

    Ok(())
}

fn build_ports(env: &BuildEnv) -> Result<(), Error> {
    let ports = env
        .config
        .components(env)
        .ports
        .iter()
        .map(|port| Port::load(env, port).map_err(|e| Error::PortError(port.clone(), e)))
        .collect::<Result<Vec<_>, Error>>()?;
    ports::build_and_install(env, ports)?;
    Ok(())
}

fn build_rootfs<S: AsRef<Path>, D: AsRef<Path>>(
    env: &BuildEnv,
    build_dir: S,
    rootfs_dir: D,
    _: AllOk,
) -> Result<(), Error> {
    let user_dir = env.workspace_root.join("userspace");
    let build_dir = build_dir.as_ref();
    let rootfs_dir = rootfs_dir.as_ref();

    log::info!("Building rootfs: {}", rootfs_dir.display());

    for dir in ["sbin", "bin", "dev", "sys", "tmp", "mnt"] {
        fs::create_dir_all(rootfs_dir.join(dir))?;
        File::create(rootfs_dir.join(dir).join(".do_not_remove"))?;
    }

    // TODO grab this from cargo somehow?
    for &(src, dst) in PROGRAMS {
        let src_path = build_dir.join(src);
        let dst_path = rootfs_dir.join(dst);

        if let Some(dst_parent) = dst_path.parent() {
            fs::create_dir_all(dst_parent)?;
        }

        log::trace!(
            "Install binary: {} -> {}",
            src_path.display(),
            dst_path.display()
        );
        util::copy_file(src_path, dst_path)?;
    }

    if let Ok(read_dir) = fs::read_dir("userspace/scripts") {
        for entry in read_dir {
            let Ok(entry) = entry else {
                continue;
            };
            let path = entry.path();
            let Some(filename) = path.file_name() else {
                continue;
            };

            fs::copy(&path, rootfs_dir.join("bin").join(filename))?;
        }
    }

    // TODO other architectures
    util::copy_file(
        env.workspace_root.join(format!(
            "userspace/dynload-program/target/{}-unknown-yggdrasil/{}/dynload-program",
            env.arch.name(),
            env.profile.name()
        )),
        rootfs_dir.join("dynload-program"),
    )?;

    // Copy /etc
    util::copy_dir_recursive(user_dir.join("etc"), rootfs_dir.join("etc"))?;

    // Copy architecture-specific directories
    let arch_dir = user_dir.join("arch").join(env.arch.name());
    let arch_rc_d = arch_dir.join("rc.d");
    let arch_inittab = arch_dir.join("inittab");

    if arch_rc_d.exists() {
        util::copy_dir_recursive(arch_rc_d, rootfs_dir.join("etc/rc.d"))?;
    }
    if arch_inittab.exists() {
        util::copy_file(arch_inittab, rootfs_dir.join("etc/inittab"))?;
    }

    Ok(())
}

fn pack_initrd<P: AsRef<Path>>(env: &BuildEnv, rootfs_dir: P) -> Result<InitrdGenerated, Error> {
    let initrd_tar = env.userspace_output_dir.join("initrd.tar");

    log::info!("Building initrd");

    let tar = File::create(&initrd_tar)?;
    let tar = BufWriter::new(tar);
    let mut tar = tar::Builder::new(tar);

    for entry in WalkDir::new(&rootfs_dir)
        .follow_links(false)
        .into_iter()
        .filter(|e| e.as_ref().map(|e| !e.file_type().is_dir()).unwrap_or(false))
    {
        let entry = entry?;
        let src_path = entry.path();
        let rel_path = src_path
            .strip_prefix(&rootfs_dir)
            .map_err(|_| Error::InvalidPath(src_path.into()))?;

        if entry.file_type().is_file() {
            let mut file = File::open(entry.path())?;
            tar.append_file(rel_path, &mut file)?;
        } else if entry.file_type().is_symlink() {
            let target = fs::read_link(entry.path())?;
            let mut header = tar::Header::new_ustar();
            header.set_entry_type(tar::EntryType::symlink());
            header.set_mode(0o755);
            tar.append_link(&mut header, rel_path, target)?;
        } else {
            todo!()
        }
    }

    Ok(InitrdGenerated(initrd_tar))
}

pub fn build_initrd(env: &BuildEnv, check: AllOk) -> Result<InitrdGenerated, Error> {
    let rootfs_dir = env.userspace_output_dir.join("rootfs");

    build_userspace(env, check)?;
    build_ports(env)?;

    build_rootfs(env, &env.userspace_output_dir, &rootfs_dir, check)?;
    pack_initrd(env, rootfs_dir)
}
