use clap::{ArgAction, CommandFactory as _, Parser, ValueHint, value_parser};
use clap_complete::aot::{Shell, generate};
use core::num::NonZeroUsize;
use fdf::filters::{FileTypeFilterParser, SizeFilterParser, TimeFilterParser};
use fdf::walk::Finder;
use fdf::{
    SearchConfigError, TraversalError,
    filters::{FileTypeFilter, SizeFilter, TimeFilter},
};
use std::env;
use std::ffi::OsString;
use std::io::{self, stdout};
use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
use std::process::Command;

#[cfg(all(
    any(target_os = "linux", target_os = "android", target_os = "macos"),
    not(miri),
    not(debug_assertions),
    feature = "mimalloc",
))]
//miri doesnt support custom allocators
#[global_allocator]
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; //Please note, don't  use v3 it has weird bugs. I might try snmalloc in future.

#[derive(Parser)]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[allow(clippy::struct_excessive_bools)]
struct Args {
    #[arg(value_name = "PATTERN", help = "Pattern to search for", index = 1)]
    pattern: Option<String>,
    #[arg(
        value_name = "PATH",
        help = format!("Path to search (defaults to current working directory)"),
        value_hint=ValueHint::DirPath,
        required=false,
        index=2
    )]
    directory: Option<OsString>,
    #[arg(
        short = 'H',
        long = "hidden",
        help = "Shows hidden files eg .gitignore or .bashrc, defaults to off"
    )]
    hidden: bool,

    #[arg(
        short = 'S',
        long = "sort",
        help = "Sort the entries alphabetically (this has quite the performance cost)",
        default_value_t = false
    )]
    sort: bool,
    #[arg(
        short = 's',
        long = "case-sensitive",
        default_value_t = true,
        help = "Enable case-sensitive matching, defaults to false"
    )]
    case_insensitive: bool,
    #[arg(
        short = 'e',
        long = "extension",
        help = "filters based on extension, eg --extension .txt or -E txt",
        long_help = "An example command would be `fdf -HI -e  c '^str' / "
    )]
    extension: Option<String>,
    #[arg(
        short = 'j',
        long = "threads",
        help = "Number of threads to use, defaults to available threads available on your computer"
    )]
    thread_num: Option<NonZeroUsize>,
    #[arg(
        short = 'a',
        long = "absolute-path",
        help = "Starts with the directory entered being resolved to full"
    )]
    absolute_path: bool,

    #[arg(
        short = 'L',
        long = "follow",
        default_value_t = false,
        help = "Include symlinks in traversal,defaults to false"
    )]
    follow_symlinks: bool,
    #[arg(
        long = "nocolour",
        alias = "nocolor",
        default_value_t = false,
        help = "Disable colouring output when sending to terminal"
    )]
    no_colour: bool,
    #[arg(
        short = 'g',
        long = "glob",
        required = false,
        default_value_t = false,
        help = "Use a glob pattern,defaults to off"
    )]
    glob: bool,

    #[arg(
        short = 'n',
        long = "max-results",
        help = "Retrieves the first eg 10 results, 'fdf  -n 10 '.cache' /"
    )]
    top_n: Option<usize>,
    #[arg(
        short = 'd',
        long = "depth",
        alias = "max-depth",
        help = "Retrieves only traverse to x depth"
    )]
    depth: Option<u32>,

    #[arg(
        short = 'p',
        long = "full-path",
        required = false,
        default_value_t = false,
        help = "Use a full path for regex matching, default to false"
    )]
    full_path: bool,

    #[arg(
        short = 'F',
        long = "fixed-strings",
        required = false,
        default_value_t = false,
        help = "Use a fixed string not a regex, defaults to false",
        conflicts_with = "glob"
    )]
    fixed_string: bool,

    #[arg(
        long = "show-errors",
        required = false,
        default_value_t = false,
        help = "Show errors when traversing"
    )]
    show_errors: bool,
    #[arg(
        long = "same-file-system",
        alias="one-file-system", //alias for fd for easier use
        required = false,
        default_value_t = false,
        help = "Only traverse the same filesystem as the starting directory"
    )]
    same_file_system: bool,
    #[arg(
        short = '0',
        long = "print0",
        alias = "null-terminated",
        required = false,
        default_value_t = false,
        help = "Makes all output null terminated",
        long_help = "Makes all output null terminated as opposed to newline terminated only applies to non-coloured output and redirected(useful for xargs)"
    )]
    print0: bool,
    #[arg(
        short = 'I',
        long = "no-ignore",
        default_value_t = false,
        help = "Do not respect .gitignore rules during traversal"
    )]
    no_ignore: bool,
    #[arg(
        long = "strip-cwd-prefix",
        default_value_t = false,
        help = "Strip the leading './' from results when searching the current directory"
    )]
    strip_cwd_prefix: bool,
    #[arg(
        short = 'Q',
        long = "quoted",
        default_value_t = false,
        help = "Wrap printed file paths in double quotes"
    )]
    quoted: bool,
    #[arg(
        long = "exec",
        value_name = "CMD",
        num_args = 1..,
        allow_hyphen_values = true,
        conflicts_with_all = ["generate", "quoted", "print0", "no_colour"],
        help = "Execute a command once per search result",
        long_help = "Execute a command once per search result.\nUse '{}' to insert the matched path into an argument; if '{}' is omitted, the path is appended as the final argument. This option should be the final CLI flag.\nExample: 'fdf 'junk.files' 'test_directory' -HI --exec rm -rf ' , delete all files meeting the criteria"
    )]
    exec: Option<Vec<OsString>>,
    #[arg(
        long = "ignore",
        value_name = "PATTERN",
        action = ArgAction::Append,
        help = "Ignore paths that match this regex pattern (repeatable)"
    )]
    ignore: Vec<String>,
    #[arg(
        long = "ignoreg",
        value_name = "GLOB",
        action = ArgAction::Append,
        help = "Ignore paths that match this glob pattern (repeatable)"
    )]
    ignoreg: Vec<String>,
    #[arg(
        long = "ignore-file",
        value_name = "path",
        action = ArgAction::Append,
        value_hint = ValueHint::FilePath,
        help = "Add a custom ignore-file in '.gitignore' format. These files have a low precedence."
    )]
    ignore_file: Vec<OsString>,
    #[arg(
        long = "and",
        value_name = "pattern",
        action = ArgAction::Append,
        help = "Add additional required search patterns, all of which must be matched.\nMultiple additional patterns can be specified. The patterns are regular expressions, unless '--glob' or '--fixed-strings' is used."
    )]
    and_opt: Vec<String>,
    /// Filter by file size
    ///
    /// PREFIXES:
    ///   +SIZE    Find files larger than SIZE
    ///   -SIZE    Find files smaller than SIZE
    ///    SIZE     Find files exactly SIZE (default)
    ///
    /// UNITS:
    ///   b        Bytes (default if no unit specified)
    ///   k, kb    Kilobytes (1000 bytes)
    ///   ki, kib  Kibibytes (1024 bytes)
    ///   m, mb    Megabytes (1000^2 bytes)
    ///   mi, mib  Mebibytes (1024^2 bytes)
    ///   g, gb    Gigabytes (1000^3 bytes)
    ///   gi, gib  Gibibytes (1024^3 bytes)
    ///   t, tb    Terabytes (1000^4 bytes)
    ///   ti, tib  Tebibytes (1024^4 bytes)
    ///
    /// EXAMPLES:
    ///   --size 100         Files exactly 100 bytes
    ///   --size +1k         Files larger than 1000 bytes
    ///   --size -10mb       Files smaller than 10 megabytes
    ///   --size +1gi        Files larger than 1 gibibyte
    ///   --size 500ki       Files exactly 500 kibibytes
    #[arg(
    long = "size",
    allow_hyphen_values = true,
    value_name = "SIZE",
    value_parser = SizeFilterParser,
    help = "Filter by file size (supports custom sizes with +/- prefixes)",
    verbatim_doc_comment
)]
    size: Option<SizeFilter>,
    /// Filter by file modification time
    ///
    /// PREFIXES:
    ///   -TIME    Find files modified within the last TIME (newer)
    ///   +TIME    Find files modified more than TIME ago (older)
    ///    TIME    Same as -TIME (default)
    ///
    /// TIME RANGE:
    ///   TIME..TIME   Find files modified between two times
    ///
    /// UNITS:
    ///   s, sec, second, seconds     - Seconds
    ///   m, min, minute, minutes     - Minutes
    ///   h, hour, hours              - Hours
    ///   d, day, days                - Days
    ///   w, week, weeks              - Weeks
    ///   y, year, years              - Years
    ///
    /// EXAMPLES:
    ///   --time -1h        Files modified within the last hour
    ///   --time +2d        Files modified more than 2 days ago
    ///   --time 1d..2h     Files modified between 1 day and 2 hours ago
    ///   --time -30m       Files modified within the last 30 minutes
    #[arg(
    long = "time-modified",
    short = 'T',
    allow_hyphen_values = true,
    value_name = "TIME",
    value_parser = TimeFilterParser,
    help = "Filter by file modification time (supports relative times with +/- prefixes)",
    verbatim_doc_comment
)]
    time: Option<TimeFilter>,

    #[arg(
    short = 't',
    long = "type",
    required = false,
    value_parser = FileTypeFilterParser,
    help = "Filter by file type",

)]
    type_of: Option<FileTypeFilter>,
    #[arg(
    long = "generate",
    action = ArgAction::Set,
    value_parser = value_parser!(Shell),
    help = "Generate shell completions",
    long_help = "
    Generate shell completions for bash/zsh/fish/powershell
    To use: eval \"$(fdf --generate SHELL)\"
    Example:
    # Add to shell config for permanent use
    echo 'eval \"$(fdf --generate zsh)\"' >> ~/.zshrc && source ~/.zshrc "
)]
    generate: Option<Shell>,
}

fn main() -> Result<(), SearchConfigError> {
    let args = Args::parse();

    if let Some(generator) = args.generate {
        let mut cmd = Args::command();
        let bin_name = cmd.get_name().to_owned();
        cmd.set_bin_name("fdf");

        generate(generator, &mut cmd, bin_name, &mut stdout());
        return Ok(());
    }

    let path: OsString = args.directory.unwrap_or_else(|| ".".into());
    // Only strip `./` when the root is actually `.` or `./`; that is the only case
    // where every emitted path is guaranteed to carry that prefix (safety invariant).
    let root_is_cwd = matches!(path.as_bytes(), b"." | b"./");
    let strip_cwd_prefix = args.strip_cwd_prefix && root_is_cwd;

    let finder = Finder::init(&path)
        .pattern(args.pattern.unwrap_or_else(String::new)) //empty string
        .and_patterns(args.and_opt)
        .keep_hidden(!args.hidden)
        .case_insensitive(args.case_insensitive)
        .fixed_string(args.fixed_string)
        .canonicalise_root(args.absolute_path)
        .file_name_only(!args.full_path)
        .extension(args.extension.unwrap_or_else(String::new))
        .max_depth(args.depth)
        .follow_symlinks(args.follow_symlinks)
        .filter_by_size(args.size)
        .filter_by_time(args.time)
        .type_filter(args.type_of)
        .collect_errors(args.show_errors)
        .use_glob(args.glob)
        .same_filesystem(args.same_file_system)
        .respect_gitignore(!args.no_ignore)
        .ignore_patterns(args.ignore)
        .ignore_glob_patterns(args.ignoreg)
        .ignore_files(args.ignore_file)
        .thread_count(args.thread_num)
        .build()?;

    let errors = finder.error_store();

    if let Some(exec) = args.exec.as_deref() {
        run_exec_search(
            finder.traverse()?,
            exec,
            args.sort,
            args.top_n,
            strip_cwd_prefix,
        )?;

        if args.show_errors {
            print_collected_errors(errors.as_deref());
        }

        return Ok(());
    }

    finder
        .build_printer()?
        .limit(args.top_n)
        .sort(args.sort)
        .null_terminated(args.print0)
        .nocolour(args.no_colour)
        .quoted(args.quoted)
        .strip_leading_dot_slash(strip_cwd_prefix)
        .print_errors(args.show_errors)
        .print()?;

    Ok(())
}
#[allow(clippy::print_stderr)] // CLI opt
fn print_collected_errors(errors: Option<&std::sync::Mutex<Vec<TraversalError>>>) {
    if let Some(errors_arc) = errors
        && let Ok(error_vec) = errors_arc.lock()
    {
        for error in error_vec.iter() {
            eprintln!("{error}");
        }
    }
}

fn run_exec_search<I>(
    paths: I,
    exec: &[OsString],
    sort: bool,
    limit: Option<usize>,
    strip_leading_dot_slash: bool,
) -> Result<(), SearchConfigError>
where
    I: Iterator<Item = fdf::fs::DirEntry>,
{
    let new_limit = limit.unwrap_or(usize::MAX);

    if sort {
        let mut collected: Vec<_> = paths.collect();
        collected.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));

        for path in collected.into_iter().take(new_limit) {
            execute_for_path(exec, &path, strip_leading_dot_slash)?;
        }
    } else {
        for path in paths.take(new_limit) {
            execute_for_path(exec, &path, strip_leading_dot_slash)?;
        }
    }

    Ok(())
}
#[allow(clippy::indexing_slicing)]
fn execute_for_path(
    exec: &[OsString],
    path: &fdf::fs::DirEntry,
    strip_leading_dot_slash: bool,
) -> Result<(), SearchConfigError> {
    let argv = build_exec_argv(exec, displayed_path_bytes(path, strip_leading_dot_slash));
    let status = Command::new(&argv[0]).args(&argv[1..]).status()?;

    if status.success() {
        return Ok(());
    }

    let command_name = argv[0].as_os_str().to_string_lossy();
    Err(SearchConfigError::IOError(io::Error::other(format!(
        "command '{command_name}' exited with status {status}"
    ))))
}

fn displayed_path_bytes(path: &fdf::fs::DirEntry, strip_leading_dot_slash: bool) -> &[u8] {
    let start = usize::from(strip_leading_dot_slash) * 2;
    // SAFETY: `strip_leading_dot_slash` is only enabled when the root is `.` or `./`,
    // so every emitted path is guaranteed to start with `./`.
    unsafe { path.get_unchecked(start..) }
}

fn build_exec_argv(exec: &[OsString], path: &[u8]) -> Vec<OsString> {
    let mut argv = Vec::with_capacity(exec.len() + 1);
    let mut replaced_placeholder = false;

    for arg in exec {
        let (replaced, did_replace) = replace_exec_placeholder(arg, path);
        replaced_placeholder |= did_replace;
        argv.push(replaced);
    }

    if !replaced_placeholder {
        argv.push(OsString::from_vec(path.to_vec()));
    }

    argv
}
#[allow(clippy::indexing_slicing)]
fn replace_exec_placeholder(arg: &OsString, path: &[u8]) -> (OsString, bool) {
    let bytes = arg.as_os_str().as_bytes();
    let mut index = 0;
    let mut replaced = false;
    let mut output = Vec::with_capacity(bytes.len().saturating_add(path.len()));

    while let Some(relative_pos) = bytes[index..].windows(2).position(|window| window == b"{}") {
        let absolute_pos = index + relative_pos;
        output.extend_from_slice(&bytes[index..absolute_pos]);
        output.extend_from_slice(path);
        index = absolute_pos + 2;
        replaced = true;
    }

    if !replaced {
        return (arg.clone(), false);
    }

    output.extend_from_slice(&bytes[index..]);
    (OsString::from_vec(output), true)
}
