use anyhow::{bail, Context, Error, Ok, Result};
use std::{
    fs::{create_dir_all, remove_file, write, File, OpenOptions},
    io::{ErrorKind::AlreadyExists, ErrorKind::NotFound, Write},
    path::Path,
};

use crate::defs;
use std::fs::metadata;
#[allow(unused_imports)]
use std::fs::{set_permissions, Permissions};
#[cfg(unix)]
use std::os::unix::prelude::PermissionsExt;

use hole_punch::*;
use std::io::{Read, Seek, SeekFrom};

#[cfg(any(target_os = "linux", target_os = "android"))]
use rustix::{
    process,
    thread::{move_into_link_name_space, unshare, LinkNameSpaceType, UnshareFlags},
};

pub fn ensure_clean_dir(dir: &str) -> Result<()> {
    let path = Path::new(dir);
    log::debug!("ensure_clean_dir: {}", path.display());
    if path.exists() {
        log::debug!("ensure_clean_dir: {} exists, remove it", path.display());
        std::fs::remove_dir_all(path)?;
    }
    Ok(std::fs::create_dir_all(path)?)
}

pub fn ensure_file_exists<T: AsRef<Path>>(file: T) -> Result<()> {
    match File::options().write(true).create_new(true).open(&file) {
        std::result::Result::Ok(_) => Ok(()),
        Err(err) => {
            if err.kind() == AlreadyExists && file.as_ref().is_file() {
                Ok(())
            } else {
                Err(Error::from(err))
                    .with_context(|| format!("{} is not a regular file", file.as_ref().display()))
            }
        }
    }
}

pub fn ensure_dir_exists<T: AsRef<Path>>(dir: T) -> Result<()> {
    let result = create_dir_all(&dir).map_err(Error::from);
    if dir.as_ref().is_dir() {
        result
    } else if result.is_ok() {
        bail!("{} is not a regular directory", dir.as_ref().display())
    } else {
        result
    }
}

pub fn ensure_binary<T: AsRef<Path>>(
    path: T,
    contents: &[u8],
    ignore_if_exist: bool,
) -> Result<()> {
    if ignore_if_exist && path.as_ref().exists() {
        return Ok(());
    }

    ensure_dir_exists(path.as_ref().parent().ok_or_else(|| {
        anyhow::anyhow!(
            "{} does not have parent directory",
            path.as_ref().to_string_lossy()
        )
    })?)?;

    if let Err(e) = remove_file(path.as_ref()) {
        if e.kind() != NotFound {
            return Err(Error::from(e))
                .with_context(|| format!("failed to unlink {}", path.as_ref().display()));
        }
    }

    write(&path, contents)?;
    #[cfg(unix)]
    set_permissions(&path, Permissions::from_mode(0o755))?;
    Ok(())
}

#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn getprop(prop: &str) -> Option<String> {
    android_properties::getprop(prop).value()
}

#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn getprop(_prop: &str) -> Option<String> {
    unimplemented!()
}

pub fn is_safe_mode() -> bool {
    let safemode = getprop("persist.sys.safemode")
        .filter(|prop| prop == "1")
        .is_some()
        || getprop("ro.sys.safemode")
            .filter(|prop| prop == "1")
            .is_some();
    log::info!("safemode: {}", safemode);
    if safemode {
        return true;
    }
    let safemode = crate::ksu::check_kernel_safemode();
    log::info!("kernel_safemode: {}", safemode);
    safemode
}

pub fn get_zip_uncompressed_size(zip_path: &str) -> Result<u64> {
    let mut zip = zip::ZipArchive::new(std::fs::File::open(zip_path)?)?;
    let total: u64 = (0..zip.len())
        .map(|i| zip.by_index(i).unwrap().size())
        .sum();
    Ok(total)
}

#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn switch_mnt_ns(pid: i32) -> Result<()> {
    use rustix::{
        fd::AsFd,
        fs::{open, Mode, OFlags},
    };
    let path = format!("/proc/{pid}/ns/mnt");
    let fd = open(path, OFlags::RDONLY, Mode::from_raw_mode(0))?;
    let current_dir = std::env::current_dir();
    move_into_link_name_space(fd.as_fd(), Some(LinkNameSpaceType::Mount))?;
    if let std::result::Result::Ok(current_dir) = current_dir {
        let _ = std::env::set_current_dir(current_dir);
    }
    Ok(())
}

#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn unshare_mnt_ns() -> Result<()> {
    unshare(UnshareFlags::NEWNS)?;
    Ok(())
}

fn switch_cgroup(grp: &str, pid: u32) {
    let path = Path::new(grp).join("cgroup.procs");
    if !path.exists() {
        return;
    }

    let fp = OpenOptions::new().append(true).open(path);
    if let std::result::Result::Ok(mut fp) = fp {
        let _ = writeln!(fp, "{pid}");
    }
}

pub fn switch_cgroups() {
    let pid = std::process::id();
    switch_cgroup("/acct", pid);
    switch_cgroup("/dev/cg2_bpf", pid);
    switch_cgroup("/sys/fs/cgroup", pid);

    if getprop("ro.config.per_app_memcg")
        .filter(|prop| prop == "false")
        .is_none()
    {
        switch_cgroup("/dev/memcg/apps", pid);
    }
}

#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn umask(mask: u32) {
    process::umask(rustix::fs::Mode::from_raw_mode(mask));
}

#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn umask(_mask: u32) {
    unimplemented!("umask is not supported on this platform")
}

pub fn has_magisk() -> bool {
    which::which("magisk").is_ok()
}

pub fn get_tmp_path() -> &'static str {
    if metadata(defs::TEMP_DIR_LEGACY).is_ok() {
        return defs::TEMP_DIR_LEGACY;
    }
    if metadata(defs::TEMP_DIR).is_ok() {
        return defs::TEMP_DIR;
    }
    ""
}

// TODO: use libxcp to improve the speed if cross's MSRV is 1.70
pub fn copy_sparse_file<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> {
    let mut src_file = File::open(src.as_ref())?;
    let mut dst_file = OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .open(dst.as_ref())?;

    dst_file.set_len(src_file.metadata()?.len())?;

    let segments = src_file.scan_chunks()?;
    for segment in segments {
        if let SegmentType::Data = segment.segment_type {
            let start = segment.start;
            let end = segment.end;

            src_file.seek(SeekFrom::Start(start))?;
            dst_file.seek(SeekFrom::Start(start))?;

            let mut buffer = [0; 4096];
            let mut total_bytes_copied = 0;

            while total_bytes_copied < end - start {
                let bytes_to_read =
                    std::cmp::min(buffer.len() as u64, end - start - total_bytes_copied);
                let bytes_read = src_file.read(&mut buffer[..bytes_to_read as usize])?;

                if bytes_read == 0 {
                    break;
                }

                dst_file.write_all(&buffer[..bytes_read])?;
                total_bytes_copied += bytes_read as u64;
            }
        }
    }

    Ok(())
}

#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn punch_hole(src: impl AsRef<Path>) -> Result<()> {
    let mut src_file = OpenOptions::new().write(true).read(true).open(src)?;

    let st = rustix::fs::fstat(&src_file)?;

    let bufsz = st.st_blksize;
    let mut buf = vec![0u8; bufsz as usize];

    let mut ct = 0;
    let mut hole_sz = 0;
    let mut hole_start = 0;

    let segments = src_file.scan_chunks()?;
    for segment in segments {
        if segment.segment_type != SegmentType::Data {
            continue;
        }

        let mut off = segment.start;
        let end = segment.end + 1;

        while off < end {
            let mut rsz = rustix::io::pread(&src_file, &mut buf, off)? as u64;
            if rsz > 0 && rsz > end - off {
                // exceed the end of the boundary
                rsz = end - off;
            }

            if rsz == 0 {
                break;
            }

            if buf.iter().all(|&x| x == 0) {
                // the whole buf is zero, mark it as a hole
                if hole_sz == 0 {
                    hole_start = off;
                }
                // for continuous zero, we can merge them into a bigger hole
                hole_sz += rsz;
            } else if hole_sz > 0 {
                if let Err(e) = rustix::fs::fallocate(
                    &src_file,
                    rustix::fs::FallocateFlags::PUNCH_HOLE | rustix::fs::FallocateFlags::KEEP_SIZE,
                    hole_start,
                    hole_sz,
                ) {
                    log::warn!("Failed to punch hole: {:?}", e);
                }

                ct += hole_sz;

                hole_sz = 0;
                hole_start = 0;
            }

            off += rsz;
        }

        // if the last segment is a hole, we need to punch it
        if hole_sz > 0 {
            let mut alloc_sz = hole_sz;
            if off >= end {
                alloc_sz += st.st_blksize as u64;
            }
            if let Err(e) = rustix::fs::fallocate(
                &src_file,
                rustix::fs::FallocateFlags::PUNCH_HOLE | rustix::fs::FallocateFlags::KEEP_SIZE,
                hole_start,
                alloc_sz,
            ) {
                log::warn!("Failed to punch hole: {:?}", e);
            }

            ct += hole_sz;
        }
    }

    log::info!(
        "Punched {} of hole",
        humansize::format_size(ct, humansize::DECIMAL)
    );

    Ok(())
}

#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn punch_hole(src: impl AsRef<Path>) -> Result<()> {
    unimplemented!()
}
