//! Shared utilities for CLI commands

use diaryx_core::YamlValue;
use diaryx_core::config::Config;
use diaryx_core::entry::prettify_filename;
use diaryx_core::fs::{RealFileSystem, SyncToAsyncFs};
use diaryx_core::link_parser::{self, LinkFormat};
use diaryx_core::workspace::Workspace;
use glob::glob;
use std::io::{self, Write};
use std::path::{Path, PathBuf};

use crate::cli::{CliDiaryxAppSync, CliWorkspace, block_on};

/// Helper for formatting links according to workspace configuration.
///
/// This struct caches the workspace root and link format to avoid repeated lookups
/// when formatting multiple links.
#[allow(dead_code)]
pub struct LinkFormatter {
    /// The workspace root directory (parent of root index file)
    workspace_root: PathBuf,
    /// The configured link format
    link_format: LinkFormat,
}

#[allow(dead_code)]
impl LinkFormatter {
    /// Create a new LinkFormatter by detecting workspace settings from a path.
    ///
    /// The path can be any file within the workspace - we'll walk up to find the root.
    pub fn from_path(ws: &CliWorkspace, path: &Path) -> Option<Self> {
        // Try to detect the workspace root from the given path
        let workspace_root_index = block_on(ws.detect_workspace(path)).ok()??;
        let workspace_root = workspace_root_index.parent()?.to_path_buf();

        // Get the link format from workspace config
        let config = block_on(ws.get_workspace_config(&workspace_root_index)).ok()?;

        Some(Self {
            workspace_root,
            link_format: config.link_format,
        })
    }

    /// Create a new LinkFormatter with explicit workspace root and format.
    pub fn new(workspace_root: PathBuf, link_format: LinkFormat) -> Self {
        Self {
            workspace_root,
            link_format,
        }
    }

    /// Get the workspace root directory.
    pub fn workspace_root(&self) -> &Path {
        &self.workspace_root
    }

    /// Get the configured link format.
    pub fn link_format(&self) -> LinkFormat {
        self.link_format
    }

    /// Convert an absolute path to a canonical (workspace-relative) path.
    ///
    /// Returns None if the path is not within the workspace.
    pub fn to_canonical(&self, path: &Path) -> Option<String> {
        // Canonicalize both paths for accurate comparison
        let path_canonical = path.canonicalize().ok()?;
        let root_canonical = self.workspace_root.canonicalize().ok()?;

        // Get the relative path from workspace root
        let relative = pathdiff::diff_paths(&path_canonical, &root_canonical)?;

        // Ensure it doesn't escape the workspace (no leading ..)
        if relative.starts_with("..") {
            return None;
        }

        Some(relative.to_string_lossy().to_string())
    }

    /// Format a link from one file to another according to workspace settings.
    ///
    /// # Arguments
    /// * `from_path` - Absolute path of the file containing the link
    /// * `to_path` - Absolute path of the target file
    /// * `title` - Optional title for the link (uses filename if None)
    ///
    /// # Returns
    /// The formatted link string, or None if paths can't be resolved
    pub fn format_link(
        &self,
        from_path: &Path,
        to_path: &Path,
        title: Option<&str>,
    ) -> Option<String> {
        let from_canonical = self.to_canonical(from_path)?;
        let to_canonical = self.to_canonical(to_path)?;

        // Use provided title or derive from filename
        let title = title.map(|s| s.to_string()).unwrap_or_else(|| {
            to_path
                .file_stem()
                .and_then(|s| s.to_str())
                .map(prettify_filename)
                .unwrap_or_else(|| to_canonical.clone())
        });

        Some(link_parser::format_link_with_format(
            &to_canonical,
            &title,
            self.link_format,
            &from_canonical,
        ))
    }

    /// Format a link for a contents entry (from parent index to child).
    pub fn format_contents_link(
        &self,
        parent_path: &Path,
        child_path: &Path,
        title: Option<&str>,
    ) -> Option<String> {
        self.format_link(parent_path, child_path, title)
    }

    /// Format a link for a part_of entry (from child to parent index).
    pub fn format_part_of_link(
        &self,
        child_path: &Path,
        parent_path: &Path,
        title: Option<&str>,
    ) -> Option<String> {
        self.format_link(child_path, parent_path, title)
    }
}

/// Format a link using workspace configuration, with fallback to relative path.
///
/// This is a convenience function that tries to use LinkFormatter, falling back
/// to a simple relative path if workspace detection fails.
pub fn format_workspace_link(
    ws: &CliWorkspace,
    from_path: &Path,
    to_path: &Path,
    title: Option<&str>,
) -> String {
    if let Some(formatter) = LinkFormatter::from_path(ws, from_path)
        && let Some(link) = formatter.format_link(from_path, to_path, title)
    {
        return link;
    }

    // Fallback to simple relative path
    calculate_relative_path(from_path, to_path)
}

/// Resolve a frontmatter reference to canonical workspace-relative path.
pub fn canonicalize_frontmatter_reference(
    raw_value: &str,
    current_file_path: &Path,
    link_format_hint: Option<LinkFormat>,
) -> String {
    let parsed = link_parser::parse_link(raw_value);
    link_parser::to_canonical_with_link_format(&parsed, current_file_path, link_format_hint)
}

fn workspace_relative_path(workspace_root: &Path, path: &Path) -> Option<String> {
    let root_canonical = workspace_root.canonicalize().ok()?;
    let absolute = if path.is_absolute() {
        path.to_path_buf()
    } else {
        std::env::current_dir().ok()?.join(path)
    };
    let normalized_absolute = if absolute.exists() {
        absolute.canonicalize().ok()?
    } else {
        let parent = absolute.parent()?.canonicalize().ok()?;
        parent.join(absolute.file_name()?)
    };
    let relative = pathdiff::diff_paths(normalized_absolute, root_canonical)?;
    if relative.starts_with("..") {
        return None;
    }
    Some(
        relative
            .iter()
            .map(|segment| segment.to_string_lossy())
            .collect::<Vec<_>>()
            .join("/"),
    )
}

/// Result of a workspace-aware file rename operation
pub struct RenameResult {
    /// Whether the file was successfully moved/renamed
    pub success: bool,
    /// Parent index that was updated (if any)
    pub parent_updated: Option<PathBuf>,
    /// Children whose part_of was updated
    pub children_updated: Vec<PathBuf>,
}

/// Rename/move a file while updating all workspace references (contents and part_of)
/// This is the canonical way to rename files in the workspace - use this instead of std::fs::rename
///
/// Updates:
/// - Parent's `contents` list (if file has `part_of`)
/// - Children's `part_of` property (if file has `contents`)
/// - The file's own `part_of` (if moving to different directory)
///
/// If `ws` is provided, links will be formatted according to the workspace's configured link format.
/// Otherwise, plain relative paths will be used.
pub fn rename_file_with_refs(
    app: &CliDiaryxAppSync,
    ws: Option<&CliWorkspace>,
    source_path: &Path,
    dest_path: &Path,
    dry_run: bool,
) -> RenameResult {
    let mut result = RenameResult {
        success: false,
        parent_updated: None,
        children_updated: Vec::new(),
    };

    let source_str = source_path.to_string_lossy();
    let formatter = ws.and_then(|workspace| LinkFormatter::from_path(workspace, source_path));
    let link_format_hint = formatter.as_ref().map(|f| f.link_format());
    let source_workspace_relative = formatter
        .as_ref()
        .and_then(|f| workspace_relative_path(f.workspace_root(), source_path));

    // Read source file's frontmatter to find its part_of (parent)
    let parent_path = match app.get_frontmatter_property(&source_str, "part_of") {
        Ok(Some(YamlValue::String(part_of))) => {
            if let (Some(formatter), Some(source_rel)) =
                (formatter.as_ref(), source_workspace_relative.as_ref())
            {
                let parent_canonical = canonicalize_frontmatter_reference(
                    &part_of,
                    Path::new(source_rel),
                    link_format_hint,
                );
                Some(formatter.workspace_root().join(parent_canonical))
            } else {
                let parsed = link_parser::parse_link(&part_of);
                match parsed.path_type {
                    link_parser::PathType::WorkspaceRoot => {
                        Some(PathBuf::from(format!("/{}", parsed.path)))
                    }
                    link_parser::PathType::Relative | link_parser::PathType::Ambiguous => {
                        source_path.parent().map(|dir| dir.join(parsed.path))
                    }
                }
            }
        }
        _ => None,
    };

    // Canonicalize paths for accurate relative path calculations
    let source_canonical = source_path
        .canonicalize()
        .unwrap_or_else(|_| source_path.to_path_buf());

    // Calculate old and new relative paths from parent's perspective
    let (old_relative, new_relative) = if let Some(ref parent) = parent_path {
        let parent_canonical = parent.canonicalize().unwrap_or_else(|_| parent.clone());
        let parent_dir = parent_canonical.parent().unwrap_or(&parent_canonical);

        let old_rel = pathdiff::diff_paths(&source_canonical, parent_dir)
            .map(|p| p.to_string_lossy().to_string())
            .unwrap_or_else(|| {
                source_path
                    .file_name()
                    .map(|f| f.to_string_lossy().to_string())
                    .unwrap_or_default()
            });

        // For new path, we need to handle the case where dest doesn't exist yet
        // Use dest's parent (which should exist) to calculate relative path
        let new_rel = if let Some(dest_parent) = dest_path.parent() {
            let dest_parent_canonical = dest_parent
                .canonicalize()
                .unwrap_or_else(|_| dest_parent.to_path_buf());
            let dest_canonical =
                dest_parent_canonical.join(dest_path.file_name().unwrap_or_default());
            pathdiff::diff_paths(&dest_canonical, parent_dir)
                .map(|p| p.to_string_lossy().to_string())
                .unwrap_or_else(|| {
                    dest_path
                        .file_name()
                        .map(|f| f.to_string_lossy().to_string())
                        .unwrap_or_default()
                })
        } else {
            dest_path
                .file_name()
                .map(|f| f.to_string_lossy().to_string())
                .unwrap_or_default()
        };

        (old_rel, new_rel)
    } else {
        // No parent, just use filenames
        let old_rel = source_path
            .file_name()
            .map(|f| f.to_string_lossy().to_string())
            .unwrap_or_default();
        let new_rel = dest_path
            .file_name()
            .map(|f| f.to_string_lossy().to_string())
            .unwrap_or_default();
        (old_rel, new_rel)
    };

    // Get children before moving (need to update their part_of after move)
    let children: Vec<PathBuf> = match app.get_frontmatter_property(&source_str, "contents") {
        Ok(Some(YamlValue::Sequence(contents))) => contents
            .iter()
            .filter_map(|v| {
                if let YamlValue::String(s) = v {
                    if let (Some(formatter), Some(source_rel)) =
                        (formatter.as_ref(), source_workspace_relative.as_ref())
                    {
                        let child_canonical = canonicalize_frontmatter_reference(
                            s,
                            Path::new(source_rel),
                            link_format_hint,
                        );
                        Some(formatter.workspace_root().join(child_canonical))
                    } else {
                        let parsed = link_parser::parse_link(s);
                        match parsed.path_type {
                            link_parser::PathType::WorkspaceRoot => {
                                Some(PathBuf::from(format!("/{}", parsed.path)))
                            }
                            link_parser::PathType::Relative | link_parser::PathType::Ambiguous => {
                                source_path.parent().map(|dir| dir.join(parsed.path))
                            }
                        }
                    }
                } else {
                    None
                }
            })
            .filter(|p| p.exists())
            .collect(),
        _ => Vec::new(),
    };

    if dry_run {
        println!(
            "Would move '{}' to '{}'",
            source_path.display(),
            dest_path.display()
        );
        if let Some(ref parent) = parent_path
            && parent.exists()
        {
            println!(
                "Would update contents in '{}': '{}' -> '{}'",
                parent.display(),
                old_relative,
                new_relative
            );
        }
        if !children.is_empty() {
            println!("Would update part_of in {} child file(s)", children.len());
        }
        result.success = true;
        return result;
    }

    // 1. Update parent's contents (if parent exists)
    if let Some(ref parent) = parent_path
        && parent.exists()
    {
        let parent_str = parent.to_string_lossy();
        if let Ok(Some(YamlValue::Sequence(mut items))) =
            app.get_frontmatter_property(&parent_str, "contents")
        {
            let parent_workspace_relative = formatter
                .as_ref()
                .and_then(|f| workspace_relative_path(f.workspace_root(), parent));
            let source_canonical = formatter
                .as_ref()
                .and_then(|f| workspace_relative_path(f.workspace_root(), source_path));
            let dest_canonical = formatter
                .as_ref()
                .and_then(|f| workspace_relative_path(f.workspace_root(), dest_path));

            let mut updated = false;
            for item in &mut items {
                if let YamlValue::String(s) = item {
                    let mut matches_old = *s == old_relative;
                    let mut replacement = new_relative.clone();

                    if let (Some(source_canonical), Some(parent_relative)) = (
                        source_canonical.as_ref(),
                        parent_workspace_relative.as_ref(),
                    ) {
                        let existing_canonical = canonicalize_frontmatter_reference(
                            s,
                            Path::new(parent_relative),
                            link_format_hint,
                        );
                        matches_old = existing_canonical == *source_canonical;

                        if matches_old
                            && let (Some(formatter), Some(dest_canonical)) =
                                (formatter.as_ref(), dest_canonical.as_ref())
                        {
                            let parsed = link_parser::parse_link(s);
                            let title = parsed
                                .title
                                .as_deref()
                                .map(str::to_string)
                                .unwrap_or_else(|| link_parser::path_to_title(dest_canonical));
                            replacement = link_parser::format_link_with_format(
                                dest_canonical,
                                &title,
                                formatter.link_format(),
                                parent_relative,
                            );
                        }
                    }

                    if matches_old {
                        *s = replacement;
                        updated = true;
                    }
                }
            }
            if updated {
                if let Err(e) = app.set_frontmatter_property(
                    &parent_str,
                    "contents",
                    YamlValue::Sequence(items),
                ) {
                    eprintln!("⚠ Error updating parent contents: {}", e);
                } else {
                    println!(
                        "✓ Updated contents in '{}': '{}' -> '{}'",
                        parent.display(),
                        old_relative,
                        new_relative
                    );
                    result.parent_updated = Some(parent.clone());
                }
            }
        }
    }

    // 2. Create destination directory if needed
    if let Some(parent_dir) = dest_path.parent()
        && !parent_dir.exists()
        && let Err(e) = std::fs::create_dir_all(parent_dir)
    {
        eprintln!("✗ Error creating directory: {}", e);
        return result;
    }

    // 3. Move/rename the file
    if let Err(e) = std::fs::rename(source_path, dest_path) {
        eprintln!("✗ Error moving file: {}", e);
        return result;
    }
    println!(
        "✓ Moved '{}' to '{}'",
        source_path.display(),
        dest_path.display()
    );
    result.success = true;

    // 4. Update part_of in the moved file if parent exists and relative path changed
    if let Some(ref parent) = parent_path {
        let dest_str = dest_path.to_string_lossy();
        let new_part_of = if let Some(ws) = ws {
            format_workspace_link(ws, dest_path, parent, None)
        } else {
            calculate_relative_path(dest_path, parent)
        };
        if let Err(e) =
            app.set_frontmatter_property(&dest_str, "part_of", YamlValue::String(new_part_of))
        {
            eprintln!("⚠ Error updating part_of in moved file: {}", e);
        }
    }

    // 5. Update children's part_of to point to new location
    for child in &children {
        let child_str = child.to_string_lossy();
        let new_part_of = if let Some(ws) = ws {
            format_workspace_link(ws, child, dest_path, None)
        } else {
            calculate_relative_path(child, dest_path)
        };
        if let Err(e) =
            app.set_frontmatter_property(&child_str, "part_of", YamlValue::String(new_part_of))
        {
            eprintln!("⚠ Error updating part_of in '{}': {}", child.display(), e);
        } else {
            println!("✓ Updated part_of in '{}'", child.display());
            result.children_updated.push(child.clone());
        }
    }

    result
}

/// Calculate the relative path from one file to another
pub fn calculate_relative_path(from: &Path, to: &Path) -> String {
    // Try to canonicalize both paths for accurate comparison
    let from_canonical = from.canonicalize().ok();
    let to_canonical = to.canonicalize().ok();

    // If they're in the same directory, just use the filename
    let same_dir = match (&from_canonical, &to_canonical) {
        (Some(fc), Some(tc)) => fc.parent() == tc.parent(),
        _ => from.parent() == to.parent(),
    };

    if same_dir {
        return to
            .file_name()
            .map(|f| f.to_string_lossy().to_string())
            .unwrap_or_else(|| to.to_string_lossy().to_string());
    }

    // Use canonical paths for pathdiff calculation
    if let (Some(from_canon), Some(to_canon)) = (&from_canonical, &to_canonical)
        && let Some(from_dir) = from_canon.parent()
        && let Some(rel) = pathdiff::diff_paths(to_canon, from_dir)
    {
        return rel.to_string_lossy().to_string();
    }

    // Fall back to non-canonical paths
    if let Some(from_dir) = from.parent()
        && let Some(rel) = pathdiff::diff_paths(to, from_dir)
    {
        let rel_str = rel.to_string_lossy().to_string();
        // Ensure we don't return an absolute path
        if !rel.is_absolute() {
            return rel_str;
        }
    }

    // Fallback: just use the filename
    to.file_name()
        .map(|f| f.to_string_lossy().to_string())
        .unwrap_or_else(|| to.to_string_lossy().to_string())
}

/// Result of a confirmation prompt
pub enum ConfirmResult {
    Yes,
    No,
    All,
    Quit,
}

/// Prompt user for confirmation
pub fn prompt_confirm(message: &str) -> ConfirmResult {
    print!("{} [y/n/a/q] ", message);
    io::stdout().flush().unwrap();

    let mut input = String::new();
    if io::stdin().read_line(&mut input).is_err() {
        return ConfirmResult::Quit;
    }

    match input.trim().to_lowercase().as_str() {
        "y" | "yes" => ConfirmResult::Yes,
        "n" | "no" => ConfirmResult::No,
        "a" | "all" => ConfirmResult::All,
        "q" | "quit" => ConfirmResult::Quit,
        _ => ConfirmResult::No,
    }
}

/// Check if a path pattern contains glob characters
pub fn is_glob_pattern(path: &str) -> bool {
    path.contains('*') || path.contains('?') || path.contains('[')
}

/// Resolve a path pattern to a list of files
/// Returns either a single resolved path (for dates/literals) or multiple paths (for globs/workspace)
///
/// Special handling:
/// - `.` resolves to all files in the current workspace (traversing from local index)
/// - `title:...` or `t:...` resolves files by their frontmatter title property (fuzzy matching)
/// - Glob patterns (`*.md`, `**/*.md`) match files by pattern
/// - Literal paths are returned as-is
pub fn resolve_paths(path: &str, _config: &Config, _app: &CliDiaryxAppSync) -> Vec<PathBuf> {
    // Check for title: or t: prefix
    if let Some(title_query) = path
        .strip_prefix("title:")
        .or_else(|| path.strip_prefix("t:"))
    {
        return match_files_by_title(title_query);
    }

    // Handle directories as workspace-aware path resolution
    let path_buf = Path::new(path);
    if path_buf.is_dir() {
        return resolve_workspace_files_in_dir(path_buf);
    }

    // Check if it's a glob pattern
    if is_glob_pattern(path) {
        match glob(path) {
            Ok(paths) => {
                let mut result: Vec<PathBuf> = paths
                    .filter_map(|p| p.ok())
                    .filter(|p| p.extension().is_some_and(|ext| ext == "md"))
                    .collect();
                result.sort();
                result
            }
            Err(e) => {
                eprintln!("✗ Invalid glob pattern: {}", e);
                vec![]
            }
        }
    } else {
        let resolved = PathBuf::from(path);

        // If path doesn't exist and doesn't look hierarchical, try fuzzy matching
        if !resolved.exists()
            && !path.contains('/')
            && !path.contains('\\')
            && let Some(matches) = fuzzy_match_files(path)
            && !matches.is_empty()
        {
            return matches;
        }

        // Fall back to the resolved path (may not exist, but that's the user's intent)
        vec![resolved]
    }
}

/// Resolve a directory to all files in its workspace
/// Finds the local index in the directory and traverses its contents
fn resolve_workspace_files_in_dir(dir: &Path) -> Vec<PathBuf> {
    let fs = SyncToAsyncFs::new(RealFileSystem);
    let workspace = Workspace::new(fs);

    // Canonicalize the directory path
    let dir = match dir.canonicalize() {
        Ok(d) => d,
        Err(e) => {
            eprintln!("✗ Could not resolve directory '{}': {}", dir.display(), e);
            return vec![];
        }
    };

    // Find a local index in the directory
    match block_on(workspace.find_any_index_in_dir(&dir)) {
        Ok(Some(index_path)) => {
            // Collect all files from the index
            match block_on(workspace.collect_workspace_files(&index_path)) {
                Ok(files) => files,
                Err(e) => {
                    eprintln!("✗ Error traversing workspace: {}", e);
                    vec![]
                }
            }
        }
        Ok(None) => {
            // No index found, fall back to all .md files in the directory
            let glob_pattern = format!("{}/*.md", dir.display());
            match glob(&glob_pattern) {
                Ok(paths) => {
                    let mut result: Vec<PathBuf> = paths.filter_map(|p| p.ok()).collect();
                    result.sort();
                    if result.is_empty() {
                        eprintln!(
                            "⚠ No index file found in '{}' and no .md files present",
                            dir.display()
                        );
                    }
                    result
                }
                Err(e) => {
                    eprintln!("✗ Error listing files: {}", e);
                    vec![]
                }
            }
        }
        Err(e) => {
            eprintln!("✗ Error searching for index: {}", e);
            vec![]
        }
    }
}

/// Match files by their frontmatter title property
/// Uses fuzzy matching: exact (case-insensitive) → prefix → contains
fn match_files_by_title(query: &str) -> Vec<PathBuf> {
    let current_dir = match std::env::current_dir() {
        Ok(d) => d,
        Err(_) => return vec![],
    };

    let query_lower = query.to_lowercase();
    let mut matches: Vec<(PathBuf, usize)> = Vec::new();

    // Scan all .md files in current directory
    if let Ok(entries) = std::fs::read_dir(&current_dir) {
        for entry in entries.filter_map(|e| e.ok()) {
            let path = entry.path();

            // Only consider .md files
            if path.extension().is_none_or(|ext| ext != "md") {
                continue;
            }

            // Read the file and extract title from frontmatter
            if let Some(title) = extract_title_from_file(&path) {
                let title_lower = title.to_lowercase();

                // Score the match:
                // - Exact match: highest priority (score 0)
                // - Prefix match: high priority (score 1)
                // - Contains match: lower priority (score 2)
                let score = if title_lower == query_lower {
                    Some(0)
                } else if title_lower.starts_with(&query_lower) {
                    Some(1)
                } else if title_lower.contains(&query_lower) {
                    Some(2)
                } else {
                    None
                };

                if let Some(s) = score {
                    matches.push((path, s));
                }
            }
        }
    }

    if matches.is_empty() {
        return vec![];
    }

    // Sort by score (best first), then by path name
    matches.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));

    // Return all matches with the best score
    let best_score = matches[0].1;
    matches
        .into_iter()
        .filter(|(_, score)| *score == best_score)
        .map(|(path, _)| path)
        .collect()
}

/// Extract the title property from a file's frontmatter
fn extract_title_from_file(path: &Path) -> Option<String> {
    let content = std::fs::read_to_string(path).ok()?;

    // Check if file starts with frontmatter
    if !content.starts_with("---") {
        return None;
    }

    // Find the end of frontmatter
    let rest = &content[3..];
    let end_idx = rest.find("---")?;
    let frontmatter_str = &rest[..end_idx];

    // Parse YAML
    let frontmatter: YamlValue = serde_yaml::from_str(frontmatter_str).ok()?;

    // Extract title
    if let YamlValue::Mapping(map) = frontmatter
        && let Some(YamlValue::String(title)) = map.get("title")
    {
        return Some(title.clone());
    }

    None
}

/// Fuzzy match a string against .md files in the current directory
/// Returns files where the filename (without extension) contains the query (case-insensitive)
/// or where the query is a prefix of the filename
fn fuzzy_match_files(query: &str) -> Option<Vec<PathBuf>> {
    let current_dir = std::env::current_dir().ok()?;
    let query_lower = query.to_lowercase();

    let mut matches: Vec<(PathBuf, usize)> = Vec::new();

    if let Ok(entries) = std::fs::read_dir(&current_dir) {
        for entry in entries.filter_map(|e| e.ok()) {
            let path = entry.path();

            // Only consider .md files
            if path.extension().is_none_or(|ext| ext != "md") {
                continue;
            }

            if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
                let stem_lower = stem.to_lowercase();

                // Score the match:
                // - Exact match (without extension): highest priority (score 0)
                // - Prefix match: high priority (score 1)
                // - Contains match: lower priority (score 2)
                let score = if stem_lower == query_lower {
                    Some(0)
                } else if stem_lower.starts_with(&query_lower) {
                    Some(1)
                } else if stem_lower.contains(&query_lower) {
                    Some(2)
                } else {
                    None
                };

                if let Some(s) = score {
                    matches.push((path, s));
                }
            }
        }
    }

    if matches.is_empty() {
        return None;
    }

    // Sort by score (best first), then by path name
    matches.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));

    // Return all matches with the best score
    let best_score = matches[0].1;
    let best_matches: Vec<PathBuf> = matches
        .into_iter()
        .filter(|(_, score)| *score == best_score)
        .map(|(path, _)| path)
        .collect();

    Some(best_matches)
}

/// Load config or print error message
pub fn load_config() -> Option<Config> {
    match Config::load() {
        Ok(c) => Some(c),
        Err(e) => {
            eprintln!("✗ Error loading config: {}", e);
            eprintln!("  Run 'diaryx init' first");
            None
        }
    }
}

/// Apply workspace-level overrides into the file-based Config.
///
/// No-op for now; kept as a stable hook while workspace and global config are unified.
pub fn apply_workspace_config(config: Config) -> Config {
    config
}

/// Format a YAML value for display
pub fn format_value(value: &YamlValue) -> String {
    match value {
        YamlValue::String(s) => s.clone(),
        YamlValue::Sequence(items) => {
            let items_str: Vec<String> = items.iter().map(format_value).collect();
            format!("[{}]", items_str.join(", "))
        }
        _ => serde_yaml::to_string(value)
            .unwrap_or_default()
            .trim()
            .to_string(),
    }
}
