use std::fs::{self, File};
use std::io::{self, Read};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};

use flate2::read::GzDecoder;
use tempfile::TempDir;
use tracing::trace;

use crate::error::Error;

pub(crate) fn read_to_bytes<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> {
    let mut buf = Vec::new();

    let mut file = File::open(path)?;
    file.read_to_end(&mut buf)?;

    Ok(buf)
}

pub(crate) fn file_mode<P: AsRef<Path>>(path: P) -> io::Result<String> {
    trace!("Determining file mode: {}", path.as_ref().to_string_lossy());

    let mode = File::open(path)?.metadata()?.permissions().mode() & 0o777;
    Ok(format!("{mode:03o}"))
}

pub(crate) fn unpack_tar_gz<P: AsRef<Path>>(path: P) -> io::Result<TempDir> {
    trace!("Unpacking tar/gz file: {}", path.as_ref().to_string_lossy());

    let file = File::open(path)?;
    let ungz = GzDecoder::new(file);
    let mut archive = tar::Archive::new(ungz);
    let out = TempDir::new()?;

    trace!("Unpacking tar/gz file into: {}", out.path().to_string_lossy());
    archive.unpack(&out)?;
    Ok(out)
}

#[derive(Debug, Default)]
pub(crate) struct DirectoryContents {
    // normal files with valid paths
    pub files: Vec<PathBuf>,
    // pointers to outside the crate
    pub outside_base: Vec<PathBuf>,
    // otherwise broken symlinks
    pub broken_links: Vec<PathBuf>,
}

pub(crate) fn get_contents<P: AsRef<Path>>(path: P, root: P) -> Result<DirectoryContents, Error> {
    trace!(
        "Listing contents of directory tree: {}",
        path.as_ref().to_string_lossy()
    );

    let wd = walkdir::WalkDir::new(&path).follow_links(true).sort_by_file_name();

    let mut dc = DirectoryContents::default();

    for result in wd {
        match result {
            Ok(entry) => {
                if entry.path_is_symlink() {
                    match fs::canonicalize(entry.path()) {
                        Ok(real_path) => {
                            if real_path.is_dir() {
                                continue;
                            }

                            if real_path.strip_prefix(&root).is_ok() {
                                dc.files.push(
                                    entry
                                        .path()
                                        .strip_prefix(&path)
                                        .map_err(|err| Error::Walk { inner: err.to_string() })?
                                        .to_owned(),
                                );
                            } else {
                                // symlinks that point outside the base directory are not considered valid
                                dc.outside_base.push(entry.path().read_link()?);
                            }
                        },
                        Err(err) => return Err(err.into()),
                    }
                } else {
                    if entry.file_type().is_dir() {
                        continue;
                    }
                    // there should be no files outside the base directory
                    let file = entry
                        .path()
                        .strip_prefix(&path)
                        .map_err(|err| Error::Walk { inner: err.to_string() })?;
                    dc.files.push(file.to_owned());
                }
            },
            Err(err) => {
                if let Some(file_path) = err.path() {
                    if let Ok(rel_path) = file_path.strip_prefix(&root) {
                        dc.broken_links.push(rel_path.to_owned());
                    } else {
                        dc.outside_base.push(file_path.to_owned());
                    }
                } else {
                    return Err(Error::Walk { inner: err.to_string() });
                }
            },
        }
    }

    Ok(dc)
}
