//! Generic directory walker over the [`Filesystem`] port.
//!
//! Used by [`crate::projects::expand`] to walk the subtree rooted
//! at a glob pattern's literal prefix and emit candidate
//! directories for matching against the compiled glob.
//!
//! The walker enforces `DISC-020` (follow symlinks via
//! canonicalisation), `DISC-021` (cycle detection via the visited
//! set), and `DISC-022` (workspace boundary). Diagnostic
//! attribution (which source pattern triggered an error) is the
//! caller's concern; the walker emits events keyed only on
//! canonical paths.

use std::collections::HashSet;
use std::path::{Path, PathBuf};

use haz_vfs::{EntryKind, Filesystem, FsError};

use crate::discovery::Discovery;
use crate::path::ResolveError;

/// Event emitted to the walker's sink.
pub(crate) enum WalkEvent<F: Filesystem> {
    /// A canonicalised, in-boundary directory the caller should
    /// test against its glob.
    Directory {
        /// Host-canonical path of the directory.
        canonical: F::CanonicalPath,
    },
    /// A canonicalisation step produced a path outside the
    /// workspace boundary (`DISC-022`).
    BoundaryEscape {
        /// Canonical target of the escape.
        canonical: PathBuf,
        /// Underlying boundary error.
        source: ResolveError,
    },
    /// A canonical path was about to be revisited (`DISC-021`).
    Cycle {
        /// Canonical path that would have been revisited.
        revisited: PathBuf,
    },
    /// A non-boundary filesystem error.
    Io {
        /// Host path that was being probed.
        target: PathBuf,
        /// Underlying filesystem error.
        source: FsError,
    },
}

impl<F: Filesystem> Discovery<'_, F> {
    /// Walk the directory tree rooted at `start_host`, emitting one
    /// [`WalkEvent::Directory`] per in-boundary canonical
    /// directory visited.
    ///
    /// `depth_remaining` bounds descent past the starting
    /// directory. `Some(0)` means "emit the starting directory
    /// only, do not recurse". `None` means unbounded; termination
    /// is guaranteed by the visited set.
    ///
    /// If `start_host` does not exist, the walk emits no events
    /// (a missing literal prefix is not by itself an error; the
    /// caller surfaces it as zero matches per `DISC-006`).
    pub(crate) fn walk_dirs<Sink>(
        &self,
        workspace_canonical: &F::CanonicalPath,
        start_host: &Path,
        depth_remaining: Option<usize>,
        visited: &mut HashSet<F::CanonicalPath>,
        sink: &mut Sink,
    ) where
        Sink: FnMut(WalkEvent<F>),
    {
        let canonical = match self.verify_within(start_host, workspace_canonical) {
            Ok(c) => c,
            Err(ResolveError::NotFound { .. }) => return,
            Err(err) => {
                emit_resolve_error(start_host, err, sink);
                return;
            }
        };

        if visited.contains(&canonical) {
            sink(WalkEvent::Cycle {
                revisited: canonical.as_ref().to_path_buf(),
            });
            return;
        }

        let meta = match self.fs().metadata(canonical.as_ref()) {
            Ok(m) => m,
            Err(FsError::NotFound { .. }) => return,
            Err(source) => {
                sink(WalkEvent::Io {
                    target: canonical.as_ref().to_path_buf(),
                    source,
                });
                return;
            }
        };
        if meta.kind != EntryKind::Dir {
            return;
        }

        visited.insert(canonical.clone());
        sink(WalkEvent::Directory {
            canonical: canonical.clone(),
        });

        let next_remaining = match depth_remaining {
            Some(0) => return,
            Some(n) => Some(n - 1),
            None => None,
        };

        let entries = match self.fs().read_dir(canonical.as_ref()) {
            Ok(e) => e,
            Err(source) => {
                sink(WalkEvent::Io {
                    target: canonical.as_ref().to_path_buf(),
                    source,
                });
                return;
            }
        };

        for entry in entries {
            if matches!(entry.metadata.kind, EntryKind::Dir | EntryKind::Symlink) {
                self.walk_dirs(
                    workspace_canonical,
                    &entry.path,
                    next_remaining,
                    visited,
                    sink,
                );
            }
        }
    }
}

fn emit_resolve_error<F: Filesystem, Sink>(start_host: &Path, err: ResolveError, sink: &mut Sink)
where
    Sink: FnMut(WalkEvent<F>),
{
    match err {
        ResolveError::OutsideBoundary { ref canonical, .. } => {
            let canonical = canonical.clone();
            sink(WalkEvent::BoundaryEscape {
                canonical,
                source: err,
            });
        }
        ResolveError::CanonicalizeFailed { path, source } => {
            sink(WalkEvent::Io {
                target: path,
                source,
            });
        }
        ResolveError::NotFound { .. } => {
            // Already filtered upstream; defensive fall-through.
        }
        ResolveError::RelativeTarget { path } => {
            sink(WalkEvent::Io {
                target: path,
                source: FsError::Io {
                    path: start_host.to_path_buf(),
                    source: std::io::Error::other("walk start was relative"),
                },
            });
        }
    }
}
