//! Utilities for File Operations
use crate::scripts::{ALL_EXTS, ARCHIVE_EXTS};
#[cfg(not(windows))]
use std::ffi::OsString;
use std::fs;
use std::io;
use std::io::{Read, Write};
#[cfg(not(windows))]
use std::path::Component;
use std::path::{Path, PathBuf};

/// Returns the relative path from `root` to `target`.
pub fn relative_path<P: AsRef<Path>, T: AsRef<Path>>(root: P, target: T) -> PathBuf {
    let root = root
        .as_ref()
        .canonicalize()
        .unwrap_or_else(|_| root.as_ref().to_path_buf());
    let target = target
        .as_ref()
        .canonicalize()
        .unwrap_or_else(|_| target.as_ref().to_path_buf());

    let mut root_components: Vec<_> = root.components().collect();
    let mut target_components: Vec<_> = target.components().collect();

    // Remove common prefix
    while !root_components.is_empty()
        && !target_components.is_empty()
        && root_components[0] == target_components[0]
    {
        root_components.remove(0);
        target_components.remove(0);
    }

    // Add ".." for each remaining root component
    let mut result = PathBuf::new();
    for _ in root_components {
        result.push("..");
    }

    // Add remaining target components
    for component in target_components {
        result.push(component);
    }

    result
}

/// Finds all files in the specified directory and its subdirectories.
pub fn find_files(path: &str, recursive: bool, no_ext_filter: bool) -> io::Result<Vec<String>> {
    let mut result = Vec::new();
    let dir_path = Path::new(&path);

    if dir_path.is_dir() {
        for entry in fs::read_dir(dir_path)? {
            let entry = entry?;
            let path = entry.path();

            if path.is_file()
                && (no_ext_filter
                    || path.file_name().map_or(false, |file| {
                        path.extension().map_or(true, |_| {
                            let file = file.to_string_lossy().to_lowercase();
                            for ext in ALL_EXTS.iter() {
                                if file.ends_with(&format!(".{}", ext)) {
                                    return true;
                                }
                            }
                            false
                        })
                    }))
            {
                if let Some(path_str) = path.to_str() {
                    result.push(path_str.to_string());
                }
            } else if recursive && path.is_dir() {
                if let Some(path_str) = path.to_str() {
                    let mut sub_files =
                        find_files(&path_str.to_string(), recursive, no_ext_filter)?;
                    result.append(&mut sub_files);
                }
            }
        }
    }

    Ok(result)
}

/// Finds all archive files in the specified directory and its subdirectories.
pub fn find_arc_files(path: &str, recursive: bool) -> io::Result<Vec<String>> {
    let mut result = Vec::new();
    let dir_path = Path::new(&path);

    if dir_path.is_dir() {
        for entry in fs::read_dir(dir_path)? {
            let entry = entry?;
            let path = entry.path();

            if path.is_file()
                && path.file_name().map_or(false, |file| {
                    path.extension().map_or(true, |_| {
                        let file = file.to_string_lossy().to_lowercase();
                        for ext in ARCHIVE_EXTS.iter() {
                            if file.ends_with(&format!(".{}", ext)) {
                                return true;
                            }
                        }
                        false
                    })
                })
            {
                if let Some(path_str) = path.to_str() {
                    result.push(path_str.to_string());
                }
            } else if recursive && path.is_dir() {
                if let Some(path_str) = path.to_str() {
                    let mut sub_files = find_arc_files(&path_str.to_string(), recursive)?;
                    result.append(&mut sub_files);
                }
            }
        }
    }

    Ok(result)
}

/// Collects files from the specified path, either as a directory or a single file.
pub fn collect_files(
    path: &str,
    recursive: bool,
    no_ext_filter: bool,
) -> io::Result<(Vec<String>, bool)> {
    let pa = Path::new(path);
    if pa.is_dir() {
        return Ok((find_files(path, recursive, no_ext_filter)?, true));
    }
    if pa.is_file() {
        return Ok((vec![path.to_string()], false));
    }
    Err(io::Error::new(
        io::ErrorKind::NotFound,
        format!("Path {} is neither a file nor a directory", pa.display()),
    ))
}

/// Finds all files with specific extensions in the specified directory and its subdirectories.
pub fn find_ext_files(path: &str, recursive: bool, exts: &[&str]) -> io::Result<Vec<String>> {
    let mut result = Vec::new();
    let dir_path = Path::new(&path);

    if dir_path.is_dir() {
        for entry in fs::read_dir(dir_path)? {
            let entry = entry?;
            let path = entry.path();

            if path.is_file()
                && path.file_name().map_or(false, |file| {
                    path.extension().map_or(true, |_| {
                        let file = file.to_string_lossy().to_lowercase();
                        for ext in exts {
                            if file.ends_with(&format!(".{}", ext)) {
                                return true;
                            }
                        }
                        false
                    })
                })
            {
                if let Some(path_str) = path.to_str() {
                    result.push(path_str.to_string());
                }
            } else if recursive && path.is_dir() {
                if let Some(path_str) = path.to_str() {
                    let mut sub_files = find_ext_files(path_str, recursive, exts)?;
                    result.append(&mut sub_files);
                }
            }
        }
    }

    Ok(result)
}

/// Collects files with specific extensions from the specified path, either as a directory or a single file.
pub fn collect_ext_files(
    path: &str,
    recursive: bool,
    exts: &[&str],
) -> io::Result<(Vec<String>, bool)> {
    let pa = Path::new(path);
    if pa.is_dir() {
        return Ok((find_ext_files(path, recursive, exts)?, true));
    }
    if pa.is_file() {
        return Ok((vec![path.to_string()], false));
    }
    Err(io::Error::new(
        io::ErrorKind::NotFound,
        format!("Path {} is neither a file nor a directory", pa.display()),
    ))
}

/// Collects archive files from the specified path, either as a directory or a single file.
pub fn collect_arc_files(path: &str, recursive: bool) -> io::Result<(Vec<String>, bool)> {
    let pa = Path::new(path);
    if pa.is_dir() {
        return Ok((find_arc_files(path, recursive)?, true));
    }
    if pa.is_file() {
        return Ok((vec![path.to_string()], false));
    }
    Err(io::Error::new(
        io::ErrorKind::NotFound,
        format!("Path {} is neither a file nor a directory", pa.display()),
    ))
}

/// Reads the content of a file or standard input if the path is "-".
pub fn read_file<F: AsRef<Path> + ?Sized>(f: &F) -> io::Result<Vec<u8>> {
    let mut content = Vec::new();
    if f.as_ref() == Path::new("-") {
        io::stdin().read_to_end(&mut content)?;
    } else {
        content = fs::read(f)?;
    }
    Ok(content)
}

/// Writes content to a file or standard output if the path is "-".
pub fn write_file<F: AsRef<Path> + ?Sized>(f: &F) -> io::Result<Box<dyn Write>> {
    Ok(if f.as_ref() == Path::new("-") {
        Box::new(io::stdout())
    } else {
        Box::new(fs::File::create(f)?)
    })
}

/// Ensures that the parent directory for the specified path exists, creating it if necessary.
pub fn make_sure_dir_exists<F: AsRef<Path> + ?Sized>(f: &F) -> io::Result<()> {
    let path = f.as_ref();
    if let Some(parent) = path.parent() {
        if !parent.exists() {
            fs::create_dir_all(parent)?;
        }
    }
    Ok(())
}

/// Replace symbols not allowed in Windows path with underscores.
pub fn sanitize_path(path: &str) -> String {
    // Split path into components, preserving separators
    if path.is_empty() {
        return String::new();
    }

    let invalid_chars: &[char] = &['<', '>', '"', '|', '?', '*'];
    let mut result = String::with_capacity(path.len());

    let reserved_names: Vec<String> = {
        let mut v = vec!["CON", "PRN", "AUX", "NUL"]
            .into_iter()
            .map(|s| s.to_string())
            .collect::<Vec<_>>();
        for i in 1..=9 {
            v.push(format!("COM{}", i));
            v.push(format!("LPT{}", i));
        }
        v
    };

    let bytes = path.as_bytes();
    let len = bytes.len();
    let mut start = 0usize;

    while start < len {
        // find next separator index
        let mut end = start;
        while end < len && bytes[end] != b'\\' && bytes[end] != b'/' {
            end += 1;
        }
        // segment is path[start..end]
        let seg = &path[start..end];

        // sanitize segment
        let mut s = String::with_capacity(seg.len());
        for (i, ch) in seg.chars().enumerate() {
            // allow drive letter colon like "C:" (i == 1, first char is ASCII letter)
            if ch == ':' {
                if i == 1 {
                    // check first char is ASCII letter
                    if seg
                        .chars()
                        .next()
                        .map(|c| c.is_ascii_alphabetic())
                        .unwrap_or(false)
                    {
                        s.push(':');
                        continue;
                    }
                }
                // otherwise treat as invalid
                s.push('_');
                continue;
            }
            // keep separators out of segment (shouldn't appear here)
            // replace control chars and other invalids
            if (ch as u32) < 32 || invalid_chars.contains(&ch) {
                s.push('_');
            } else {
                s.push(ch);
            }
        }

        // trim trailing spaces and dots (Windows disallows filenames ending with space or dot)
        while s.ends_with(' ') || s.ends_with('.') {
            s.pop();
        }

        if s.is_empty() {
            s.push('_');
        } else {
            // check reserved names (base name before first '.')
            let base = s.split('.').next().unwrap_or("").to_ascii_uppercase();
            if reserved_names.iter().any(|r| r == &base) {
                s = format!("_{}", s);
            }
        }

        result.push_str(&s);

        // append separator if present
        if end < len {
            // keep original separator (preserve '\' or '/')
            result.push(path.as_bytes()[end] as char);
            start = end + 1;
        } else {
            start = end;
        }
    }

    result
}

pub fn get_ignorecase_path<P: AsRef<Path>>(path: P) -> std::io::Result<PathBuf> {
    #[cfg(windows)]
    return Ok(path.as_ref().to_path_buf());
    #[cfg(not(windows))]
    {
        let path = path.as_ref();
        // If the path exists as is, return it
        if path.exists() {
            return Ok(path.to_path_buf());
        }
        {
            // Helper: try to resolve the remaining tail components starting from base,
            // performing case-insensitive matches for each step.
            fn resolve_from_base(base: PathBuf, tail: &[OsString]) -> io::Result<Option<PathBuf>> {
                let mut cur = base;
                for comp in tail {
                    let direct = cur.join(comp);
                    if direct.exists() {
                        cur = direct;
                        continue;
                    }

                    if !cur.is_dir() {
                        return Ok(None);
                    }

                    let mut found = None;
                    for entry in fs::read_dir(&cur)? {
                        let entry = entry?;
                        let name = entry.file_name();
                        if name
                            .to_string_lossy()
                            .eq_ignore_ascii_case(&comp.to_string_lossy())
                        {
                            found = Some(cur.join(name));
                            break;
                        }
                    }

                    match found {
                        Some(p) => cur = p,
                        None => return Ok(None),
                    }
                }
                Ok(Some(cur))
            }

            let orig = path;
            // If it exists as-is, return immediately
            if orig.exists() {
                return Ok(orig.to_path_buf());
            }

            // Collect components as OsString (preserve Prefix/RootDir as components)
            let comps: Vec<OsString> = orig
                .components()
                .map(|c| match c {
                    Component::Prefix(p) => p.as_os_str().to_os_string(),
                    Component::RootDir => OsString::from(std::path::MAIN_SEPARATOR.to_string()),
                    other => other.as_os_str().to_os_string(),
                })
                .collect();

            // Try replacing components from the bottom (leaf) upward.
            let len = comps.len();
            for idx in (0..len).rev() {
                // Build parent path from comps[0..idx]
                let mut parent = PathBuf::new();
                for j in 0..idx {
                    parent.push(&comps[j]);
                }
                if parent.as_os_str().is_empty() {
                    parent = PathBuf::from(".");
                }

                // If parent doesn't exist or is not a directory, skip this level
                if !parent.exists() || !parent.is_dir() {
                    continue;
                }

                // Look for a case-insensitive match for the component at idx inside parent
                let target = &comps[idx];
                let mut matched_name: Option<OsString> = None;
                for entry in fs::read_dir(&parent)? {
                    let entry = entry?;
                    let name = entry.file_name();
                    if name
                        .to_string_lossy()
                        .eq_ignore_ascii_case(&target.to_string_lossy())
                    {
                        matched_name = Some(name);
                        break;
                    }
                }

                if let Some(name) = matched_name {
                    // Reconstruct candidate path: parent + matched_name + remaining original tail
                    let candidate_base = parent.join(name);
                    let tail: Vec<OsString> = comps.iter().skip(idx + 1).cloned().collect();
                    if tail.is_empty() {
                        if candidate_base.exists() {
                            return Ok(candidate_base);
                        } else {
                            // Even if leaf matched, final file may not exist (e.g., different deeper casing),
                            // attempt to resolve remaining components (none here) so treat as not found.
                            continue;
                        }
                    }

                    if let Some(resolved) = resolve_from_base(candidate_base, &tail)? {
                        return Ok(resolved);
                    }
                }
            }

            Err(io::Error::new(
                io::ErrorKind::NotFound,
                format!(
                    "Path {} not found (case-insensitive search failed)",
                    path.display()
                ),
            ))
        }
    }
}
