//! Utilities for editing null-separated path lists.
//!
//! This module operates on the same null-separated path-list format consumed by `repo2markdown`.
//! The functions are intended for small Unix-style filter binaries such as `r2md-append`,
//! `r2md-prepend`, and `r2md-remove`.
//!
//! Paths that are kept from stdin are written back using their original byte representation.
//! Removal compares paths after normalization relative to a supplied origin base.

use std::{
    collections::HashSet,
    ffi::OsString,
    io::{Read, Write},
    os::unix::ffi::OsStrExt,
    path::{Path, PathBuf},
};

use crate::{normalizer::Normalizer, util::path_list::paths_from_null_separated_bytes};

const NUL: &[u8] = b"\0";

/// Copies a null-separated path list from `input`, then appends `new_paths`.
///
/// Each path in `new_paths` is written as a NUL-terminated entry.
///
/// This function is defensive about unterminated input: if `input` is non-empty and does not end
/// with a NUL byte, a NUL byte is inserted before appended paths are written. If `new_paths` is
/// empty, the input stream is passed through unchanged.
///
/// This function does not normalize, deduplicate, or validate paths.
pub fn append_paths<R: Read, W: Write>(
    input: R,
    mut output: W,
    new_paths: &[OsString],
) -> std::io::Result<()> {
    let last_byte_in_input = copy_while_tracking_last_byte(input, &mut output)?;
    // Defensively ensure non-empty input is NUL-terminated.
    if !new_paths.is_empty() && matches!(last_byte_in_input, Some(byte) if byte != 0) {
        output.write_all(NUL)?;
    }
    for path in new_paths {
        output.write_all(path.as_bytes())?;
        output.write_all(NUL)?;
    }
    Ok(())
}

/// Copies all bytes from `input` to `output`, returning the last byte copied.
///
/// Returns `Ok(None)` when `input` is empty.
///
/// This helper is used by [`append_paths`] to preserve streaming behavior while still detecting
/// whether the input path list ended with a NUL byte.
fn copy_while_tracking_last_byte<R: Read, W: Write>(
    mut input: R,
    mut output: W,
) -> std::io::Result<Option<u8>> {
    let mut buf = [0; 8192];
    let mut last = None;

    loop {
        let read_bytes = input.read(&mut buf)?;
        if read_bytes == 0 {
            break;
        }

        last = Some(buf[read_bytes - 1]);
        output.write_all(&buf[..read_bytes])?;
    }
    Ok(last)
}

/// Prepends `new_paths` before the null-separated path list from `input`.
///
/// Each path in `new_paths` is written as a NUL-terminated entry before the original input stream
/// is copied unchanged.
///
/// This function does not normalize, deduplicate, validate, or repair the input stream. In
/// particular, unlike [`append_paths`], it does not need to inspect whether the input stream is
/// NUL-terminated because no new path is written after stdin.
pub fn prepend_paths<R: Read, W: Write>(
    mut input: R,
    mut output: W,
    new_paths: &[OsString],
) -> std::io::Result<()> {
    for path in new_paths {
        output.write_all(path.as_bytes())?;
        output.write_all(NUL)?;
    }
    std::io::copy(&mut input, &mut output)?;
    Ok(())
}

/// Removes paths from a null-separated path list.
///
/// The `input` stream is interpreted as a NUL-separated sequence of path entries. Each entry is
/// normalized relative to `origin_base` and compared against the normalized form of each path in
/// `unwanted_paths`.
///
/// Entries whose normalized path matches one of the normalized `unwanted_paths` are skipped.
/// Entries that are kept are written to `output` using their original byte form, followed by a NUL
/// byte.
pub fn remove_paths<R: Read, W: Write>(
    mut input: R,
    mut output: W,
    unwanted_paths: &[PathBuf],
    origin_base: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    // The root is arbitrary here because root_relative is not user-facing.
    // It only needs to be consistent across both sides of the comparison.
    let normalizer = Normalizer::new(Path::new("."), origin_base)?;
    // Normalize unwanted paths once and place them in a HashSet for efficient lookup.
    let unwanted_paths = unwanted_paths
        .iter()
        .map(|path| normalizer.normalize(path))
        .collect::<Result<HashSet<_>, _>>()?;

    let mut input_buf = Vec::new();
    input.read_to_end(&mut input_buf)?;
    for path in paths_from_null_separated_bytes(&input_buf) {
        let normalized_path = normalizer.normalize(path)?;
        if !unwanted_paths.contains(&normalized_path) {
            output.write_all(path.as_os_str().as_bytes())?;
            output.write_all(NUL)?;
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::{
        ffi::OsString,
        os::unix::ffi::OsStrExt,
        path::{Path, PathBuf},
    };

    use super::{append_paths, prepend_paths, remove_paths};

    #[test]
    fn append_paths_works_with_empty_stdin_and_args() {
        let input = b"";
        let mut output = Vec::new();

        append_paths(&input[..], &mut output, &[]).unwrap();

        assert_eq!(output, b"");
    }

    #[test]
    fn append_paths_works_with_empty_args() {
        let input = b"README.md\0";
        let mut output = Vec::new();

        append_paths(&input[..], &mut output, &[]).unwrap();

        assert_eq!(output, b"README.md\0");
    }

    #[test]
    fn append_paths_works_with_empty_stdin() {
        let input = b"";
        let mut output = Vec::new();

        append_paths(&input[..], &mut output, &[OsString::from("README.md")]).unwrap();

        assert_eq!(output, b"README.md\0");
    }

    #[test]
    fn append_paths_works_with_unique_stdin_and_args() {
        let input = b"a.rs\0b.rs\0";
        let mut output = Vec::new();

        append_paths(&input[..], &mut output, &[OsString::from("c.rs")]).unwrap();

        assert_eq!(output, b"a.rs\0b.rs\0c.rs\0");
    }

    #[test]
    fn append_paths_keeps_duplicate_entries() {
        let input = b"a.rs\0b.rs\0";
        let mut output = Vec::new();

        append_paths(&input[..], &mut output, &[OsString::from("a.rs")]).unwrap();

        assert_eq!(output, b"a.rs\0b.rs\0a.rs\0");
    }

    #[test]
    fn append_paths_inserts_separator_when_stdin_is_not_null_terminated() {
        let input = b"a.rs";
        let mut output = Vec::new();

        append_paths(&input[..], &mut output, &[OsString::from("b.rs")]).unwrap();

        assert_eq!(output, b"a.rs\0b.rs\0");
    }

    #[test]
    fn append_paths_does_not_insert_extra_separator_when_stdin_is_null_terminated() {
        let input = b"a.rs\0";
        let mut output = Vec::new();

        append_paths(&input[..], &mut output, &[OsString::from("b.rs")]).unwrap();

        assert_eq!(output, b"a.rs\0b.rs\0");
    }

    #[test]
    fn append_paths_handles_partially_terminated_input_sequence() {
        let input = b"a.rs\0b.rs";
        let mut output = Vec::new();

        append_paths(&input[..], &mut output, &[OsString::from("c.rs")]).unwrap();

        assert_eq!(output, b"a.rs\0b.rs\0c.rs\0");
    }

    #[test]
    fn append_paths_does_not_insert_nul_when_no_new_paths_and_input_not_terminated() {
        let input = b"a.rs";
        let mut output = Vec::new();

        append_paths(&input[..], &mut output, &[]).unwrap();

        assert_eq!(output, b"a.rs");
    }

    #[test]
    fn prepend_paths_works_with_empty_stdin_and_args() {
        let input = b"";
        let mut output = Vec::new();

        prepend_paths(&input[..], &mut output, &[]).unwrap();

        assert_eq!(output, b"");
    }

    #[test]
    fn prepend_paths_works_with_empty_args() {
        let input = b"README.md\0";
        let mut output = Vec::new();

        prepend_paths(&input[..], &mut output, &[]).unwrap();

        assert_eq!(output, b"README.md\0");
    }

    #[test]
    fn prepend_paths_works_with_empty_stdin() {
        let input = b"";
        let mut output = Vec::new();

        prepend_paths(&input[..], &mut output, &[OsString::from("README.md")]).unwrap();

        assert_eq!(output, b"README.md\0");
    }

    #[test]
    fn prepend_paths_works_with_unique_stdin_and_args() {
        let input = b"a.rs\0b.rs\0";
        let mut output = Vec::new();

        prepend_paths(&input[..], &mut output, &[OsString::from("c.rs")]).unwrap();

        assert_eq!(output, b"c.rs\0a.rs\0b.rs\0");
    }

    #[test]
    fn prepend_paths_keeps_duplicate_entries() {
        let input = b"a.rs\0b.rs\0";
        let mut output = Vec::new();

        prepend_paths(&input[..], &mut output, &[OsString::from("a.rs")]).unwrap();

        assert_eq!(output, b"a.rs\0a.rs\0b.rs\0");
    }

    #[test]
    fn remove_paths_works_with_empty_stdin_and_args() {
        let input = b"";
        let mut output = Vec::new();

        remove_paths(&input[..], &mut output, &[], Path::new(".")).unwrap();

        assert_eq!(output, b"");
    }

    #[test]
    fn remove_paths_works_with_empty_args() {
        let input = b"README.md\0";
        let mut output = Vec::new();

        remove_paths(&input[..], &mut output, &[], Path::new(".")).unwrap();

        assert_eq!(output, b"README.md\0");
    }

    #[test]
    fn remove_paths_works_with_empty_stdin() {
        let input = b"";
        let mut output = Vec::new();

        remove_paths(
            &input[..],
            &mut output,
            &[PathBuf::from("README.md")],
            Path::new("."),
        )
        .unwrap();

        assert_eq!(output, b"");
    }

    #[test]
    fn remove_paths_filters_matching_entries() {
        let input = b"a.rs\0b.rs\0c.rs\0";
        let mut output = Vec::new();

        remove_paths(
            &input[..],
            &mut output,
            &[PathBuf::from("b.rs")],
            Path::new("."),
        )
        .unwrap();

        assert_eq!(output, b"a.rs\0c.rs\0");
    }

    #[test]
    fn remove_paths_removes_all_matching_entries() {
        let input = b"a.rs\0b.rs\0b.rs\0c.rs\0";
        let mut output = Vec::new();

        remove_paths(
            &input[..],
            &mut output,
            &[PathBuf::from("b.rs")],
            Path::new("."),
        )
        .unwrap();

        assert_eq!(output, b"a.rs\0c.rs\0");
    }

    #[test]
    fn remove_paths_matches_dot_normalized_entries() {
        let input = b"src/./main.rs\0src/lib.rs\0";
        let mut output = Vec::new();

        remove_paths(
            &input[..],
            &mut output,
            &[PathBuf::from("src/main.rs")],
            Path::new("."),
        )
        .unwrap();

        assert_eq!(output, b"src/lib.rs\0");
    }

    #[test]
    fn remove_paths_matches_parent_dir_normalized_entries() {
        let input = b"src/../a.rs\0b.rs\0";
        let mut output = Vec::new();

        remove_paths(
            &input[..],
            &mut output,
            &[PathBuf::from("a.rs")],
            Path::new("."),
        )
        .unwrap();

        assert_eq!(output, b"b.rs\0");
    }

    #[test]
    fn remove_paths_matches_when_unwanted_path_needs_normalization() {
        let input = b"a.rs\0b.rs\0";
        let mut output = Vec::new();

        remove_paths(
            &input[..],
            &mut output,
            &[PathBuf::from("src/../a.rs")],
            Path::new("."),
        )
        .unwrap();

        assert_eq!(output, b"b.rs\0");
    }

    #[test]
    fn remove_paths_matches_when_both_sides_need_normalization() {
        let input = b"src/../a.rs\0b.rs\0";
        let mut output = Vec::new();

        remove_paths(
            &input[..],
            &mut output,
            &[PathBuf::from("./a.rs")],
            Path::new("."),
        )
        .unwrap();

        assert_eq!(output, b"b.rs\0");
    }

    #[test]
    fn remove_paths_preserves_original_bytes_for_kept_paths() {
        let input = b"src/./main.rs\0src/lib.rs\0";
        let mut output = Vec::new();

        remove_paths(
            &input[..],
            &mut output,
            &[PathBuf::from("other.rs")],
            Path::new("."),
        )
        .unwrap();

        assert_eq!(output, b"src/./main.rs\0src/lib.rs\0");
    }

    #[test]
    fn remove_paths_matches_relative_unwanted_path_against_absolute_input_using_origin_base() {
        let temp_dir = tempfile::tempdir().unwrap();
        let origin = temp_dir.path();
        let absolute_path = origin.join("a.rs");

        let mut input = Vec::new();
        input.extend(absolute_path.as_os_str().as_bytes());
        input.push(0);
        input.extend(b"b.rs\0");

        let mut output = Vec::new();

        remove_paths(&input[..], &mut output, &[PathBuf::from("a.rs")], origin).unwrap();

        assert_eq!(output, b"b.rs\0");
    }
}
