use crate::fs_model::{home_dir, human_size, trash_files_dir, trash_info_dir};
use std::env;
use std::ffi::OsStr;
use std::fs::{self, Metadata};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{self, Receiver, SyncSender};
use std::thread;
use std::time::{SystemTime, UNIX_EPOCH};

const OPERATION_EVENT_CAPACITY: usize = 64;
pub const CHILD_REAPER_LIMIT: usize = 32;
pub const OPERATION_SOURCE_LIMIT: usize = 20_000;
pub const OPERATION_ITEM_LIMIT: usize = 250_000;
pub const OPERATION_DEPTH_LIMIT: usize = 128;

static ACTIVE_CHILD_REAPERS: AtomicUsize = AtomicUsize::new(0);

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ClipboardMode {
    Copy,
    Cut,
}

#[derive(Clone, Debug)]
pub struct ClipboardPayload {
    pub mode: ClipboardMode,
    pub paths: Vec<PathBuf>,
}

impl ClipboardPayload {
    pub fn label(&self) -> &'static str {
        match self.mode {
            ClipboardMode::Copy => "COPY BUFFER",
            ClipboardMode::Cut => "CUT BUFFER",
        }
    }
}

#[derive(Clone, Debug)]
pub enum OperationRequest {
    Copy {
        sources: Vec<PathBuf>,
        target_dir: PathBuf,
    },
    Move {
        sources: Vec<PathBuf>,
        target_dir: PathBuf,
    },
    Duplicate {
        sources: Vec<PathBuf>,
    },
    Trash {
        sources: Vec<PathBuf>,
    },
    Delete {
        sources: Vec<PathBuf>,
    },
    Undo(UndoAction),
}

impl OperationRequest {
    pub fn title(&self) -> &'static str {
        match self {
            Self::Copy { .. } => "COPY",
            Self::Move { .. } => "MOVE",
            Self::Duplicate { .. } => "DUPLICATE",
            Self::Trash { .. } => "MOVE TO TRASH",
            Self::Delete { .. } => "PERMANENT DELETE",
            Self::Undo(_) => "UNDO",
        }
    }
}

#[derive(Clone, Debug)]
pub enum UndoAction {
    RemoveCreated {
        paths: Vec<PathBuf>,
        label: String,
    },
    MoveBack {
        moves: Vec<FileMove>,
        label: String,
    },
    RestoreTrash {
        records: Vec<TrashRecord>,
        label: String,
    },
    RenameBack {
        from: PathBuf,
        to: PathBuf,
    },
}

impl UndoAction {
    pub fn label(&self) -> &str {
        match self {
            Self::RemoveCreated { label, .. }
            | Self::MoveBack { label, .. }
            | Self::RestoreTrash { label, .. } => label,
            Self::RenameBack { .. } => "UNDO RENAME",
        }
    }
}

#[derive(Clone, Debug)]
pub struct FileMove {
    pub from: PathBuf,
    pub to: PathBuf,
}

#[derive(Clone, Debug)]
pub struct TrashRecord {
    pub original_path: PathBuf,
    pub trashed_path: PathBuf,
    pub info_path: PathBuf,
}

#[derive(Clone, Debug)]
pub enum CompletedAction {
    Copied(Vec<PathBuf>),
    Moved(Vec<FileMove>),
    Duplicated(Vec<PathBuf>),
    Trashed(Vec<TrashRecord>),
    Deleted(Vec<PathBuf>),
    UndoApplied,
}

#[derive(Clone, Debug)]
pub struct OperationOutcome {
    pub summary: String,
    pub action: CompletedAction,
}

#[derive(Clone, Debug)]
pub struct OperationProgress {
    pub done_items: usize,
    pub total_items: usize,
    pub done_bytes: u64,
    pub total_bytes: u64,
    pub current: String,
}

#[derive(Clone, Debug)]
pub enum OperationEvent {
    Started {
        title: String,
        total_items: usize,
        total_bytes: u64,
    },
    Progress(OperationProgress),
    Finished(Result<OperationOutcome, String>),
}

pub fn start_operation(request: OperationRequest) -> Receiver<OperationEvent> {
    let (tx, rx) = mpsc::sync_channel(OPERATION_EVENT_CAPACITY);
    let worker_tx = tx.clone();
    if let Err(error) = thread::Builder::new()
        .name("guth-file-operation".to_string())
        .spawn(move || {
            let result = run_operation(request, &worker_tx);
            let _ = worker_tx.send(OperationEvent::Finished(result));
        })
    {
        let _ = tx.send(OperationEvent::Finished(Err(format!(
            "Could not start operation worker: {error}"
        ))));
    }
    rx
}

pub fn undo_for_created_folder(path: PathBuf) -> UndoAction {
    UndoAction::RemoveCreated {
        paths: vec![path],
        label: "UNDO NEW FOLDER".to_string(),
    }
}

pub fn undo_for_created_file(path: PathBuf) -> UndoAction {
    UndoAction::RemoveCreated {
        paths: vec![path],
        label: "UNDO NEW FILE".to_string(),
    }
}

pub fn undo_for_rename(from: PathBuf, to: PathBuf) -> UndoAction {
    UndoAction::RenameBack { from, to }
}

pub fn launch_default(path: &Path) -> Result<(), String> {
    let mut xdg_open = Command::new("xdg-open");
    xdg_open.arg(path);
    spawn_managed_child(xdg_open, &format!("default opener {}", path.display())).or_else(
        |first_error| {
            let mut gio = Command::new("gio");
            gio.arg("open").arg(path);
            spawn_managed_child(gio, &format!("gio open {}", path.display())).map_err(
                |second_error| {
                    format!(
                        "No default opener accepted {}: {first_error}; {second_error}",
                        path.display()
                    )
                },
            )
        },
    )
}

pub fn launch_terminal_at(path: &Path) -> Result<(), String> {
    let cwd = if path.is_dir() {
        path.to_path_buf()
    } else {
        path.parent()
            .unwrap_or_else(|| Path::new("/"))
            .to_path_buf()
    };

    let mut candidates = Vec::new();
    if let Ok(terminal) = env::var("TERMINAL") {
        candidates.push(terminal);
    }
    candidates.extend([
        "kgx".to_string(),
        "gnome-terminal".to_string(),
        "x-terminal-emulator".to_string(),
        "konsole".to_string(),
        "xfce4-terminal".to_string(),
        "alacritty".to_string(),
        "kitty".to_string(),
        "xterm".to_string(),
    ]);

    for binary in candidates {
        let mut command = Command::new(&binary);
        command.current_dir(&cwd);
        if spawn_managed_child(command, &format!("terminal {binary}")).is_ok() {
            return Ok(());
        }
    }

    Err(format!(
        "No terminal emulator launched in {}",
        cwd.display()
    ))
}

pub fn launch_new_window(path: &Path) -> Result<(), String> {
    let current_exe =
        env::current_exe().map_err(|error| format!("Cannot locate executable: {error}"))?;
    let mut command = Command::new(current_exe);
    command.arg(path);
    spawn_managed_child(command, &format!("new window {}", path.display()))
        .map_err(|error| format!("Cannot open new window: {error}"))
}

pub fn active_child_reapers() -> usize {
    ACTIVE_CHILD_REAPERS.load(Ordering::Relaxed)
}

fn spawn_managed_child(mut command: Command, label: &str) -> Result<(), String> {
    let permit = ChildReaperPermit::try_acquire()?;
    let (child_tx, child_rx) = mpsc::sync_channel::<Child>(1);
    thread::Builder::new()
        .name("guth-child-reaper".to_string())
        .spawn(move || {
            let _permit = permit;
            if let Ok(mut child) = child_rx.recv() {
                let _ = child.wait();
            }
        })
        .map_err(|error| format!("Could not start child reaper for {label}: {error}"))?;

    let child = command
        .spawn()
        .map_err(|error| format!("Could not launch {label}: {error}"))?;
    match child_tx.send(child) {
        Ok(()) => Ok(()),
        Err(error) => {
            let mut child = error.0;
            let _ = child.kill();
            let _ = child.wait();
            Err(format!("Child reaper stopped before receiving {label}"))
        }
    }
}

struct ChildReaperPermit;

impl ChildReaperPermit {
    fn try_acquire() -> Result<Self, String> {
        let mut current = ACTIVE_CHILD_REAPERS.load(Ordering::Relaxed);
        loop {
            if current >= CHILD_REAPER_LIMIT {
                return Err(format!(
                    "Child process reaper limit reached: {current}/{CHILD_REAPER_LIMIT}"
                ));
            }
            match ACTIVE_CHILD_REAPERS.compare_exchange_weak(
                current,
                current + 1,
                Ordering::AcqRel,
                Ordering::Relaxed,
            ) {
                Ok(_) => return Ok(Self),
                Err(actual) => current = actual,
            }
        }
    }
}

impl Drop for ChildReaperPermit {
    fn drop(&mut self) {
        ACTIVE_CHILD_REAPERS.fetch_sub(1, Ordering::AcqRel);
    }
}

fn run_operation(
    request: OperationRequest,
    tx: &SyncSender<OperationEvent>,
) -> Result<OperationOutcome, String> {
    match request {
        OperationRequest::Copy {
            sources,
            target_dir,
        } => run_copy(sources, target_dir, tx),
        OperationRequest::Move {
            sources,
            target_dir,
        } => run_move(sources, target_dir, tx),
        OperationRequest::Duplicate { sources } => run_duplicate(sources, tx),
        OperationRequest::Trash { sources } => run_trash(sources, tx),
        OperationRequest::Delete { sources } => run_delete(sources, tx),
        OperationRequest::Undo(action) => run_undo(action, tx),
    }
}

fn run_copy(
    sources: Vec<PathBuf>,
    target_dir: PathBuf,
    tx: &SyncSender<OperationEvent>,
) -> Result<OperationOutcome, String> {
    let total = measure_sources(&sources)?;
    send_started(tx, "COPY", total);
    let mut tracker = ProgressTracker::new(tx, total);
    let mut created = Vec::new();

    for source in sources {
        let name = file_name_string(&source)?;
        let destination = unique_child_path(&target_dir, &name);
        ensure_destination_is_not_inside_source(&source, &destination)?;
        copy_path(&source, &destination, &mut tracker).map_err(|error| {
            format!(
                "Copy failed for {} -> {}: {error}",
                source.display(),
                destination.display()
            )
        })?;
        created.push(destination);
    }

    Ok(OperationOutcome {
        summary: format!("COPIED {}", plural_items(created.len())),
        action: CompletedAction::Copied(created),
    })
}

fn run_move(
    sources: Vec<PathBuf>,
    target_dir: PathBuf,
    tx: &SyncSender<OperationEvent>,
) -> Result<OperationOutcome, String> {
    let total = measure_sources(&sources)?;
    send_started(tx, "MOVE", total);
    let mut tracker = ProgressTracker::new(tx, total);
    let mut moves = Vec::new();

    for source in sources {
        let name = file_name_string(&source)?;
        let destination = unique_child_path(&target_dir, &name);
        ensure_destination_is_not_inside_source(&source, &destination)?;
        move_path(&source, &destination, &mut tracker).map_err(|error| {
            format!(
                "Move failed for {} -> {}: {error}",
                source.display(),
                destination.display()
            )
        })?;
        moves.push(FileMove {
            from: source,
            to: destination,
        });
    }

    Ok(OperationOutcome {
        summary: format!("MOVED {}", plural_items(moves.len())),
        action: CompletedAction::Moved(moves),
    })
}

fn run_duplicate(
    sources: Vec<PathBuf>,
    tx: &SyncSender<OperationEvent>,
) -> Result<OperationOutcome, String> {
    let total = measure_sources(&sources)?;
    send_started(tx, "DUPLICATE", total);
    let mut tracker = ProgressTracker::new(tx, total);
    let mut created = Vec::new();

    for source in sources {
        let parent = source
            .parent()
            .ok_or_else(|| format!("Cannot duplicate {} without a parent", source.display()))?;
        let name = file_name_string(&source)?;
        let destination = unique_copy_path(parent, &name);
        copy_path(&source, &destination, &mut tracker).map_err(|error| {
            format!(
                "Duplicate failed for {} -> {}: {error}",
                source.display(),
                destination.display()
            )
        })?;
        created.push(destination);
    }

    Ok(OperationOutcome {
        summary: format!("DUPLICATED {}", plural_items(created.len())),
        action: CompletedAction::Duplicated(created),
    })
}

fn run_trash(
    sources: Vec<PathBuf>,
    tx: &SyncSender<OperationEvent>,
) -> Result<OperationOutcome, String> {
    let total = measure_sources(&sources)?;
    send_started(tx, "MOVE TO TRASH", total);
    let mut tracker = ProgressTracker::new(tx, total);
    let files_dir = trash_files_dir();
    let info_dir = trash_info_dir();
    fs::create_dir_all(&files_dir)
        .map_err(|error| format!("Cannot create trash files dir: {error}"))?;
    fs::create_dir_all(&info_dir)
        .map_err(|error| format!("Cannot create trash info dir: {error}"))?;
    let mut records = Vec::new();

    for source in sources {
        let name = file_name_string(&source)?;
        let trashed_path = unique_child_path(&files_dir, &name);
        let info_name = format!("{}.trashinfo", file_name_string(&trashed_path)?);
        let info_path = info_dir.join(info_name);
        write_trash_info(&info_path, &source)?;
        move_path(&source, &trashed_path, &mut tracker).map_err(|error| {
            let _ = fs::remove_file(&info_path);
            format!("Trash failed for {}: {error}", source.display())
        })?;
        records.push(TrashRecord {
            original_path: source,
            trashed_path,
            info_path,
        });
    }

    Ok(OperationOutcome {
        summary: format!("TRASHED {}", plural_items(records.len())),
        action: CompletedAction::Trashed(records),
    })
}

fn run_delete(
    sources: Vec<PathBuf>,
    tx: &SyncSender<OperationEvent>,
) -> Result<OperationOutcome, String> {
    let total = measure_sources(&sources)?;
    send_started(tx, "PERMANENT DELETE", total);
    let mut tracker = ProgressTracker::new(tx, total);
    let mut deleted = Vec::new();

    for source in sources {
        remove_path_with_progress(&source, &mut tracker)
            .map_err(|error| format!("Delete failed for {}: {error}", source.display()))?;
        deleted.push(source);
    }

    Ok(OperationOutcome {
        summary: format!("PERMANENTLY DELETED {}", plural_items(deleted.len())),
        action: CompletedAction::Deleted(deleted),
    })
}

fn run_undo(
    action: UndoAction,
    tx: &SyncSender<OperationEvent>,
) -> Result<OperationOutcome, String> {
    match action {
        UndoAction::RemoveCreated { paths, .. } => {
            let total = measure_sources(&paths)?;
            send_started(tx, "UNDO REMOVE CREATED", total);
            let mut tracker = ProgressTracker::new(tx, total);
            for path in paths {
                remove_path_with_progress(&path, &mut tracker).map_err(|error| {
                    format!("Undo remove failed for {}: {error}", path.display())
                })?;
            }
        }
        UndoAction::MoveBack { moves, .. } => {
            let sources: Vec<PathBuf> =
                moves.iter().map(|file_move| file_move.to.clone()).collect();
            let total = measure_sources(&sources)?;
            send_started(tx, "UNDO MOVE", total);
            let mut tracker = ProgressTracker::new(tx, total);
            for file_move in moves {
                move_path(&file_move.to, &file_move.from, &mut tracker).map_err(|error| {
                    format!(
                        "Undo move failed for {} -> {}: {error}",
                        file_move.to.display(),
                        file_move.from.display()
                    )
                })?;
            }
        }
        UndoAction::RestoreTrash { records, .. } => {
            let sources: Vec<PathBuf> = records
                .iter()
                .map(|record| record.trashed_path.clone())
                .collect();
            let total = measure_sources(&sources)?;
            send_started(tx, "UNDO TRASH", total);
            let mut tracker = ProgressTracker::new(tx, total);
            for record in records {
                let destination = if record.original_path.exists() {
                    let parent = record
                        .original_path
                        .parent()
                        .unwrap_or_else(|| Path::new("/"));
                    let name = file_name_string(&record.original_path)?;
                    unique_child_path(parent, &name)
                } else {
                    record.original_path.clone()
                };
                if let Some(parent) = destination.parent() {
                    fs::create_dir_all(parent).map_err(|error| {
                        format!(
                            "Cannot recreate restore parent {}: {error}",
                            parent.display()
                        )
                    })?;
                }
                move_path(&record.trashed_path, &destination, &mut tracker).map_err(|error| {
                    format!(
                        "Undo trash failed for {} -> {}: {error}",
                        record.trashed_path.display(),
                        destination.display()
                    )
                })?;
                let _ = fs::remove_file(record.info_path);
            }
        }
        UndoAction::RenameBack { from, to } => {
            let total = measure_sources(std::slice::from_ref(&to))?;
            send_started(tx, "UNDO RENAME", total);
            let mut tracker = ProgressTracker::new(tx, total);
            move_path(&to, &from, &mut tracker).map_err(|error| {
                format!(
                    "Undo rename failed for {} -> {}: {error}",
                    to.display(),
                    from.display()
                )
            })?;
        }
    }

    Ok(OperationOutcome {
        summary: "UNDO APPLIED".to_string(),
        action: CompletedAction::UndoApplied,
    })
}

#[derive(Clone, Copy, Debug, Default)]
struct SourceMeasure {
    items: usize,
    bytes: u64,
}

fn measure_sources(sources: &[PathBuf]) -> Result<SourceMeasure, String> {
    validate_source_count(sources)?;
    let mut total = SourceMeasure::default();
    for source in sources {
        measure_path(source, &mut total, 0)?;
    }
    if total.items == 0 {
        total.items = sources.len().max(1);
    }
    Ok(total)
}

fn validate_source_count(sources: &[PathBuf]) -> Result<(), String> {
    if sources.len() > OPERATION_SOURCE_LIMIT {
        return Err(format!(
            "Operation source limit reached: {} selected, limit {}",
            sources.len(),
            OPERATION_SOURCE_LIMIT
        ));
    }
    Ok(())
}

fn measure_path(path: &Path, total: &mut SourceMeasure, depth: usize) -> Result<(), String> {
    if depth > OPERATION_DEPTH_LIMIT {
        return Err(format!(
            "Operation depth limit reached at {} (limit {})",
            path.display(),
            OPERATION_DEPTH_LIMIT
        ));
    }
    let Ok(metadata) = fs::symlink_metadata(path) else {
        return Ok(());
    };
    if total.items >= OPERATION_ITEM_LIMIT {
        return Err(format!(
            "Operation item limit reached at {} (limit {})",
            path.display(),
            OPERATION_ITEM_LIMIT
        ));
    }
    total.items = total.items.saturating_add(1);
    total.bytes = total.bytes.saturating_add(metadata.len());
    if metadata.file_type().is_dir() {
        let Ok(read_dir) = fs::read_dir(path) else {
            return Ok(());
        };
        for child in read_dir.flatten() {
            measure_path(&child.path(), total, depth + 1)?;
        }
    }
    Ok(())
}

struct ProgressTracker<'a> {
    tx: &'a SyncSender<OperationEvent>,
    done_items: usize,
    processed_items: usize,
    done_bytes: u64,
    total: SourceMeasure,
}

impl<'a> ProgressTracker<'a> {
    fn new(tx: &'a SyncSender<OperationEvent>, total: SourceMeasure) -> Self {
        Self {
            tx,
            done_items: 0,
            processed_items: 0,
            done_bytes: 0,
            total,
        }
    }

    fn bump_item(&mut self, bytes: u64) -> io::Result<()> {
        if self.processed_items >= OPERATION_ITEM_LIMIT {
            return Err(operation_limit_error(format!(
                "operation item limit reached at {} items",
                OPERATION_ITEM_LIMIT
            )));
        }
        self.processed_items = self.processed_items.saturating_add(1);
        self.done_items = self
            .done_items
            .saturating_add(1)
            .min(self.total.items.max(1));
        self.done_bytes = self.done_bytes.saturating_add(bytes).min(self.total.bytes);
        Ok(())
    }

    fn progress(&self, current: String, bytes: u64) {
        let _ = self.tx.send(OperationEvent::Progress(OperationProgress {
            done_items: self.done_items,
            total_items: self.total.items.max(1),
            done_bytes: self.done_bytes.saturating_add(bytes).min(self.total.bytes),
            total_bytes: self.total.bytes,
            current,
        }));
    }

    fn step(&mut self, path: &Path, bytes: u64) -> io::Result<()> {
        self.progress(path.display().to_string(), bytes);
        self.bump_item(bytes)
    }
}

fn send_started(tx: &SyncSender<OperationEvent>, title: &str, total: SourceMeasure) {
    let _ = tx.send(OperationEvent::Started {
        title: title.to_string(),
        total_items: total.items.max(1),
        total_bytes: total.bytes,
    });
}

fn copy_path(
    source: &Path,
    destination: &Path,
    tracker: &mut ProgressTracker<'_>,
) -> io::Result<()> {
    copy_path_at_depth(source, destination, tracker, 0)
}

fn copy_path_at_depth(
    source: &Path,
    destination: &Path,
    tracker: &mut ProgressTracker<'_>,
    depth: usize,
) -> io::Result<()> {
    if depth > OPERATION_DEPTH_LIMIT {
        return Err(operation_limit_error(format!(
            "operation depth limit reached at {} (limit {})",
            source.display(),
            OPERATION_DEPTH_LIMIT
        )));
    }
    let metadata = fs::symlink_metadata(source)?;
    if metadata.file_type().is_dir() {
        fs::create_dir_all(destination)?;
        tracker.step(destination, 0)?;
        for child in fs::read_dir(source)? {
            let child = child?;
            copy_path_at_depth(
                &child.path(),
                &destination.join(child.file_name()),
                tracker,
                depth + 1,
            )?;
        }
        return Ok(());
    }

    if metadata.file_type().is_symlink() {
        copy_symlink(source, destination, &metadata)?;
        tracker.step(destination, metadata.len())?;
        return Ok(());
    }

    if let Some(parent) = destination.parent() {
        fs::create_dir_all(parent)?;
    }
    tracker.progress(destination.display().to_string(), metadata.len());
    fs::copy(source, destination)?;
    let _ = fs::set_permissions(destination, metadata.permissions());
    tracker.bump_item(metadata.len())?;
    Ok(())
}

fn operation_limit_error(message: String) -> io::Error {
    io::Error::new(io::ErrorKind::InvalidInput, message)
}

#[cfg(unix)]
fn copy_symlink(source: &Path, destination: &Path, _metadata: &Metadata) -> io::Result<()> {
    use std::os::unix::fs::symlink;
    if let Some(parent) = destination.parent() {
        fs::create_dir_all(parent)?;
    }
    let target = fs::read_link(source)?;
    symlink(target, destination)
}

#[cfg(not(unix))]
fn copy_symlink(source: &Path, destination: &Path, _metadata: &Metadata) -> io::Result<()> {
    fs::copy(source, destination).map(|_| ())
}

fn move_path(
    source: &Path,
    destination: &Path,
    tracker: &mut ProgressTracker<'_>,
) -> io::Result<()> {
    if let Some(parent) = destination.parent() {
        fs::create_dir_all(parent)?;
    }
    tracker.progress(destination.display().to_string(), 0);
    match fs::rename(source, destination) {
        Ok(()) => {
            tracker.bump_item(0)?;
            Ok(())
        }
        Err(rename_error) => {
            copy_path(source, destination, tracker).map_err(|copy_error| {
                io::Error::new(
                    copy_error.kind(),
                    format!("rename failed: {rename_error}; fallback copy failed: {copy_error}"),
                )
            })?;
            remove_path(source)
        }
    }
}

fn remove_path(path: &Path) -> io::Result<()> {
    remove_path_bounded(path, |_| Ok(()))
}

fn remove_path_with_progress(path: &Path, tracker: &mut ProgressTracker<'_>) -> io::Result<()> {
    remove_path_bounded(path, |removed| {
        tracker.progress(removed.display().to_string(), 0);
        tracker.bump_item(0)
    })
}

enum RemoveFrame {
    Enter { path: PathBuf, depth: usize },
    Exit { path: PathBuf },
}

fn remove_path_bounded<F>(path: &Path, mut on_removed: F) -> io::Result<()>
where
    F: FnMut(&Path) -> io::Result<()>,
{
    let mut scheduled = 1usize;
    let mut stack = vec![RemoveFrame::Enter {
        path: path.to_path_buf(),
        depth: 0,
    }];

    while let Some(frame) = stack.pop() {
        match frame {
            RemoveFrame::Enter { path, depth } => {
                if depth > OPERATION_DEPTH_LIMIT {
                    return Err(operation_limit_error(format!(
                        "operation depth limit reached at {} (limit {})",
                        path.display(),
                        OPERATION_DEPTH_LIMIT
                    )));
                }
                let metadata = fs::symlink_metadata(&path)?;
                if metadata.file_type().is_dir() {
                    stack.push(RemoveFrame::Exit { path: path.clone() });
                    for child in fs::read_dir(&path)? {
                        let child = child?;
                        if scheduled >= OPERATION_ITEM_LIMIT {
                            return Err(operation_limit_error(format!(
                                "operation item limit reached at {} items",
                                OPERATION_ITEM_LIMIT
                            )));
                        }
                        scheduled = scheduled.saturating_add(1);
                        stack.push(RemoveFrame::Enter {
                            path: child.path(),
                            depth: depth + 1,
                        });
                    }
                } else {
                    fs::remove_file(&path)?;
                    on_removed(&path)?;
                }
            }
            RemoveFrame::Exit { path } => {
                fs::remove_dir(&path)?;
                on_removed(&path)?;
            }
        }
    }

    Ok(())
}

fn write_trash_info(info_path: &Path, original_path: &Path) -> Result<(), String> {
    if let Some(parent) = info_path.parent() {
        fs::create_dir_all(parent)
            .map_err(|error| format!("Cannot create trash info parent: {error}"))?;
    }
    let mut file = fs::File::create(info_path).map_err(|error| {
        format!(
            "Cannot write trash metadata {}: {error}",
            info_path.display()
        )
    })?;
    writeln!(file, "[Trash Info]").map_err(|error| error.to_string())?;
    writeln!(file, "Path={}", percent_encode_path(original_path))
        .map_err(|error| error.to_string())?;
    writeln!(file, "DeletionDate={}", trash_timestamp()).map_err(|error| error.to_string())?;
    Ok(())
}

fn file_name_string(path: &Path) -> Result<String, String> {
    path.file_name()
        .and_then(OsStr::to_str)
        .map(str::to_string)
        .ok_or_else(|| format!("{} has no valid file name", path.display()))
}

fn ensure_destination_is_not_inside_source(
    source: &Path,
    destination: &Path,
) -> Result<(), String> {
    if destination.starts_with(source) {
        return Err(format!(
            "Refusing to place {} inside selected source {}",
            destination.display(),
            source.display()
        ));
    }
    Ok(())
}

fn unique_child_path(parent: &Path, name: &str) -> PathBuf {
    let direct = parent.join(name);
    if !direct.exists() {
        return direct;
    }
    unique_copy_path(parent, name)
}

fn unique_copy_path(parent: &Path, name: &str) -> PathBuf {
    let (stem, extension) = split_extension(name);
    for index in 1..10_000 {
        let candidate_name = if index == 1 {
            match extension {
                Some(extension) => format!("{stem} copy.{extension}"),
                None => format!("{stem} copy"),
            }
        } else {
            match extension {
                Some(extension) => format!("{stem} copy {index}.{extension}"),
                None => format!("{stem} copy {index}"),
            }
        };
        let candidate = parent.join(candidate_name);
        if !candidate.exists() {
            return candidate;
        }
    }
    parent.join(format!("{name} copy {}", process_suffix()))
}

fn split_extension(name: &str) -> (&str, Option<&str>) {
    if let Some(index) = name.rfind('.') {
        if index > 0 && index < name.len() - 1 {
            return (&name[..index], Some(&name[index + 1..]));
        }
    }
    (name, None)
}

fn process_suffix() -> u128 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_millis())
        .unwrap_or_default()
}

fn plural_items(count: usize) -> String {
    if count == 1 {
        "1 ITEM".to_string()
    } else {
        format!("{count} ITEMS")
    }
}

fn percent_encode_path(path: &Path) -> String {
    let text = path.to_string_lossy();
    let mut encoded = String::new();
    for byte in text.as_bytes() {
        let is_safe =
            byte.is_ascii_alphanumeric() || matches!(*byte, b'/' | b'.' | b'-' | b'_' | b'~');
        if is_safe {
            encoded.push(*byte as char);
        } else {
            encoded.push_str(&format!("%{byte:02X}"));
        }
    }
    encoded
}

fn trash_timestamp() -> String {
    let seconds = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_secs())
        .unwrap_or_default();
    let days = (seconds / 86_400) as i64;
    let seconds_of_day = seconds % 86_400;
    let (year, month, day) = civil_from_days(days);
    let hour = seconds_of_day / 3_600;
    let minute = (seconds_of_day % 3_600) / 60;
    let second = seconds_of_day % 60;
    format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}")
}

fn civil_from_days(days_since_epoch: i64) -> (i64, u32, u32) {
    let z = days_since_epoch + 719_468;
    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
    let doe = z - era * 146_097;
    let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
    let y = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let day = doy - (153 * mp + 2) / 5 + 1;
    let month = mp + if mp < 10 { 3 } else { -9 };
    let year = y + if month <= 2 { 1 } else { 0 };
    (year, month as u32, day as u32)
}

pub fn operation_percent(done_items: usize, total_items: usize) -> f32 {
    if total_items == 0 {
        0.0
    } else {
        done_items as f32 / total_items as f32
    }
}

pub fn operation_detail(progress: &OperationProgress) -> String {
    let byte_detail = if progress.total_bytes > 0 {
        format!(
            " // {} / {}",
            human_size(progress.done_bytes),
            human_size(progress.total_bytes)
        )
    } else {
        String::new()
    };
    format!(
        "{} / {} ITEMS{byte_detail} // {}",
        progress.done_items, progress.total_items, progress.current
    )
}

pub fn path_is_inside_home(path: &Path) -> bool {
    path.starts_with(home_dir())
}

#[cfg(test)]
mod tests {
    use super::*;

    struct TestDir(PathBuf);

    impl TestDir {
        fn new(label: &str) -> Self {
            let path = env::temp_dir().join(format!(
                "guth-ops-{label}-{}-{}",
                std::process::id(),
                process_suffix()
            ));
            fs::create_dir_all(&path).unwrap();
            Self(path)
        }

        fn path(&self) -> &Path {
            &self.0
        }
    }

    impl Drop for TestDir {
        fn drop(&mut self) {
            let _ = fs::remove_dir_all(&self.0);
        }
    }

    #[test]
    fn unique_copy_names_keep_extensions() {
        let parent = env::temp_dir().join(format!("guth-test-{}", process_suffix()));
        fs::create_dir_all(&parent).unwrap();
        fs::write(parent.join("alpha.txt"), b"x").unwrap();
        assert_eq!(
            unique_child_path(&parent, "alpha.txt"),
            parent.join("alpha copy.txt")
        );
        let _ = fs::remove_dir_all(parent);
    }

    #[test]
    fn trash_timestamp_has_iso_shape() {
        let stamp = trash_timestamp();
        assert_eq!(stamp.len(), 19);
        assert_eq!(&stamp[10..11], "T");
    }

    #[test]
    fn measure_sources_rejects_source_limit() {
        let sources: Vec<PathBuf> = (0..=OPERATION_SOURCE_LIMIT)
            .map(|index| PathBuf::from(format!("missing-{index}")))
            .collect();

        let error = measure_sources(&sources).unwrap_err();
        assert!(error.contains("source limit"));
    }

    #[test]
    fn measure_sources_rejects_depth_limit() {
        let root = TestDir::new("depth-limit");
        let mut current = root.path().to_path_buf();
        for index in 0..=OPERATION_DEPTH_LIMIT {
            current = current.join(format!("d{index}"));
            fs::create_dir(&current).unwrap();
        }

        let error = measure_sources(&[root.path().to_path_buf()]).unwrap_err();
        assert!(error.contains("depth limit"));
    }

    #[test]
    fn progress_tracker_rejects_item_overflow() {
        let (tx, _rx) = mpsc::sync_channel(1);
        let mut tracker = ProgressTracker::new(
            &tx,
            SourceMeasure {
                items: OPERATION_ITEM_LIMIT,
                bytes: 0,
            },
        );
        tracker.processed_items = OPERATION_ITEM_LIMIT;

        assert!(tracker.bump_item(0).is_err());
    }

    #[test]
    fn bounded_remove_deletes_nested_tree() {
        let root = TestDir::new("bounded-remove");
        let nested = root.path().join("a").join("b");
        fs::create_dir_all(&nested).unwrap();
        fs::write(nested.join("file.txt"), b"x").unwrap();

        let mut removed = 0usize;
        remove_path_bounded(root.path(), |_| {
            removed += 1;
            Ok(())
        })
        .unwrap();

        assert_eq!(removed, 4);
        assert!(!root.path().exists());
    }

    #[test]
    fn bounded_remove_rejects_depth_limit() {
        let root = TestDir::new("remove-depth-limit");
        let mut current = root.path().to_path_buf();
        for index in 0..=OPERATION_DEPTH_LIMIT {
            current = current.join(format!("d{index}"));
            fs::create_dir(&current).unwrap();
        }

        let error = remove_path(root.path()).unwrap_err();
        assert!(error.to_string().contains("depth limit"));
        assert!(root.path().exists());
    }

    #[test]
    fn child_reaper_permits_are_bounded() {
        let initial = active_child_reapers();
        let available = CHILD_REAPER_LIMIT.saturating_sub(initial);
        let mut permits = Vec::new();
        for _ in 0..available {
            permits.push(ChildReaperPermit::try_acquire().unwrap());
        }

        assert!(ChildReaperPermit::try_acquire().is_err());
        drop(permits);
        assert_eq!(active_child_reapers(), initial);
    }
}
