use std::{
    env, fs, io, path,
    path::{Path, PathBuf},
};

#[cfg(unix)]
pub mod unix;

#[cfg(windows)]
pub mod windows;

pub mod sys {
    use std::{
        ffi::{OsStr, OsString},
        path::{Path, PathBuf},
    };

    #[cfg(unix)]
    use super::unix as sys;

    #[cfg(windows)]
    use super::windows as sys;

    /// Returns the null device for the current platform
    ///
    /// On unix, this is `/dev/null`
    /// On windows, this is `NUL`
    pub fn null_path() -> &'static Path {
        Path::new(sys::NULL_PATH)
    }

    /// Wrapper around [`sanitize_os_str()`], but returns a [`String`] instead of an [`OsString`]
    pub fn sanitize_str(str: impl AsRef<str>) -> String {
        sanitize_os_str(str.as_ref())
            .into_string()
            .expect("Put UTF-8 in, got non-UTF-8 out")
    }

    /// Sanitizes a string, removing platform-specific illegal characters from it
    pub fn sanitize_os_str(os_str: impl AsRef<OsStr>) -> OsString {
        sys::sanitize_string(os_str)
    }

    /// Sanitizes a path, removing platform-specific illegal characters from its components
    pub fn sanitize_path(path: impl AsRef<Path>) -> PathBuf {
        path.as_ref()
            .components()
            .map(|c| match c {
                std::path::Component::Normal(os_str) => sanitize_os_str(os_str),
                _ => c.as_os_str().to_owned(),
            })
            .collect()
    }

    /// Returns all paths which 'protect' the input path, if the path is not protected, this returns [`None`]
    ///
    /// The path is protected if is contained inside [`sys::protected_paths()`] or starts with a path in [`sys::protected_directories()`]
    pub fn is_protected_path(item: impl AsRef<Path>) -> Vec<PathBuf> {
        let item = item.as_ref();

        let mut protected_by = Vec::new();

        if let Some(path) = sys::protected_paths().iter().find(|p| *p == item) {
            protected_by.push(path.to_owned());
        }

        sys::protected_directories().iter().for_each(|path| {
            if item.starts_with(path) {
                protected_by.push(path.to_owned());
            }
        });

        protected_by
    }
}

/// Absolutizes a path to the current working directory of the application
///
/// # Errors
///
/// Errors if [`std::env::current_dir()`] errors.
pub fn absolutize_to_cwd(path: impl AsRef<Path>) -> io::Result<PathBuf> {
    let abs = env::current_dir()?.join(path);
    Ok(abs)
}

/// Returns the latest existing ancestor (directory) of a path
///
/// If you had `/foo/exists/doesnt_exist/bar`, this function would return `/foo/exists`
/// If you had `/foo/file`, this function would return `/foo`
pub fn latest_existing_ancestor(path: impl AsRef<Path>) -> io::Result<PathBuf> {
    let mut latest = Some(path.as_ref());

    while let Some(path) = latest {
        match path.symlink_metadata() {
            Ok(meta) => {
                if meta.is_dir() {
                    return Ok(path.to_path_buf());
                }
                latest = path.parent()
            }
            Err(err) if matches!(err.kind(), io::ErrorKind::NotFound) => latest = path.parent(),
            Err(err) => return Err(err),
        }
    }

    Err(io::Error::new(
        io::ErrorKind::NotFound,
        format!(
            "No ancestor was found for {path}",
            path = path.as_ref().display()
        ),
    ))
}

/// Normalizes a path, removing `.` and `..`
pub fn normalize_path(path: impl AsRef<Path>) -> PathBuf {
    let mut normalized = PathBuf::new();

    for component in path.as_ref().components() {
        match component {
            path::Component::CurDir => {}
            path::Component::ParentDir => {
                if !normalized.pop() {
                    normalized.push("..");
                }
            }
            _ => normalized.push(component),
        }
    }

    normalized.iter().collect()
}

/// Follows a symlink to the target it points to
///
/// # Errors
///
/// Errors if [`std::fs::read_link()`] or [`std::fs::symlink_metadata()`] returns an error
pub fn follow_symlink(symlink: impl AsRef<Path>) -> io::Result<PathBuf> {
    let target = fs::read_link(symlink.as_ref())?;

    let real_target = if target.is_absolute() {
        target
    } else {
        symlink
            .as_ref()
            .parent()
            .unwrap_or(Path::new("/"))
            .join(target)
    };

    if let Err(err) = real_target.symlink_metadata() {
        if matches!(err.kind(), io::ErrorKind::NotFound) {
            return Err(io::Error::new(
                io::ErrorKind::NotFound,
                format!(
                    "Target of symlink '{symlink}' does not exist ({target})",
                    symlink = symlink.as_ref().display(),
                    target = real_target.display()
                ),
            ));
        } else {
            return Err(err);
        }
    };

    Ok(real_target)
}

/// Follows an entire symlink chain until it reaches a file, directory, or a loop
///
/// Paths are normalized via [`normalize_path()`] to prevent cyclical symlink chains
///
/// # Errors
///
/// Errors if [`follow_symlink()`] errors
pub fn follow_symlink_chain(symlink: impl AsRef<Path>) -> (Vec<PathBuf>, Option<io::Error>) {
    let mut stack = vec![symlink.as_ref().to_path_buf()];

    loop {
        let target = match follow_symlink(stack.last().unwrap()) {
            Ok(target) => normalize_path(target),
            Err(err) => return (stack, Some(err)),
        };

        if stack.contains(&target) {
            stack.push(target);
            return (
                stack,
                Some(io::Error::new(
                    io::ErrorKind::TooManyLinks,
                    format!(
                        "{symlink} is a cyclical",
                        symlink = symlink.as_ref().display()
                    ),
                )),
            );
        }

        if target.is_symlink() {
            stack.push(target);
            continue;
        } else {
            stack.push(target);
            return (stack, None);
        }
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn sanitize_is_lossless() {
        use std::ffi::OsString;

        use crate::paths::sys::sanitize_os_str;

        let os_str = OsString::from("nothing_to_replace");
        let sanitized = sanitize_os_str(os_str);

        assert_eq!(sanitized, OsString::from("nothing_to_replace"))
    }
}
