//! Workspace command handlers

use diaryx_core::YamlValue;
use diaryx_core::config::Config;
use diaryx_core::entry::{prettify_filename, slugify};
use diaryx_core::fs::{FileSystem, RealFileSystem, SyncToAsyncFs};
use diaryx_core::link_parser::LinkFormat;
use diaryx_core::validate::ValidationFixer;
use diaryx_core::workspace::Workspace;
use std::path::{Path, PathBuf};

use crate::cli::args::WorkspaceCommands;
use crate::cli::util::{format_workspace_link, resolve_paths};
use crate::cli::{CliDiaryxAppSync, CliWorkspace, block_on};
use crate::editor::launch_editor;

/// Create a rooted workspace with link format from workspace config.
///
/// The core's `move_entry`/`sync_move_metadata` need the workspace root and
/// link format to properly format `contents` and `part_of` references.
fn make_rooted_workspace(ws: &CliWorkspace, path: &Path) -> CliWorkspace {
    if let Ok(Some(root_index)) = block_on(ws.detect_workspace(path)) {
        let workspace_root = root_index.parent().unwrap_or(Path::new("")).to_path_buf();
        let link_format = block_on(ws.get_workspace_config(&root_index))
            .map(|c| c.link_format)
            .unwrap_or_default();
        Workspace::with_link_format(ws.fs_ref().clone(), workspace_root, link_format)
    } else {
        // Fallback: no workspace detected, return unrooted workspace
        Workspace::new(ws.fs_ref().clone())
    }
}

/// Returns true on success, false on error
pub fn handle_workspace_command(
    command: WorkspaceCommands,
    workspace_override: Option<PathBuf>,
    ws: &CliWorkspace,
    app: &CliDiaryxAppSync,
) -> bool {
    let config = Config::load().ok();
    let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));

    match command {
        WorkspaceCommands::List => handle_list_workspaces(&config),

        WorkspaceCommands::Register { path, name } => {
            handle_register_workspace(&config, path, name)
        }

        WorkspaceCommands::Unregister { name_or_path, yes } => {
            handle_unregister_workspace(&config, &name_or_path, yes)
        }

        WorkspaceCommands::Default { name_or_path } => {
            handle_default_workspace(&config, name_or_path)
        }

        WorkspaceCommands::Info {
            path,
            depth,
            properties,
            delimiter,
        } => {
            handle_info(
                workspace_override,
                ws,
                &config,
                &current_dir,
                path,
                depth,
                properties,
                delimiter,
            );
            true // errors are printed to stderr
        }

        WorkspaceCommands::Init {
            dir,
            title,
            description,
        } => {
            handle_init(ws, dir, title, description, &current_dir);
            true
        }

        WorkspaceCommands::Path => {
            handle_path(workspace_override, ws, &config, &current_dir);
            true
        }

        WorkspaceCommands::Add {
            parent_or_child,
            child,
            new_index,
            recursive,
            yes,
            dry_run,
        } => {
            if let Some(ref cfg) = config {
                if recursive {
                    // Recursive add - create indexes for directory hierarchy
                    handle_add_recursive(
                        app,
                        cfg,
                        ws,
                        &current_dir,
                        &parent_or_child,
                        yes,
                        dry_run,
                    );
                    true
                } else if let Some(index_name) = new_index {
                    // Create new index and add files to it
                    handle_add_with_new_index(
                        app,
                        cfg,
                        ws,
                        &current_dir,
                        &parent_or_child,
                        child,
                        &index_name,
                        yes,
                        dry_run,
                    );
                    true
                } else {
                    let (parent, child_pattern) =
                        resolve_parent_child(ws, &current_dir, &parent_or_child, child);
                    if let (Some(p), Some(c)) = (parent, child_pattern) {
                        handle_add(app, cfg, ws, &p, &c, yes, dry_run);
                        true
                    } else {
                        false
                    }
                }
            } else {
                eprintln!("✗ No config found. Run 'diaryx init' first");
                false
            }
        }

        WorkspaceCommands::Create {
            parent_or_name,
            name,
            title,
            description,
            template,
            index,
            edit,
        } => {
            if let Some(ref cfg) = config {
                let (parent, name) = resolve_parent_name(ws, &current_dir, &parent_or_name, name);
                if let (Some(p), Some(n)) = (parent, name) {
                    handle_create(app, cfg, &p, &n, title, description, template, index, edit);
                    true
                } else {
                    false
                }
            } else {
                eprintln!("✗ No config found. Run 'diaryx init' first");
                false
            }
        }

        WorkspaceCommands::Remove {
            parent_or_child,
            child,
            dry_run,
        } => {
            if let Some(ref cfg) = config {
                let (parent, child) =
                    resolve_parent_child(ws, &current_dir, &parent_or_child, child);
                if let (Some(p), Some(c)) = (parent, child) {
                    handle_remove(app, ws, cfg, &p, &c, dry_run);
                    true
                } else {
                    false
                }
            } else {
                eprintln!("✗ No config found. Run 'diaryx init' first");
                false
            }
        }

        WorkspaceCommands::Mv {
            source,
            dest,
            new_index,
            dry_run,
        } => {
            if let Some(ref cfg) = config {
                handle_mv(app, cfg, ws, &source, &dest, new_index, dry_run);
                true
            } else {
                eprintln!("✗ No config found. Run 'diaryx init' first");
                false
            }
        }

        WorkspaceCommands::Validate {
            path,
            fix,
            recursive,
            search_build_folders,
            verbose,
        } => handle_validate(
            workspace_override,
            ws,
            &config,
            &current_dir,
            path,
            fix,
            recursive,
            search_build_folders,
            verbose,
        ),

        WorkspaceCommands::ConvertLinks {
            format,
            path,
            dry_run,
            yes,
        } => {
            handle_convert_links(
                workspace_override,
                ws,
                &config,
                &current_dir,
                &format,
                path,
                dry_run,
                yes,
            );
            true
        }

        WorkspaceCommands::Combine {
            source,
            target,
            yes,
            dry_run,
        } => {
            if config.is_some() {
                handle_combine(
                    workspace_override,
                    ws,
                    &config,
                    &current_dir,
                    &source,
                    &target,
                    yes,
                    dry_run,
                );
                true
            } else {
                eprintln!("✗ No config found. Run 'diaryx init' first");
                false
            }
        }
    }
}

/// Handle the 'workspace validate' command
/// Validates workspace link integrity (part_of and contents references)
#[allow(clippy::too_many_arguments)]
fn handle_validate(
    workspace_override: Option<PathBuf>,
    ws: &CliWorkspace,
    config: &Option<Config>,
    current_dir: &Path,
    file_path: Option<String>,
    fix: bool,
    recursive: bool,
    search_build_folders: bool,
    verbose: bool,
) -> bool {
    use diaryx_core::fs::RealFileSystem as CoreRealFileSystem;
    use diaryx_core::validate::{ValidationFixer, ValidationResult, Validator};

    let async_fs = SyncToAsyncFs::new(CoreRealFileSystem);
    let validator = Validator::new(async_fs.clone());
    let fixer = ValidationFixer::new(async_fs);

    // If a specific path is provided, validate it (file or directory)
    if let Some(ref path_str) = file_path {
        let input_path = PathBuf::from(path_str);
        let resolved_path = if input_path.is_absolute() {
            input_path
        } else {
            current_dir.join(&input_path)
        };

        // Check if it's a directory
        if resolved_path.is_dir() {
            // Validate all markdown files in the directory
            let files = if recursive {
                collect_md_files_recursive(&resolved_path, search_build_folders)
            } else {
                collect_md_files(&resolved_path)
            };

            if files.is_empty() {
                println!("✓ No markdown files found in {}", resolved_path.display());
                return true;
            }

            if verbose {
                println!(
                    "Validating {} file(s) in {}",
                    files.len(),
                    resolved_path.display()
                );
            }

            let mut total_result = ValidationResult::default();

            for file in &files {
                match block_on(validator.validate_file(file)) {
                    Ok(result) => {
                        total_result.files_checked += result.files_checked;
                        total_result.errors.extend(result.errors);
                        total_result.warnings.extend(result.warnings);
                    }
                    Err(e) => {
                        if verbose {
                            eprintln!("  ⚠ Error validating {}: {}", file.display(), e);
                        }
                    }
                }
            }

            // Report and fix using the aggregated result
            report_and_fix_validation(&fixer, &total_result, fix, &resolved_path, verbose);
            return true;
        }

        // Single file validation
        if verbose {
            println!("Validating file: {}", resolved_path.display());
        }

        let result = match block_on(validator.validate_file(&resolved_path)) {
            Ok(r) => r,
            Err(e) => {
                eprintln!("✗ Error validating file: {}", e);
                return false;
            }
        };

        report_and_fix_validation(&fixer, &result, fix, &resolved_path, verbose);
        return true;
    }

    // Full workspace validation
    // Find workspace root
    let root_path = if let Some(override_path) = workspace_override {
        override_path.clone()
    } else if let Ok(Some(detected)) = block_on(ws.detect_workspace(current_dir)) {
        detected
    } else if let Some(cfg) = config {
        if let Ok(Some(root)) = block_on(ws.find_root_index_in_dir(&cfg.default_workspace)) {
            root
        } else {
            eprintln!("✗ No workspace found");
            eprintln!("  Run 'diaryx init' or 'diaryx workspace init' first");
            return false;
        }
    } else {
        eprintln!("✗ No workspace found");
        eprintln!("  Run 'diaryx init' or 'diaryx workspace init' first");
        return false;
    };

    if verbose {
        println!("Validating workspace: {}", root_path.display());
    }

    // Use depth limit of 2 to match tree view behavior and improve performance
    // For full validation, users can use the --recursive flag on a directory
    let result = match block_on(validator.validate_workspace(&root_path, Some(2))) {
        Ok(r) => r,
        Err(e) => {
            eprintln!("✗ Error validating workspace: {}", e);
            return false;
        }
    };

    if result.is_ok() && result.warnings.is_empty() {
        println!(
            "✓ Workspace validation passed ({} files checked)",
            result.files_checked
        );
        return true;
    }

    report_and_fix_validation(&fixer, &result, fix, &root_path, verbose);
    true
}

/// Handle the 'workspace combine' command
#[allow(clippy::too_many_arguments)]
fn handle_combine(
    workspace_override: Option<PathBuf>,
    ws: &CliWorkspace,
    config: &Option<Config>,
    current_dir: &Path,
    source: &str,
    target: &str,
    yes: bool,
    dry_run: bool,
) {
    // 1. Resolve workspace root
    let root_path = if let Some(override_path) = workspace_override {
        override_path
    } else if let Ok(Some(detected)) = block_on(ws.detect_workspace(current_dir)) {
        detected
    } else if let Some(cfg) = config {
        if let Ok(Some(root)) = block_on(ws.find_root_index_in_dir(&cfg.default_workspace)) {
            root
        } else {
            eprintln!("✗ No workspace found");
            return;
        }
    } else {
        eprintln!("✗ No workspace found");
        return;
    };

    // We need a workspace instance with the correct root for linking
    // Re-create workspace with detected root
    // Note: CliWorkspace wraps a core Workspace, but we might need to verify if it has root set correctly.
    // The `ws` passed in is created in main.
    // Let's assume `ws` is good enough, but we might need to ensure it has the root set if we use it for combining.
    // Actually `ws` in `run_cli` is initialized with `Workspace::new` or similar.
    // `handle_workspace_command` receives `ws: &CliWorkspace`.
    // The core `Workspace` has `with_link_format`.
    // Let's check `ws` definition. `type CliWorkspace = Workspace<SyncToAsyncFs<RealFileSystem>>;`

    // 2. Resolve source and target paths to absolute paths
    // They can be relative to current dir or absolute
    let source_path = current_dir.join(source);
    let target_path = current_dir.join(target);

    // 3. Confirm action
    if !yes && !dry_run {
        println!("This will:");
        println!("  1. Move all contents from '{}' to '{}'", source, target);
        println!("  2. Append the body text of source to target");
        println!("  3. DELETE the source file '{}'", source);
        println!();

        use std::io::{self, Write};
        print!("Continue? [y/N] ");
        io::stdout().flush().ok();

        let mut input = String::new();
        io::stdin().read_line(&mut input).ok();

        if !input.trim().eq_ignore_ascii_case("y") {
            println!("Aborted.");
            return;
        }
    }

    if dry_run {
        println!("(Dry run) Would combine '{}' into '{}'", source, target);
        return;
    }

    // 4. Perform combine
    println!("Combining indices...");

    // Create a new workspace instance with the correctly resolved root path
    // This allows combine_indices to correctly resolve workspace-relative paths
    // even if the original ws was created without knowing the root.
    // We try to get the link format from config or default to MarkdownRoot
    let link_format = if let Some(cfg) = config {
        cfg.link_format
    } else {
        LinkFormat::MarkdownRoot
    };

    // Since CliWorkspace is a type alias for Workspace<SyncToAsyncFs<RealFileSystem>>,
    // we can construct a new one using the same filesystem implementation.
    // The original ws has the filesystem we want to reuse (cloning it is cheap as it wraps unit struct/Arc).
    let ws_with_root = diaryx_core::workspace::Workspace::with_link_format(
        ws.fs_ref().clone(),
        root_path.parent().unwrap_or(Path::new(".")).to_path_buf(), // root_path is the README file, we want the directory
        link_format,
    );

    match block_on(ws_with_root.combine_indices(&source_path, &target_path)) {
        Ok(_) => {
            println!("✓ Successfully combined '{}' into '{}'", source, target);
        }
        Err(e) => {
            eprintln!("✗ Failed to combine indices: {}", e);
        }
    }
}

/// Helper function to report validation results and optionally fix issues
fn report_and_fix_validation(
    fixer: &ValidationFixer<SyncToAsyncFs<RealFileSystem>>,
    result: &diaryx_core::validate::ValidationResult,
    fix: bool,
    context_path: &Path,
    verbose: bool,
) {
    if result.is_ok() && result.warnings.is_empty() {
        println!(
            "✓ Validation passed: {} ({} file(s) checked)",
            context_path.display(),
            result.files_checked
        );
        return;
    }

    let mut fixed_count = 0;

    if !result.errors.is_empty() {
        if verbose {
            println!("Errors ({}):", result.errors.len());
        }
        for err in &result.errors {
            let fix_result = if fix {
                Some(block_on(fixer.fix_error(err)))
            } else {
                None
            };
            match fix_result {
                Some(fr) if fr.success => {
                    println!("  ✓ Fixed: {}", fr.message);
                    fixed_count += 1;
                }
                Some(fr) => {
                    println!(
                        "  ✗ {}: {} (fix failed: {})",
                        err.description(),
                        err.detail(),
                        fr.message
                    );
                }
                None => {
                    println!("  ✗ {}: {}", err.description(), err.detail());
                }
            }
        }
    }

    if !result.warnings.is_empty() {
        if verbose {
            println!("Warnings ({}):", result.warnings.len());
        }
        for warn in &result.warnings {
            let fix_result = if fix && warn.can_auto_fix() {
                block_on(fixer.fix_warning(warn))
            } else {
                None
            };
            match fix_result {
                Some(fr) if fr.success => {
                    println!("  ✓ Fixed: {}", fr.message);
                    fixed_count += 1;
                }
                Some(fr) => {
                    println!(
                        "  ⚠ {}: {} (fix failed: {})",
                        warn.description(),
                        warn.detail(),
                        fr.message
                    );
                }
                None => {
                    println!("  ⚠ {}: {}", warn.description(), warn.detail());
                }
            }
        }
    }

    println!();
    if fix && fixed_count > 0 {
        println!(
            "Summary: {} issue(s) fixed, {} file(s) checked",
            fixed_count, result.files_checked
        );
    } else {
        println!(
            "Summary: {} error(s), {} warning(s), {} file(s) checked",
            result.errors.len(),
            result.warnings.len(),
            result.files_checked
        );
    }
}

// Note: The fix functions (fix_broken_contents_ref, add_file_to_contents, fix_non_portable_path,
// fix_broken_attachment, add_file_to_attachments, fix_missing_part_of) have been moved to
// diaryx_core::validate::ValidationFixer for code reuse across CLI, WASM, and Tauri backends.

/// Handle the 'workspace mv' command
/// Moves/renames a file while updating workspace hierarchy references
fn handle_mv(
    app: &CliDiaryxAppSync,
    config: &Config,
    ws: &CliWorkspace,
    source: &str,
    dest: &str,
    new_index: Option<String>,
    dry_run: bool,
) {
    // Resolve source path (should be a single file)
    let source_paths = resolve_paths(source, config, app);
    if source_paths.is_empty() {
        eprintln!("✗ No files matched source: {}", source);
        return;
    }
    if source_paths.len() > 1 {
        eprintln!("✗ Source must be a single file, but matched multiple:");
        for p in &source_paths {
            eprintln!("  {}", p.display());
        }
        return;
    }
    let source_path = &source_paths[0];

    if !source_path.exists() {
        eprintln!("✗ Source file does not exist: {}", source_path.display());
        return;
    }

    // Determine destination path
    let dest_input = PathBuf::from(dest);
    // Canonicalize the destination directory if it exists, to get clean paths
    let dest_path = if dest_input.is_dir() {
        // If dest is a directory, move file into it with same name
        let canonical_dir = dest_input.canonicalize().unwrap_or(dest_input);
        canonical_dir.join(source_path.file_name().unwrap_or_default())
    } else if !dest.ends_with(".md") {
        // Add .md extension if not present
        let with_ext = PathBuf::from(format!("{}.md", dest));
        // Try to canonicalize the parent directory
        if let Some(parent) = with_ext.parent() {
            if parent.exists() {
                if let Ok(canonical_parent) = parent.canonicalize() {
                    canonical_parent.join(with_ext.file_name().unwrap_or_default())
                } else {
                    with_ext
                }
            } else {
                with_ext
            }
        } else {
            with_ext
        }
    } else {
        // Try to canonicalize the parent directory
        if let Some(parent) = dest_input.parent() {
            if parent.exists() {
                if let Ok(canonical_parent) = parent.canonicalize() {
                    canonical_parent.join(dest_input.file_name().unwrap_or_default())
                } else {
                    dest_input
                }
            } else {
                dest_input
            }
        } else {
            dest_input
        }
    };

    if dest_path.exists() {
        eprintln!("✗ Destination already exists: {}", dest_path.display());
        return;
    }

    if dry_run {
        println!(
            "Would move '{}' to '{}'",
            source_path.display(),
            dest_path.display()
        );
        if let Some(index_name) = new_index {
            let index_filename = if index_name.ends_with(".md") {
                index_name
            } else {
                format!("{}.md", index_name)
            };
            let index_path = dest_path
                .parent()
                .map(|p| p.join(&index_filename))
                .unwrap_or_else(|| PathBuf::from(&index_filename));
            if index_path.exists() {
                println!(
                    "Would set part_of to existing index '{}'",
                    index_path.display()
                );
            } else {
                println!(
                    "Would create new index '{}' and set as parent",
                    index_path.display()
                );
            }
        }
        return;
    }

    // Use the core workspace's move_entry which properly handles link format
    // normalization and workspace config (contents/part_of updates).
    // Create a rooted workspace so the core knows the workspace root and link format.
    let rooted_ws = make_rooted_workspace(ws, source_path);
    match block_on(rooted_ws.move_entry(source_path, &dest_path)) {
        Ok(()) => {
            println!(
                "✓ Moved '{}' to '{}'",
                source_path.display(),
                dest_path.display()
            );
        }
        Err(e) => {
            eprintln!("✗ Error moving file: {}", e);
            return;
        }
    }

    // If --new-index is specified and move succeeded, create/use index as parent
    if let Some(index_name) = new_index {
        set_new_index_as_parent(app, ws, &dest_path, &index_name);
    }
}

/// Set a new or existing index as the parent of a file
fn set_new_index_as_parent(
    app: &CliDiaryxAppSync,
    ws: &CliWorkspace,
    file_path: &Path,
    index_name: &str,
) {
    let file_dir = file_path.parent().unwrap_or(Path::new("."));

    // Create index filename
    let index_filename = if index_name.ends_with(".md") {
        index_name.to_string()
    } else {
        format!("{}.md", index_name)
    };
    let index_path = file_dir.join(&index_filename);
    let index_str = index_path.to_string_lossy();

    // Check if index exists, if not create it
    if !index_path.exists() {
        // Create title from index name
        let title = index_name.trim_end_matches(".md").replace(['_', '-'], " ");
        let title = title
            .split_whitespace()
            .map(|word| {
                let mut chars = word.chars();
                match chars.next() {
                    None => String::new(),
                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
                }
            })
            .collect::<Vec<_>>()
            .join(" ");

        // Create the index with title and empty contents
        if let Err(e) = app.set_frontmatter_property(&index_str, "title", YamlValue::String(title))
        {
            eprintln!("✗ Error creating index file: {}", e);
            return;
        }

        if let Err(e) =
            app.set_frontmatter_property(&index_str, "contents", YamlValue::Sequence(vec![]))
        {
            eprintln!("✗ Error setting contents in index: {}", e);
            return;
        }

        // Find parent index for the new index
        if let Ok(Some(parent_index)) = block_on(ws.find_any_index_in_dir(file_dir)) {
            // Don't set parent if it's the same as the new index
            if parent_index != index_path {
                let relative_parent = format_workspace_link(ws, &index_path, &parent_index, None);
                if let Err(e) = app.set_frontmatter_property(
                    &index_str,
                    "part_of",
                    YamlValue::String(relative_parent),
                ) {
                    eprintln!("⚠ Error setting part_of in new index: {}", e);
                }

                // Add new index to parent's contents
                let parent_str = parent_index.to_string_lossy();
                let relative_index = format_workspace_link(ws, &parent_index, &index_path, None);

                if let Ok(Some(YamlValue::Sequence(mut items))) =
                    app.get_frontmatter_property(&parent_str, "contents")
                {
                    items.push(YamlValue::String(relative_index.clone()));
                    if let Err(e) = app.set_frontmatter_property(
                        &parent_str,
                        "contents",
                        YamlValue::Sequence(items),
                    ) {
                        eprintln!("⚠ Error updating parent contents: {}", e);
                    } else {
                        println!(
                            "✓ Added '{}' to parent '{}'",
                            relative_index,
                            parent_index.display()
                        );
                    }
                }
            }
        }

        println!("✓ Created index '{}'", index_path.display());
    }

    // Add file to index's contents
    let relative_file = format_workspace_link(ws, &index_path, file_path, None);
    match app.get_frontmatter_property(&index_str, "contents") {
        Ok(Some(YamlValue::Sequence(mut items))) => {
            let file_value = YamlValue::String(relative_file.clone());
            if !items.contains(&file_value) {
                items.push(file_value);
                if let Err(e) =
                    app.set_frontmatter_property(&index_str, "contents", YamlValue::Sequence(items))
                {
                    eprintln!("✗ Error updating index contents: {}", e);
                    return;
                }
                println!(
                    "✓ Added '{}' to index '{}'",
                    relative_file,
                    index_path.display()
                );
            }
        }
        Ok(None) => {
            let items = vec![YamlValue::String(relative_file.clone())];
            if let Err(e) =
                app.set_frontmatter_property(&index_str, "contents", YamlValue::Sequence(items))
            {
                eprintln!("✗ Error creating index contents: {}", e);
                return;
            }
            println!(
                "✓ Added '{}' to index '{}'",
                relative_file,
                index_path.display()
            );
        }
        _ => {
            eprintln!("✗ Index contents is not a list");
            return;
        }
    }

    // Set part_of in the moved file
    let file_str = file_path.to_string_lossy();
    let relative_index = format_workspace_link(ws, file_path, &index_path, None);
    if let Err(e) =
        app.set_frontmatter_property(&file_str, "part_of", YamlValue::String(relative_index))
    {
        eprintln!("✗ Error setting part_of in moved file: {}", e);
    } else {
        println!("✓ Set part_of in '{}'", file_path.display());
    }
}

/// Collect markdown files in a directory (non-recursive)
fn collect_md_files(dir: &Path) -> Vec<PathBuf> {
    let mut files = Vec::new();
    if let Ok(entries) = std::fs::read_dir(dir) {
        for entry in entries.filter_map(|e| e.ok()) {
            let path = entry.path();
            if path.is_file() && path.extension().is_some_and(|ext| ext == "md") {
                files.push(path);
            }
        }
    }
    files
}

/// Directories to skip by default for performance (build outputs, dependencies, VCS)
const DEFAULT_SKIP_DIRS: &[&str] = &[
    "node_modules",
    "target",
    ".git",
    ".svn",
    ".hg",
    "dist",
    "build",
    "__pycache__",
    ".next",
    ".nuxt",
    "vendor",
    ".cargo",
    ".venv",
    "venv",
    ".tox",
    "coverage",
    ".nyc_output",
    ".cache",
    ".parcel-cache",
    ".turbo",
    "out",
    ".output",
    "gen",
];

/// Collect markdown files in a directory (recursive)
/// If `search_build_folders` is false, skips common build/dependency directories
fn collect_md_files_recursive(dir: &Path, search_build_folders: bool) -> Vec<PathBuf> {
    let mut files = Vec::new();
    collect_md_files_recursive_helper(dir, &mut files, search_build_folders);
    files
}

fn collect_md_files_recursive_helper(
    dir: &Path,
    files: &mut Vec<PathBuf>,
    search_build_folders: bool,
) {
    if let Ok(entries) = std::fs::read_dir(dir) {
        for entry in entries.filter_map(|e| e.ok()) {
            let path = entry.path();
            if path.is_dir() {
                // Check if we should skip this directory
                if !search_build_folders
                    && let Some(name) = path.file_name().and_then(|n| n.to_str())
                {
                    // Skip hidden directories
                    if name.starts_with('.') {
                        continue;
                    }
                    // Skip common build/dependency directories
                    if DEFAULT_SKIP_DIRS.contains(&name) {
                        continue;
                    }
                }
                collect_md_files_recursive_helper(&path, files, search_build_folders);
            } else if path.is_file() && path.extension().is_some_and(|ext| ext == "md") {
                files.push(path);
            }
        }
    }
}

/// Resolve parent and child arguments, using local index as default parent if only one arg provided
fn resolve_parent_child(
    ws: &CliWorkspace,
    current_dir: &Path,
    parent_or_child: &str,
    child: Option<String>,
) -> (Option<String>, Option<String>) {
    match child {
        // Two arguments provided: parent_or_child is parent, child is child
        Some(c) => (Some(parent_or_child.to_string()), Some(c)),
        // One argument provided: find local index as parent, parent_or_child is child
        None => match block_on(ws.find_any_index_in_dir(current_dir)) {
            Ok(Some(index_path)) => {
                let parent = index_path.to_string_lossy().to_string();
                (Some(parent), Some(parent_or_child.to_string()))
            }
            Ok(None) => {
                eprintln!("✗ No index file found in current directory");
                eprintln!("  Either specify a parent explicitly or create an index first");
                (None, None)
            }
            Err(e) => {
                eprintln!("✗ Error finding index: {}", e);
                (None, None)
            }
        },
    }
}

/// Resolve parent and name arguments for create, using local index as default parent if only one arg provided
fn resolve_parent_name(
    ws: &CliWorkspace,
    current_dir: &Path,
    parent_or_name: &str,
    name: Option<String>,
) -> (Option<String>, Option<String>) {
    match name {
        // Two arguments provided: parent_or_name is parent, name is name
        Some(n) => (Some(parent_or_name.to_string()), Some(n)),
        // One argument provided: find local index as parent, parent_or_name is name
        None => match block_on(ws.find_any_index_in_dir(current_dir)) {
            Ok(Some(index_path)) => {
                let parent = index_path.to_string_lossy().to_string();
                (Some(parent), Some(parent_or_name.to_string()))
            }
            Ok(None) => {
                eprintln!("✗ No index file found in current directory");
                eprintln!("  Either specify a parent explicitly or create an index first");
                (None, None)
            }
            Err(e) => {
                eprintln!("✗ Error finding index: {}", e);
                (None, None)
            }
        },
    }
}

/// Handle the 'workspace info' command
#[allow(clippy::too_many_arguments)]
fn handle_info(
    workspace_override: Option<PathBuf>,
    ws: &CliWorkspace,
    config: &Option<Config>,
    current_dir: &Path,
    path: Option<String>,
    max_depth: usize,
    properties: Option<Vec<String>>,
    delimiter: String,
) {
    // If a path is provided, resolve it (supports "." for local index)
    let root_path = if let Some(ref p) = path {
        if p == "." {
            // Resolve to local index in current directory
            match block_on(ws.find_any_index_in_dir(current_dir)) {
                Ok(Some(index)) => index,
                Ok(None) => {
                    eprintln!("✗ No index found in current directory");
                    return;
                }
                Err(e) => {
                    eprintln!("✗ Error finding index: {}", e);
                    return;
                }
            }
        } else {
            // Treat as a path to resolve
            let path_buf = Path::new(p);
            if path_buf.is_file() {
                path_buf.to_path_buf()
            } else if path_buf.is_dir() {
                // Find index in that directory
                match block_on(ws.find_any_index_in_dir(path_buf)) {
                    Ok(Some(index)) => index,
                    Ok(None) => {
                        eprintln!("✗ No index found in directory: {}", p);
                        return;
                    }
                    Err(e) => {
                        eprintln!("✗ Error finding index: {}", e);
                        return;
                    }
                }
            } else {
                eprintln!("✗ Path not found: {}", p);
                return;
            }
        }
    } else if let Some(override_path) = workspace_override {
        override_path.clone()
    } else if let Ok(Some(detected)) = block_on(ws.detect_workspace(current_dir)) {
        detected
    } else if let Some(cfg) = config {
        if let Ok(Some(root)) = block_on(ws.find_root_index_in_dir(&cfg.default_workspace)) {
            root
        } else {
            eprintln!("✗ No workspace found");
            eprintln!("  Run 'diaryx init' or 'diaryx workspace init' first");
            return;
        }
    } else {
        eprintln!("✗ No workspace found");
        eprintln!("  Run 'diaryx init' or 'diaryx workspace init' first");
        return;
    };

    // Convert 0 to None (unlimited), otherwise Some(depth)
    let depth_limit = if max_depth == 0 {
        None
    } else {
        Some(max_depth)
    };

    // Default properties: title, description
    let props = properties.unwrap_or_else(|| vec!["title".to_string(), "description".to_string()]);

    let result =
        block_on(ws.workspace_info_with_properties(&root_path, depth_limit, &props, &delimiter));

    match result {
        Ok(tree_output) => {
            println!("{}", tree_output);
        }
        Err(e) => eprintln!("✗ Error reading workspace: {}", e),
    }
}

/// Handle the 'workspace init' command
fn handle_init(
    ws: &CliWorkspace,
    dir: Option<PathBuf>,
    title: Option<String>,
    description: Option<String>,
    current_dir: &Path,
) {
    let target_dir = dir.unwrap_or_else(|| current_dir.to_path_buf());
    let canonical = std::fs::canonicalize(&target_dir).unwrap_or_else(|_| target_dir.clone());

    match block_on(ws.init_workspace(&target_dir, title.as_deref(), description.as_deref())) {
        Ok(readme_path) => {
            println!("✓ Initialized workspace");
            println!("  Index file: {}", readme_path.display());

            // Auto-register in the workspace registry
            auto_register_workspace(&canonical, title.as_deref());
        }
        Err(e) => eprintln!("✗ Error initializing workspace: {}", e),
    }
}

/// Handle the 'workspace path' command
fn handle_path(
    workspace_override: Option<PathBuf>,
    ws: &CliWorkspace,
    config: &Option<Config>,
    current_dir: &Path,
) {
    let root_path = if let Some(override_path) = workspace_override {
        Some(override_path.clone())
    } else if let Ok(Some(detected)) = block_on(ws.detect_workspace(current_dir)) {
        Some(detected)
    } else if let Some(cfg) = config {
        block_on(ws.find_root_index_in_dir(&cfg.default_workspace))
            .ok()
            .flatten()
    } else {
        None
    };

    match root_path {
        Some(path) => {
            if let Some(dir) = path.parent() {
                println!("{}", dir.display());
            } else {
                println!("{}", path.display());
            }
        }
        None => {
            eprintln!("✗ No workspace found");
            eprintln!("  Run 'diaryx init' or 'diaryx workspace init' first");
        }
    }
}

/// Handle the 'workspace add --recursive' command
/// Recursively creates indexes for a directory hierarchy and connects them
fn handle_add_recursive(
    app: &CliDiaryxAppSync,
    config: &Config,
    ws: &CliWorkspace,
    current_dir: &Path,
    dir_path: &str,
    yes: bool,
    dry_run: bool,
) {
    use crate::cli::util::{ConfirmResult, prompt_confirm};

    // Resolve the directory path
    let path = Path::new(dir_path);
    let dir = if path.is_absolute() || path.exists() {
        PathBuf::from(dir_path)
    } else {
        current_dir.join(dir_path)
    };

    if !dir.exists() {
        eprintln!("✗ Directory does not exist: {}", dir.display());
        return;
    }

    if !dir.is_dir() {
        eprintln!("✗ Path is not a directory: {}", dir.display());
        eprintln!("  Use 'diaryx w add' without --recursive for files");
        return;
    }

    // Collect the directory structure
    let mut plan = RecursiveAddPlan::new();
    build_recursive_plan(&dir, &mut plan, ws);

    if plan.directories.is_empty() {
        eprintln!("✗ No directories to process");
        return;
    }

    // Show the plan
    println!("Recursive add plan for '{}':", dir.display());
    println!();

    let mut total_indexes = 0;
    let mut total_files = 0;

    for dir_plan in &plan.directories {
        let action = if dir_plan.index_exists {
            "use existing"
        } else {
            total_indexes += 1;
            "create"
        };
        println!(
            "  {} ({} index): {}",
            dir_plan.dir.display(),
            action,
            dir_plan
                .index_path
                .file_name()
                .unwrap_or_default()
                .to_string_lossy()
        );
        for file in &dir_plan.files {
            println!(
                "    + {}",
                file.file_name().unwrap_or_default().to_string_lossy()
            );
            total_files += 1;
        }
        for subdir in &dir_plan.subdirs {
            println!(
                "    → {}",
                subdir.file_name().unwrap_or_default().to_string_lossy()
            );
        }
    }

    println!();
    println!(
        "Summary: {} new index(es), {} file(s) to add",
        total_indexes, total_files
    );

    if dry_run {
        println!();
        println!("Dry run - no changes made");
        return;
    }

    // Confirm
    if !yes {
        println!();
        match prompt_confirm("Proceed?") {
            ConfirmResult::Yes | ConfirmResult::All => {}
            _ => {
                println!("Aborted");
                return;
            }
        }
    }

    // Execute the plan (process directories from deepest to shallowest)
    // This ensures child indexes exist before we link them to parents
    let mut sorted_dirs = plan.directories.clone();
    sorted_dirs.sort_by(|a, b| {
        let depth_a = a.dir.components().count();
        let depth_b = b.dir.components().count();
        depth_b.cmp(&depth_a) // Deepest first
    });

    for dir_plan in &sorted_dirs {
        execute_dir_plan(app, config, ws, dir_plan);
    }

    // Connect root index to workspace if applicable
    if let Some(root_plan) = plan.directories.first() {
        // Find parent index for the root directory
        if let Some(parent_dir) = root_plan.dir.parent()
            && let Ok(Some(parent_index)) = block_on(ws.find_any_index_in_dir(parent_dir))
        {
            // Check if root index is already in parent's contents
            let parent_str = parent_index.to_string_lossy();
            let relative_root =
                format_workspace_link(ws, &parent_index, &root_plan.index_path, None);

            let already_linked = match app.get_frontmatter_property(&parent_str, "contents") {
                Ok(Some(YamlValue::Sequence(items))) => items.iter().any(|item| {
                    if let YamlValue::String(s) = item {
                        s == &relative_root
                    } else {
                        false
                    }
                }),
                _ => false,
            };

            if !already_linked {
                // Add to parent's contents
                match app.get_frontmatter_property(&parent_str, "contents") {
                    Ok(Some(YamlValue::Sequence(mut items))) => {
                        items.push(YamlValue::String(relative_root.clone()));
                        if let Err(e) = app.set_frontmatter_property(
                            &parent_str,
                            "contents",
                            YamlValue::Sequence(items),
                        ) {
                            eprintln!("⚠ Error updating parent contents: {}", e);
                        } else {
                            println!(
                                "✓ Added '{}' to workspace index '{}'",
                                relative_root,
                                parent_index.display()
                            );
                        }
                    }
                    Ok(None) | Ok(Some(_)) => {
                        let items = vec![YamlValue::String(relative_root.clone())];
                        if let Err(e) = app.set_frontmatter_property(
                            &parent_str,
                            "contents",
                            YamlValue::Sequence(items),
                        ) {
                            eprintln!("⚠ Error creating parent contents: {}", e);
                        } else {
                            println!(
                                "✓ Added '{}' to workspace index '{}'",
                                relative_root,
                                parent_index.display()
                            );
                        }
                    }
                    Err(e) => {
                        eprintln!("⚠ Error reading parent contents: {}", e);
                    }
                }

                // Set part_of in root index
                let root_str = root_plan.index_path.to_string_lossy();
                let relative_parent =
                    format_workspace_link(ws, &root_plan.index_path, &parent_index, None);
                if let Err(e) = app.set_frontmatter_property(
                    &root_str,
                    "part_of",
                    YamlValue::String(relative_parent),
                ) {
                    eprintln!("⚠ Error setting part_of in root index: {}", e);
                }
            }
        }
    }

    println!();
    println!("✓ Recursive add complete");
}

/// Plan for a single directory in recursive add
#[derive(Clone)]
struct DirPlan {
    dir: PathBuf,
    index_path: PathBuf,
    index_exists: bool,
    files: Vec<PathBuf>,
    subdirs: Vec<PathBuf>,
}

/// Overall plan for recursive add
struct RecursiveAddPlan {
    directories: Vec<DirPlan>,
}

impl RecursiveAddPlan {
    fn new() -> Self {
        Self {
            directories: Vec::new(),
        }
    }
}

/// Build a plan for recursive directory processing
fn build_recursive_plan(dir: &Path, plan: &mut RecursiveAddPlan, ws: &CliWorkspace) {
    // Determine index path for this directory
    let dir_name = dir
        .file_name()
        .map(|n| n.to_string_lossy().to_lowercase())
        .unwrap_or_else(|| "index".into());
    let index_filename = format!("{}_index.md", dir_name);
    let index_path = dir.join(&index_filename);

    // Check if an index already exists
    let (final_index_path, index_exists) = match block_on(ws.find_any_index_in_dir(dir)) {
        Ok(Some(existing)) => (existing, true),
        _ => (index_path, false),
    };

    // Collect files and subdirectories
    let mut files = Vec::new();
    let mut subdirs = Vec::new();

    if let Ok(entries) = std::fs::read_dir(dir) {
        for entry in entries.filter_map(|e| e.ok()) {
            let path = entry.path();
            if path.is_dir() {
                subdirs.push(path);
            } else if path.is_file()
                && path.extension().is_some_and(|ext| ext == "md")
                && path != final_index_path
            {
                files.push(path);
            }
        }
    }

    // Sort for consistent ordering
    files.sort();
    subdirs.sort();

    // Add this directory to the plan
    plan.directories.push(DirPlan {
        dir: dir.to_path_buf(),
        index_path: final_index_path,
        index_exists,
        files,
        subdirs: subdirs.clone(),
    });

    // Recurse into subdirectories
    for subdir in subdirs {
        build_recursive_plan(&subdir, plan, ws);
    }
}

/// Execute the plan for a single directory
fn execute_dir_plan(
    app: &CliDiaryxAppSync,
    _config: &Config,
    ws: &CliWorkspace,
    dir_plan: &DirPlan,
) {
    let index_str = dir_plan.index_path.to_string_lossy();

    // Create index if it doesn't exist
    if !dir_plan.index_exists {
        let dir_name = dir_plan
            .dir
            .file_name()
            .map(|n| n.to_string_lossy().to_string())
            .unwrap_or_else(|| "Index".to_string());

        // Title case the directory name
        let title = dir_name
            .replace(['_', '-'], " ")
            .split_whitespace()
            .map(|word| {
                let mut chars = word.chars();
                match chars.next() {
                    None => String::new(),
                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
                }
            })
            .collect::<Vec<_>>()
            .join(" ");

        // Create with title
        if let Err(e) =
            app.set_frontmatter_property(&index_str, "title", YamlValue::String(title.clone()))
        {
            eprintln!(
                "✗ Error creating index '{}': {}",
                dir_plan.index_path.display(),
                e
            );
            return;
        }
        println!("✓ Created index '{}'", dir_plan.index_path.display());
    } else {
        println!("✓ Using existing index '{}'", dir_plan.index_path.display());
    }

    // Build contents list
    let mut contents: Vec<String> = Vec::new();

    // Get existing contents if index existed
    if dir_plan.index_exists
        && let Ok(Some(YamlValue::Sequence(items))) =
            app.get_frontmatter_property(&index_str, "contents")
    {
        for item in items {
            if let YamlValue::String(s) = item {
                contents.push(s);
            }
        }
    }

    // Add files
    for file in &dir_plan.files {
        let relative = format_workspace_link(ws, &dir_plan.index_path, file, None);
        if !contents.contains(&relative) {
            contents.push(relative.clone());

            // Set part_of in the file
            let file_str = file.to_string_lossy();
            let relative_to_index = format_workspace_link(ws, file, &dir_plan.index_path, None);
            if let Err(e) = app.set_frontmatter_property(
                &file_str,
                "part_of",
                YamlValue::String(relative_to_index),
            ) {
                eprintln!("⚠ Error setting part_of in '{}': {}", file.display(), e);
            }
        }
    }

    // Add subdirectory indexes
    for subdir in &dir_plan.subdirs {
        // Find the index in the subdirectory
        if let Ok(Some(subdir_index)) = block_on(ws.find_any_index_in_dir(subdir)) {
            let relative = format_workspace_link(ws, &dir_plan.index_path, &subdir_index, None);
            if !contents.contains(&relative) {
                contents.push(relative.clone());

                // Set part_of in the subdirectory index
                let subdir_str = subdir_index.to_string_lossy();
                let relative_to_parent =
                    format_workspace_link(ws, &subdir_index, &dir_plan.index_path, None);
                if let Err(e) = app.set_frontmatter_property(
                    &subdir_str,
                    "part_of",
                    YamlValue::String(relative_to_parent),
                ) {
                    eprintln!(
                        "⚠ Error setting part_of in '{}': {}",
                        subdir_index.display(),
                        e
                    );
                }
            }
        } else {
            // Subdirectory index should have been created - construct the expected path
            let subdir_name = subdir
                .file_name()
                .map(|n| n.to_string_lossy().to_lowercase())
                .unwrap_or_else(|| "index".into());
            let subdir_index = subdir.join(format!("{}_index.md", subdir_name));

            if subdir_index.exists() {
                let relative = format_workspace_link(ws, &dir_plan.index_path, &subdir_index, None);
                if !contents.contains(&relative) {
                    contents.push(relative.clone());

                    // Set part_of in the subdirectory index
                    let subdir_str = subdir_index.to_string_lossy();
                    let relative_to_parent =
                        format_workspace_link(ws, &subdir_index, &dir_plan.index_path, None);
                    if let Err(e) = app.set_frontmatter_property(
                        &subdir_str,
                        "part_of",
                        YamlValue::String(relative_to_parent),
                    ) {
                        eprintln!(
                            "⚠ Error setting part_of in '{}': {}",
                            subdir_index.display(),
                            e
                        );
                    }
                }
            }
        }
    }

    // Update contents in index
    let contents_yaml: Vec<YamlValue> = contents
        .iter()
        .map(|s| YamlValue::String(s.clone()))
        .collect();

    if let Err(e) =
        app.set_frontmatter_property(&index_str, "contents", YamlValue::Sequence(contents_yaml))
    {
        eprintln!(
            "✗ Error setting contents in '{}': {}",
            dir_plan.index_path.display(),
            e
        );
    }
}

/// Handle the 'workspace add --new-index' command
/// Creates a new index file and adds files to it
#[allow(clippy::too_many_arguments)]
fn handle_add_with_new_index(
    app: &CliDiaryxAppSync,
    config: &Config,
    ws: &CliWorkspace,
    current_dir: &Path,
    file_pattern: &str,
    additional_pattern: Option<String>,
    index_name: &str,
    yes: bool,
    dry_run: bool,
) {
    use crate::cli::util::{ConfirmResult, prompt_confirm};

    // Collect all file patterns
    let mut all_patterns = vec![file_pattern.to_string()];
    if let Some(additional) = additional_pattern {
        all_patterns.push(additional);
    }

    // Resolve all file paths
    let mut all_files: Vec<PathBuf> = Vec::new();
    for pattern in &all_patterns {
        let paths = resolve_paths(pattern, config, app);
        all_files.extend(paths);
    }

    if all_files.is_empty() {
        eprintln!("✗ No files matched the pattern(s)");
        return;
    }

    // Determine the directory for the new index
    // Use the directory of the first file, or current directory
    let index_dir = all_files
        .first()
        .and_then(|p| p.parent())
        .map(|p| p.to_path_buf())
        .unwrap_or_else(|| current_dir.to_path_buf());

    // Create index filename
    let index_filename = if index_name.ends_with(".md") {
        index_name.to_string()
    } else {
        format!("{}.md", index_name)
    };
    let index_path = index_dir.join(&index_filename);
    let index_dir = &index_path.parent().unwrap_or(&index_dir);

    if index_path.exists() {
        eprintln!("✗ Index file already exists: {}", index_path.display());
        eprintln!(
            "  Use 'diaryx w add {}' to add files to it",
            index_path.display()
        );
        return;
    }

    // Filter out the new index path from files (in case of glob matching)
    let index_canonical = index_path.canonicalize().ok();
    let all_files: Vec<_> = all_files
        .into_iter()
        .filter(|p| {
            if let Some(ref ic) = index_canonical {
                p.canonicalize().ok().as_ref() != Some(ic)
            } else {
                true
            }
        })
        .collect();

    if all_files.is_empty() {
        eprintln!("✗ No files to add after filtering");
        return;
    }

    // Find parent index for the new index (local index in that directory)
    let parent_index = block_on(ws.find_any_index_in_dir(index_dir)).ok().flatten();

    if dry_run {
        println!("Would create new index: {}", index_path.display());
        if let Some(ref parent) = parent_index {
            println!("Would add new index to parent: {}", parent.display());
        }
        println!("Would add {} file(s) to new index:", all_files.len());
        for f in &all_files {
            println!("  {}", f.display());
        }
        return;
    }

    // Confirm creation
    if !yes {
        println!(
            "Create new index '{}' with {} file(s)?",
            index_path.display(),
            all_files.len()
        );
        match prompt_confirm("Proceed?") {
            ConfirmResult::Yes | ConfirmResult::All => {}
            _ => {
                println!("Aborted");
                return;
            }
        }
    }

    // Create the index file with title
    let title = index_name.replace(['_', '-'], " ");
    let title = title
        .split_whitespace()
        .map(|word| {
            let mut chars = word.chars();
            match chars.next() {
                None => String::new(),
                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
            }
        })
        .collect::<Vec<_>>()
        .join(" ");

    // Build initial contents list
    let contents: Vec<String> = all_files
        .iter()
        .map(|f| format_workspace_link(ws, &index_path, f, None))
        .collect();

    let contents_yaml: Vec<YamlValue> = contents
        .iter()
        .map(|s| YamlValue::String(s.clone()))
        .collect();

    // Create the index file
    let index_str = index_path.to_string_lossy();

    // First create with title
    if let Err(e) =
        app.set_frontmatter_property(&index_str, "title", YamlValue::String(title.clone()))
    {
        eprintln!("✗ Error creating index file: {}", e);
        return;
    }

    // Add contents
    if let Err(e) =
        app.set_frontmatter_property(&index_str, "contents", YamlValue::Sequence(contents_yaml))
    {
        eprintln!("✗ Error setting contents: {}", e);
        return;
    }

    println!("✓ Created index '{}'", index_path.display());

    // Add part_of to new index if there's a parent
    if let Some(ref parent) = parent_index {
        let relative_parent = format_workspace_link(ws, &index_path, parent, None);
        if let Err(e) = app.set_frontmatter_property(
            &index_str,
            "part_of",
            YamlValue::String(relative_parent.clone()),
        ) {
            eprintln!("⚠ Error setting part_of in new index: {}", e);
        }

        // Add new index to parent's contents
        let parent_str = parent.to_string_lossy();
        let relative_index = format_workspace_link(ws, parent, &index_path, None);

        match app.get_frontmatter_property(&parent_str, "contents") {
            Ok(Some(YamlValue::Sequence(mut items))) => {
                items.push(YamlValue::String(relative_index.clone()));
                if let Err(e) = app.set_frontmatter_property(
                    &parent_str,
                    "contents",
                    YamlValue::Sequence(items),
                ) {
                    eprintln!("⚠ Error updating parent contents: {}", e);
                } else {
                    println!(
                        "✓ Added '{}' to parent '{}'",
                        relative_index,
                        parent.display()
                    );
                }
            }
            Ok(None) => {
                let items = vec![YamlValue::String(relative_index.clone())];
                if let Err(e) = app.set_frontmatter_property(
                    &parent_str,
                    "contents",
                    YamlValue::Sequence(items),
                ) {
                    eprintln!("⚠ Error creating parent contents: {}", e);
                } else {
                    println!(
                        "✓ Added '{}' to parent '{}'",
                        relative_index,
                        parent.display()
                    );
                }
            }
            _ => {}
        }
    }

    // Update part_of in all added files
    for file_path in &all_files {
        let file_str = file_path.to_string_lossy();
        let relative_to_index = format_workspace_link(ws, file_path, &index_path, None);

        if let Err(e) =
            app.set_frontmatter_property(&file_str, "part_of", YamlValue::String(relative_to_index))
        {
            eprintln!(
                "⚠ Error setting part_of in '{}': {}",
                file_path.display(),
                e
            );
        } else {
            println!("✓ Set part_of in '{}'", file_path.display());
        }
    }

    println!(
        "✓ Added {} file(s) to '{}'",
        all_files.len(),
        index_path.display()
    );
}

/// Handle the 'workspace add' command
/// Adds existing file(s) as children of a parent index
fn handle_add(
    app: &CliDiaryxAppSync,
    config: &Config,
    ws: &CliWorkspace,
    parent: &str,
    child_pattern: &str,
    yes: bool,
    dry_run: bool,
) {
    use crate::cli::util::{ConfirmResult, prompt_confirm};

    // Resolve parent path (should be a single file)
    let parent_paths = resolve_paths(parent, config, app);
    if parent_paths.is_empty() {
        eprintln!("✗ No files matched parent: {}", parent);
        return;
    }
    if parent_paths.len() > 1 {
        eprintln!("✗ Parent must be a single file, but matched multiple:");
        for p in &parent_paths {
            eprintln!("  {}", p.display());
        }
        return;
    }
    let parent_path = &parent_paths[0];

    // Resolve child paths (can be multiple)
    let child_paths = resolve_paths(child_pattern, config, app);
    if child_paths.is_empty() {
        eprintln!("✗ No files matched child pattern: {}", child_pattern);
        return;
    }

    // Filter out the parent from children (auto-skip)
    let parent_canonical = parent_path.canonicalize().ok();
    let child_paths: Vec<_> = child_paths
        .into_iter()
        .filter(|p| {
            let dominated = parent_canonical
                .as_ref()
                .map(|pc| p.canonicalize().ok().as_ref() == Some(pc))
                .unwrap_or(false);
            if dominated {
                println!("⚠ Skipping parent file: {}", p.display());
            }
            !dominated
        })
        .collect();

    if child_paths.is_empty() {
        eprintln!("✗ No child files to add (all matched files were skipped)");
        return;
    }

    let multiple = child_paths.len() > 1;
    let mut confirm_all = yes;

    for child_path in &child_paths {
        // Format links according to workspace config, with fallback to relative paths
        let contents_link = format_workspace_link(ws, parent_path, child_path, None);
        let part_of_link = format_workspace_link(ws, child_path, parent_path, None);

        if dry_run {
            println!(
                "Would add '{}' to contents of '{}'",
                contents_link,
                parent_path.display()
            );
            println!(
                "Would set part_of to '{}' in '{}'",
                part_of_link,
                child_path.display()
            );
            continue;
        }

        // Confirm if multiple files and not auto-confirming
        if multiple && !confirm_all {
            println!(
                "Add '{}' to '{}'?",
                child_path.display(),
                parent_path.display()
            );
            match prompt_confirm("Proceed?") {
                ConfirmResult::Yes => {}
                ConfirmResult::No => {
                    println!("Skipped");
                    continue;
                }
                ConfirmResult::All => {
                    confirm_all = true;
                }
                ConfirmResult::Quit => {
                    println!("Aborted");
                    return;
                }
            }
        }

        // Add single child
        add_single_child(app, parent_path, child_path, &contents_link, &part_of_link);
    }
}

/// Add a single child to a parent index
fn add_single_child(
    app: &CliDiaryxAppSync,
    parent_path: &Path,
    child_path: &Path,
    relative_child: &str,
    relative_parent: &str,
) {
    let parent_str = parent_path.to_string_lossy();
    let child_str = child_path.to_string_lossy();

    // Update parent's contents
    match app.get_frontmatter_property(&parent_str, "contents") {
        Ok(Some(YamlValue::Sequence(mut items))) => {
            // Check if already present
            let child_value = YamlValue::String(relative_child.to_string());
            if items.contains(&child_value) {
                println!(
                    "⚠ '{}' is already in contents of '{}'",
                    relative_child,
                    parent_path.display()
                );
                return;
            } else {
                items.push(child_value);
                if let Err(e) = app.set_frontmatter_property(
                    &parent_str,
                    "contents",
                    YamlValue::Sequence(items),
                ) {
                    eprintln!("✗ Error updating parent contents: {}", e);
                    return;
                }
                println!(
                    "✓ Added '{}' to contents of '{}'",
                    relative_child,
                    parent_path.display()
                );
            }
        }
        Ok(Some(_)) => {
            eprintln!("✗ Parent's 'contents' property is not a list");
            return;
        }
        Ok(None) => {
            // Create contents with just this child
            let items = vec![YamlValue::String(relative_child.to_string())];
            if let Err(e) =
                app.set_frontmatter_property(&parent_str, "contents", YamlValue::Sequence(items))
            {
                eprintln!("✗ Error creating parent contents: {}", e);
                return;
            }
            println!(
                "✓ Created contents with '{}' in '{}'",
                relative_child,
                parent_path.display()
            );
        }
        Err(e) => {
            eprintln!("✗ Error reading parent: {}", e);
            return;
        }
    }

    // Update child's part_of
    if let Err(e) = app.set_frontmatter_property(
        &child_str,
        "part_of",
        YamlValue::String(relative_parent.to_string()),
    ) {
        eprintln!("✗ Error updating child part_of: {}", e);
        return;
    }
    println!(
        "✓ Set part_of to '{}' in '{}'",
        relative_parent,
        child_path.display()
    );
}

/// Handle the 'workspace create' command
/// Creates a new child file under a parent index
#[allow(clippy::too_many_arguments)]
fn handle_create(
    app: &CliDiaryxAppSync,
    config: &Config,
    parent: &str,
    name: &str,
    title: Option<String>,
    description: Option<String>,
    template: Option<String>,
    is_index: bool,
    edit: bool,
) {
    // Resolve parent path
    let parent_paths = resolve_paths(parent, config, app);
    if parent_paths.is_empty() {
        eprintln!("✗ No files matched parent: {}", parent);
        return;
    }
    if parent_paths.len() > 1 {
        eprintln!("✗ Parent must be a single file, but matched multiple:");
        for p in &parent_paths {
            eprintln!("  {}", p.display());
        }
        return;
    }
    let parent_path = &parent_paths[0];

    // Determine child path (same directory as parent)
    let parent_dir = parent_path.parent().unwrap_or(Path::new("."));
    let child_filename = if name.ends_with(".md") {
        name.to_string()
    } else {
        format!("{}.md", name)
    };
    let child_path = parent_dir.join(&child_filename);

    if child_path.exists() {
        eprintln!("✗ File already exists: {}", child_path.display());
        return;
    }

    // Calculate relative paths
    let relative_child = child_filename.clone();
    let relative_parent = parent_path
        .file_name()
        .map(|f| f.to_string_lossy().to_string())
        .unwrap_or_else(|| parent_path.to_string_lossy().to_string());

    // Build title
    let display_title = title.unwrap_or_else(|| {
        // Convert filename to title (capitalize, remove extension)
        let stem = name.trim_end_matches(".md");
        prettify_filename(stem)
    });

    let template_name = template.as_deref().unwrap_or("note");
    let _filename = child_filename.trim_end_matches(".md");

    // Try to render template via the templating plugin
    #[allow(unused_labels)]
    let content = 'tmpl: {
        #[cfg(feature = "plugins")]
        {
            let workspace_root = &config.default_workspace;
            if let Ok(ctx) =
                super::plugin_loader::CliPluginContext::load(workspace_root, "diaryx.templating")
            {
                let mut custom = serde_json::Map::new();
                if let Some(ref desc) = description {
                    custom.insert(
                        "description".into(),
                        serde_json::Value::String(desc.clone()),
                    );
                }

                let params = serde_json::json!({
                    "template": template_name,
                    "title": display_title,
                    "filename": _filename,
                    "part_of": relative_parent,
                    "custom": custom,
                });

                if let Ok(result) = ctx.cmd("RenderCreationTemplate", params) {
                    if let Some(rendered) = result.as_str() {
                        let mut rendered = rendered.to_string();

                        // If this should be an index, add contents: [] to frontmatter
                        if is_index && !rendered.contains("contents:") {
                            if let Some(idx) = rendered.find("\n---\n") {
                                if rendered.starts_with("---\n") {
                                    rendered.insert_str(idx, "\ncontents: []");
                                }
                            }
                        }

                        break 'tmpl rendered;
                    }
                }
            }
        }

        // Fallback: build content manually (plugin not available)
        eprintln!(
            "Warning: Template '{}' not found, using default format",
            template_name
        );
        let mut frontmatter = format!("---\ntitle: {}\n", display_title);
        if let Some(ref desc) = description {
            frontmatter.push_str(&format!("description: {}\n", desc));
        }
        frontmatter.push_str(&format!("part_of: {}\n", relative_parent));
        if is_index {
            frontmatter.push_str("contents: []\n");
        }
        frontmatter.push_str("---\n\n");
        let body = format!("# {}\n\n", display_title);
        format!("{}{}", frontmatter, body)
    };

    // Create the file
    if let Err(e) = std::fs::write(&child_path, &content) {
        eprintln!("✗ Error creating file: {}", e);
        return;
    }
    println!("✓ Created '{}'", child_path.display());

    // Update parent's contents
    let parent_str = parent_path.to_string_lossy();
    match app.get_frontmatter_property(&parent_str, "contents") {
        Ok(Some(YamlValue::Sequence(mut items))) => {
            items.push(YamlValue::String(relative_child.clone()));
            if let Err(e) =
                app.set_frontmatter_property(&parent_str, "contents", YamlValue::Sequence(items))
            {
                eprintln!("✗ Error updating parent contents: {}", e);
                return;
            }
            println!(
                "✓ Added '{}' to contents of '{}'",
                relative_child,
                parent_path.display()
            );
        }
        Ok(Some(_)) => {
            eprintln!("⚠ Parent's 'contents' property is not a list, skipping update");
        }
        Ok(None) => {
            // Create contents with just this child
            let items = vec![YamlValue::String(relative_child.clone())];
            if let Err(e) =
                app.set_frontmatter_property(&parent_str, "contents", YamlValue::Sequence(items))
            {
                eprintln!("✗ Error creating parent contents: {}", e);
                return;
            }
            println!(
                "✓ Created contents with '{}' in '{}'",
                relative_child,
                parent_path.display()
            );
        }
        Err(e) => {
            eprintln!("⚠ Error reading parent (file was still created): {}", e);
        }
    }

    // Open in editor if requested
    if edit && let Ok(cfg) = Config::load() {
        println!("Opening: {}", child_path.display());
        if let Err(e) = launch_editor(&child_path, &cfg) {
            eprintln!("✗ Error launching editor: {}", e);
        }
    }
}

/// Handle the 'workspace remove' command
/// Removes a child from a parent's hierarchy (does not delete the file)
fn handle_remove(
    app: &CliDiaryxAppSync,
    ws: &CliWorkspace,
    config: &Config,
    parent: &str,
    child: &str,
    dry_run: bool,
) {
    // Resolve parent path
    let parent_paths = resolve_paths(parent, config, app);
    if parent_paths.is_empty() {
        eprintln!("✗ No files matched parent: {}", parent);
        return;
    }
    if parent_paths.len() > 1 {
        eprintln!("✗ Parent must be a single file, but matched multiple:");
        for p in &parent_paths {
            eprintln!("  {}", p.display());
        }
        return;
    }
    let parent_path = &parent_paths[0];

    // Resolve child path
    let child_paths = resolve_paths(child, config, app);
    if child_paths.is_empty() {
        eprintln!("✗ No files matched child: {}", child);
        return;
    }
    if child_paths.len() > 1 {
        eprintln!("✗ Child must be a single file, but matched multiple:");
        for p in &child_paths {
            eprintln!("  {}", p.display());
        }
        return;
    }
    let child_path = &child_paths[0];

    // Calculate relative path from parent to child
    let relative_child = format_workspace_link(ws, parent_path, child_path, None);

    if dry_run {
        println!(
            "Would remove '{}' from contents of '{}'",
            relative_child,
            parent_path.display()
        );
        println!("Would remove part_of from '{}'", child_path.display());
        return;
    }

    let parent_str = parent_path.to_string_lossy();
    let child_str = child_path.to_string_lossy();

    // Update parent's contents
    match app.get_frontmatter_property(&parent_str, "contents") {
        Ok(Some(YamlValue::Sequence(mut items))) => {
            let child_value = YamlValue::String(relative_child.clone());
            let original_len = items.len();
            items.retain(|item| item != &child_value);

            if items.len() == original_len {
                println!(
                    "⚠ '{}' was not in contents of '{}'",
                    relative_child,
                    parent_path.display()
                );
            } else {
                if let Err(e) = app.set_frontmatter_property(
                    &parent_str,
                    "contents",
                    YamlValue::Sequence(items),
                ) {
                    eprintln!("✗ Error updating parent contents: {}", e);
                    return;
                }
                println!(
                    "✓ Removed '{}' from contents of '{}'",
                    relative_child,
                    parent_path.display()
                );
            }
        }
        Ok(Some(_)) => {
            eprintln!("✗ Parent's 'contents' property is not a list");
            return;
        }
        Ok(None) => {
            println!(
                "⚠ Parent '{}' has no contents property",
                parent_path.display()
            );
        }
        Err(e) => {
            eprintln!("✗ Error reading parent: {}", e);
            return;
        }
    }

    // Remove child's part_of
    if let Err(e) = app.remove_frontmatter_property(&child_str, "part_of") {
        eprintln!("✗ Error removing child part_of: {}", e);
        return;
    }
    println!("✓ Removed part_of from '{}'", child_path.display());
}

/// Create a new index file in the given directory if none exists.
/// Returns the path to the created index.
fn create_new_index(app: &CliDiaryxAppSync, dir: &Path) -> Option<PathBuf> {
    // 1. Determine name
    let dir_name = dir.file_name().and_then(|n| n.to_str()).unwrap_or("index");
    let safe_name = slugify(dir_name);
    let index_name = format!("{}.md", safe_name);
    let index_path = dir.join(&index_name);

    // 2. Check existence using a local FS instance (since app.fs is private)
    let fs = RealFileSystem;
    if fs.exists(&index_path) {
        return Some(index_path);
    }

    // 3. Create file with basic frontmatter
    let title = prettify_filename(dir_name);
    let path_str = index_path.to_string_lossy();

    // Create title
    if let Err(e) =
        app.set_frontmatter_property(&path_str, "title", YamlValue::String(title.clone()))
    {
        eprintln!("Error creating index: {}", e);
        return None;
    }
    // Create contents
    if let Err(e) = app.set_frontmatter_property(&path_str, "contents", YamlValue::Sequence(vec![]))
    {
        eprintln!("Error initializing index contents: {}", e);
        return None;
    }
    // Add title header
    let _ = app.set_content(&path_str, &format!("# {}", title));

    println!("  ✓ Created new index: {}", index_path.display());
    Some(index_path)
}

/// Display a LinkFormat in snake_case (matching user input style)
fn format_link_format(format: diaryx_core::link_parser::LinkFormat) -> &'static str {
    use diaryx_core::link_parser::LinkFormat;
    match format {
        LinkFormat::MarkdownRoot => "markdown_root",
        LinkFormat::MarkdownRelative => "markdown_relative",
        LinkFormat::PlainRelative => "plain_relative",
        LinkFormat::PlainCanonical => "plain_canonical",
    }
}

/// Detect the apparent format of a link string
fn detect_link_format(link: &str) -> diaryx_core::link_parser::LinkFormat {
    use diaryx_core::link_parser::{LinkFormat, PathType, parse_link};

    let parsed = parse_link(link);
    let is_markdown = parsed.title.is_some();

    match (is_markdown, parsed.path_type) {
        (true, PathType::WorkspaceRoot) => LinkFormat::MarkdownRoot,
        (true, PathType::Relative) => LinkFormat::MarkdownRelative,
        (true, PathType::Ambiguous) => LinkFormat::MarkdownRelative, // ambiguous markdown → likely relative
        (false, PathType::WorkspaceRoot) => LinkFormat::PlainCanonical, // plain with / → canonical
        (false, PathType::Relative) => LinkFormat::PlainRelative,
        (false, PathType::Ambiguous) => LinkFormat::PlainRelative, // ambiguous plain → likely relative
    }
}

/// Handle the 'workspace convert-links' command
/// Converts all links in workspace files to a specific format
#[allow(clippy::too_many_arguments)]
fn handle_convert_links(
    workspace_override: Option<PathBuf>,
    ws: &CliWorkspace,
    config: &Option<Config>,
    current_dir: &Path,
    format: &str,
    path: Option<String>,
    dry_run: bool,
    yes: bool,
) {
    use diaryx_core::link_parser::LinkFormat;
    use std::io::{self, Write};

    // Parse the format
    let target_format = match format.to_lowercase().as_str() {
        "markdown_root" | "markdownroot" => LinkFormat::MarkdownRoot,
        "markdown_relative" | "markdownrelative" => LinkFormat::MarkdownRelative,
        "plain_relative" | "plainrelative" => LinkFormat::PlainRelative,
        "plain_canonical" | "plaincanonical" => LinkFormat::PlainCanonical,
        _ => {
            eprintln!("Invalid link format: {}", format);
            eprintln!(
                "Valid formats: markdown_root, markdown_relative, plain_relative, plain_canonical"
            );
            return;
        }
    };

    // Find workspace root
    let root_index = if let Some(override_path) = workspace_override {
        if override_path.extension().is_some_and(|ext| ext == "md") {
            override_path.clone()
        } else if let Ok(Some(root)) = block_on(ws.find_root_index_in_dir(&override_path)) {
            root
        } else {
            eprintln!("No workspace found in: {}", override_path.display());
            return;
        }
    } else if let Ok(Some(detected)) = block_on(ws.detect_workspace(current_dir)) {
        detected
    } else if let Some(cfg) = config {
        if let Ok(Some(root)) = block_on(ws.find_root_index_in_dir(&cfg.default_workspace)) {
            root
        } else {
            eprintln!("No workspace found. Run 'diaryx init' or 'diaryx workspace init' first.");
            return;
        }
    } else {
        eprintln!("No workspace found. Run 'diaryx init' or 'diaryx workspace init' first.");
        return;
    };

    // Show format info
    let configured_format = block_on(ws.get_link_format(&root_index)).unwrap_or_default();
    let target_format_str = format_link_format(target_format);
    let configured_format_str = format_link_format(configured_format);
    let format_example = match target_format {
        LinkFormat::MarkdownRoot => "[Title](/path/file.md)",
        LinkFormat::MarkdownRelative => "[Title](../file.md)",
        LinkFormat::PlainRelative => "../file.md",
        LinkFormat::PlainCanonical => "path/file.md",
    };

    println!("Convert Links");
    println!("=============");
    println!();
    println!("Workspace root: {}", root_index.display());
    println!(
        "Configured:     {} (in workspace config)",
        configured_format_str
    );
    println!("Target:         {} ({})", target_format_str, format_example);
    if let Some(ref p) = path {
        println!("Scope:          Single file: {}", p);
    } else {
        println!("Scope:          Entire workspace");
    }
    if dry_run {
        println!();
        println!("(Dry run - no changes will be made)");
    }
    println!();

    // Confirm unless -y flag or dry_run
    if !yes && !dry_run {
        print!("Proceed with conversion? [y/N] ");
        io::stdout().flush().unwrap();

        let mut input = String::new();
        if io::stdin().read_line(&mut input).is_err() {
            eprintln!("Failed to read input");
            return;
        }

        let input = input.trim().to_lowercase();
        if input != "y" && input != "yes" {
            println!("Cancelled.");
            return;
        }
    }

    // Perform the conversion using the workspace methods
    // For simplicity, we'll iterate manually here using the workspace tree
    use std::collections::HashSet;

    let tree = match block_on(ws.build_tree_with_depth(&root_index, None, &mut HashSet::new())) {
        Ok(t) => t,
        Err(e) => {
            eprintln!("Error building workspace tree: {}", e);
            return;
        }
    };

    // Get workspace root directory (parent of root index file)
    let workspace_root = root_index.parent().unwrap_or_else(|| Path::new(""));

    // If specific path provided, filter to just that file
    let files_to_process: Vec<PathBuf> = if let Some(ref file_path) = path {
        vec![PathBuf::from(file_path)]
    } else {
        // Collect all files from tree
        let mut files = Vec::new();
        collect_tree_files(&tree, &mut files);
        files
    };

    let mut files_modified = 0;
    let mut links_converted = 0;

    for file_path in &files_to_process {
        // Compute workspace-relative path for link conversion
        let relative_path = file_path
            .strip_prefix(workspace_root)
            .unwrap_or(file_path)
            .to_string_lossy()
            .to_string();

        let result = convert_file_links(
            file_path,
            &relative_path,
            target_format,
            Some(configured_format),
            dry_run,
        );
        if result.was_modified {
            files_modified += 1;
            links_converted += result.links_converted;

            // Build source format description
            let target_format_str = format_link_format(target_format);
            let source_desc = if result.source_formats.len() == 1 {
                let (format_name, _) = &result.source_formats[0];
                if format_name == target_format_str {
                    String::new() // Same format, no need to show
                } else {
                    format!(" (from {})", format_name)
                }
            } else if !result.source_formats.is_empty() {
                let formats: Vec<_> = result
                    .source_formats
                    .iter()
                    .filter(|(name, _)| name != target_format_str)
                    .map(|(name, _)| name.as_str())
                    .collect();
                if formats.is_empty() {
                    String::new()
                } else {
                    format!(" (from {})", formats.join(", "))
                }
            } else {
                String::new()
            };

            let action = if dry_run {
                "would convert"
            } else {
                "converted"
            };
            println!(
                "  {} {} link(s) in {}{}",
                action, result.links_converted, relative_path, source_desc
            );
        }
    }

    // Update workspace config (unless dry run)
    if !dry_run
        && files_modified > 0
        && let Err(e) = block_on(ws.set_link_format(&root_index, target_format))
    {
        eprintln!(
            "Warning: Failed to update workspace link_format setting: {}",
            e
        );
    }

    println!();
    if dry_run {
        println!(
            "Would modify {} file(s), {} link(s)",
            files_modified, links_converted
        );
    } else {
        println!(
            "Modified {} file(s), {} link(s)",
            files_modified, links_converted
        );
    }
}

/// Collect all file paths from a tree node
fn collect_tree_files(node: &diaryx_core::workspace::TreeNode, files: &mut Vec<PathBuf>) {
    files.push(node.path.clone());
    for child in &node.children {
        collect_tree_files(child, files);
    }
}

/// Result of converting links in a file
struct ConvertResult {
    /// Number of links converted
    links_converted: usize,
    /// Whether the file was modified
    was_modified: bool,
    /// Source formats encountered as strings (format_name -> count)
    source_formats: Vec<(String, usize)>,
}

/// Convert links in a single file
///
/// # Arguments
/// * `file_path` - Absolute path to the file (for reading/writing)
/// * `relative_path` - Workspace-relative path (for link conversion)
/// * `target_format` - The target link format
/// * `dry_run` - If true, don't write changes
fn convert_file_links(
    file_path: &Path,
    relative_path: &str,
    target_format: diaryx_core::link_parser::LinkFormat,
    source_format_hint: Option<diaryx_core::link_parser::LinkFormat>,
    dry_run: bool,
) -> ConvertResult {
    use diaryx_core::frontmatter;
    use diaryx_core::fs::FileSystem;
    use diaryx_core::link_parser;

    let fs = RealFileSystem;

    let mut result = ConvertResult {
        links_converted: 0,
        was_modified: false,
        source_formats: Vec::new(),
    };

    // Read the file
    let content = match fs.read_to_string(file_path) {
        Ok(c) => c,
        Err(_) => return result,
    };

    let parsed = match frontmatter::parse_or_empty(&content) {
        Ok(p) => p,
        Err(_) => return result,
    };

    let mut fm = parsed.frontmatter.clone();

    // Helper to track source format
    fn track_source_format(formats: &mut Vec<(String, usize)>, link_str: &str) {
        let source_format = detect_link_format(link_str);
        let format_name = format_link_format(source_format).to_string();
        if let Some(entry) = formats.iter_mut().find(|(name, _)| name == &format_name) {
            entry.1 += 1;
        } else {
            formats.push((format_name, 1));
        }
    }

    fn attachment_title(path: &str) -> String {
        Path::new(path)
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or(path)
            .to_string()
    }

    fn resolve_attachment_link_target_with_hint(
        raw_link: &str,
        from_canonical_path: &str,
        source_format_hint: Option<link_parser::LinkFormat>,
    ) -> String {
        let parsed = link_parser::parse_link(raw_link);

        if parsed.path_type == link_parser::PathType::Ambiguous {
            let current_dir = Path::new(from_canonical_path)
                .parent()
                .and_then(|parent| parent.to_str())
                .unwrap_or("");
            let plain_path_looks_canonical = !current_dir.is_empty()
                && parsed.path.starts_with(current_dir)
                && parsed
                    .path
                    .as_bytes()
                    .get(current_dir.len())
                    .is_some_and(|ch| *ch == b'/');

            if plain_path_looks_canonical {
                return link_parser::to_canonical_with_link_format(
                    &parsed,
                    Path::new(from_canonical_path),
                    Some(link_parser::LinkFormat::PlainCanonical),
                );
            }
        }

        link_parser::to_canonical_with_link_format(
            &parsed,
            Path::new(from_canonical_path),
            source_format_hint,
        )
    }

    // Convert part_of if present (can be string or array)
    if let Some(part_of_value) = fm.get("part_of") {
        if let Some(part_of_str) = part_of_value.as_str() {
            // Single string value
            let converted = link_parser::convert_link_with_hint(
                part_of_str,
                target_format,
                relative_path,
                None,
                source_format_hint,
            );
            if converted != part_of_str {
                track_source_format(&mut result.source_formats, part_of_str);
                fm.insert("part_of".to_string(), YamlValue::String(converted));
                result.links_converted += 1;
                result.was_modified = true;
            }
        } else if let Some(part_of_seq) = part_of_value.as_sequence() {
            // Array of strings
            let mut new_part_of = Vec::new();
            let mut part_of_changed = false;

            for item in part_of_seq {
                if let Some(item_str) = item.as_str() {
                    let converted = link_parser::convert_link_with_hint(
                        item_str,
                        target_format,
                        relative_path,
                        None,
                        source_format_hint,
                    );
                    if converted != item_str {
                        track_source_format(&mut result.source_formats, item_str);
                        part_of_changed = true;
                        result.links_converted += 1;
                    }
                    new_part_of.push(YamlValue::String(converted));
                } else {
                    new_part_of.push(item.clone());
                }
            }

            if part_of_changed {
                fm.insert("part_of".to_string(), YamlValue::Sequence(new_part_of));
                result.was_modified = true;
            }
        }
    }

    // Convert contents if present
    if let Some(contents_value) = fm.get("contents")
        && let Some(contents_seq) = contents_value.as_sequence()
    {
        let mut new_contents = Vec::new();
        let mut contents_changed = false;

        for item in contents_seq {
            if let Some(item_str) = item.as_str() {
                let converted = link_parser::convert_link_with_hint(
                    item_str,
                    target_format,
                    relative_path,
                    None,
                    source_format_hint,
                );
                if converted != item_str {
                    track_source_format(&mut result.source_formats, item_str);
                    contents_changed = true;
                    result.links_converted += 1;
                }
                new_contents.push(YamlValue::String(converted));
            } else {
                new_contents.push(item.clone());
            }
        }

        if contents_changed {
            fm.insert("contents".to_string(), YamlValue::Sequence(new_contents));
            result.was_modified = true;
        }
    }

    // Convert attachments if present
    if let Some(attachments_value) = fm.get("attachments")
        && let Some(attachments_seq) = attachments_value.as_sequence()
    {
        let mut new_attachments = Vec::new();
        let mut attachments_changed = false;

        for item in attachments_seq {
            if let Some(item_str) = item.as_str() {
                let parsed = link_parser::parse_link(item_str);
                let canonical_target = resolve_attachment_link_target_with_hint(
                    item_str,
                    relative_path,
                    source_format_hint,
                );
                let title = parsed
                    .title
                    .unwrap_or_else(|| attachment_title(&canonical_target));
                let converted = link_parser::format_link_with_format(
                    &canonical_target,
                    &title,
                    target_format,
                    relative_path,
                );
                if converted != item_str {
                    track_source_format(&mut result.source_formats, item_str);
                    attachments_changed = true;
                    result.links_converted += 1;
                }
                new_attachments.push(YamlValue::String(converted));
            } else {
                new_attachments.push(item.clone());
            }
        }

        if attachments_changed {
            fm.insert(
                "attachments".to_string(),
                YamlValue::Sequence(new_attachments),
            );
            result.was_modified = true;
        }
    }

    // Write the file if modified and not dry run
    if result.was_modified
        && !dry_run
        && let Ok(new_content) = frontmatter::serialize(&fm, &parsed.body)
    {
        let _ = fs.write_file(file_path, &new_content);
    }

    result
}

// ============================================================================
// Workspace Registry Commands
// ============================================================================

/// List all registered workspaces.
fn handle_list_workspaces(config: &Option<Config>) -> bool {
    let Some(cfg) = config else {
        eprintln!("No configuration found. Run 'diaryx init' first.");
        return false;
    };

    let reg = cfg.workspace_registry();

    if reg.entries.is_empty() {
        println!("No workspaces registered.");
        println!();
        println!("Register a workspace with:");
        println!("  diaryx workspace register <path>");
        return true;
    }

    println!("Registered workspaces:");
    for entry in &reg.entries {
        let is_default = reg.default_id.as_deref() == Some(&*entry.id);
        let marker = if is_default { " (default)" } else { "" };
        let path_str = entry
            .path
            .as_ref()
            .map(|p| p.display().to_string())
            .unwrap_or_else(|| "(no path)".into());
        println!("  {} — {}{}", entry.name, path_str, marker);
    }
    true
}

/// Register a workspace directory.
fn handle_register_workspace(config: &Option<Config>, path: PathBuf, name: Option<String>) -> bool {
    let Some(cfg) = config else {
        eprintln!("No configuration found. Run 'diaryx init' first.");
        return false;
    };

    let canonical = std::fs::canonicalize(&path).unwrap_or(path);
    let display_name = name.unwrap_or_else(|| {
        canonical
            .file_name()
            .map(|n| n.to_string_lossy().into_owned())
            .unwrap_or_else(|| "workspace".into())
    });

    let mut reg = cfg.workspace_registry();
    if reg.find_by_path(&canonical).is_some() {
        println!("Workspace already registered: {}", canonical.display());
        return true;
    }

    let entry = reg.register(display_name.clone(), Some(canonical.clone()));
    let id = entry.id.clone();

    // If this is the first workspace, make it the default
    if reg.entries.len() == 1 {
        reg.set_default(&id);
    }

    let mut cfg = cfg.clone();
    cfg.apply_registry(&reg);
    if let Err(e) = cfg.save() {
        eprintln!("✗ Error saving config: {}", e);
        return false;
    }

    println!(
        "✓ Registered workspace \"{}\" at {}",
        display_name,
        canonical.display()
    );
    true
}

/// Unregister a workspace by name or path.
fn handle_unregister_workspace(config: &Option<Config>, name_or_path: &str, yes: bool) -> bool {
    use std::io::{self, Write};

    let Some(cfg) = config else {
        eprintln!("No configuration found. Run 'diaryx init' first.");
        return false;
    };

    let mut reg = cfg.workspace_registry();

    // Try as name first, then as path
    let entry_id = reg
        .find_by_name(name_or_path)
        .or_else(|| {
            let path = PathBuf::from(name_or_path);
            let canonical = std::fs::canonicalize(&path).unwrap_or(path);
            reg.find_by_path(&canonical)
        })
        .map(|e| (e.id.clone(), e.name.clone()));

    let Some((id, name)) = entry_id else {
        eprintln!("✗ No registered workspace matching \"{}\"", name_or_path);
        return false;
    };

    if !yes {
        print!(
            "Unregister workspace \"{}\"? Files will not be deleted. [y/N] ",
            name
        );
        io::stdout().flush().unwrap();
        let mut input = String::new();
        if io::stdin().read_line(&mut input).is_err() || !input.trim().eq_ignore_ascii_case("y") {
            println!("Cancelled.");
            return true;
        }
    }

    reg.unregister(&id);

    let mut cfg = cfg.clone();
    cfg.apply_registry(&reg);
    if let Err(e) = cfg.save() {
        eprintln!("✗ Error saving config: {}", e);
        return false;
    }

    println!("✓ Unregistered workspace \"{}\"", name);
    true
}

/// Show or set the default workspace.
fn handle_default_workspace(config: &Option<Config>, name_or_path: Option<String>) -> bool {
    let Some(cfg) = config else {
        eprintln!("No configuration found. Run 'diaryx init' first.");
        return false;
    };

    let mut reg = cfg.workspace_registry();

    match name_or_path {
        None => {
            // Show current default
            match reg.default_entry() {
                Some(entry) => {
                    let path_str = entry
                        .path
                        .as_ref()
                        .map(|p| p.display().to_string())
                        .unwrap_or_else(|| "(no path)".into());
                    println!("{} — {}", entry.name, path_str);
                }
                None => {
                    println!("Default workspace: {}", cfg.default_workspace.display());
                    if !reg.entries.is_empty() {
                        println!("(not set — use 'diaryx workspace default <name>' to set)");
                    }
                }
            }
            true
        }
        Some(ref val) => {
            // Find by name or path
            let entry_id = reg
                .find_by_name(val)
                .or_else(|| {
                    let path = PathBuf::from(val);
                    let canonical = std::fs::canonicalize(&path).unwrap_or(path);
                    reg.find_by_path(&canonical)
                })
                .map(|e| (e.id.clone(), e.name.clone()));

            let Some((id, name)) = entry_id else {
                eprintln!("✗ No registered workspace matching \"{}\"", val);
                return false;
            };

            reg.set_default(&id);

            let mut cfg = cfg.clone();
            cfg.apply_registry(&reg);
            if let Err(e) = cfg.save() {
                eprintln!("✗ Error saving config: {}", e);
                return false;
            }

            println!("✓ Default workspace set to \"{}\"", name);
            true
        }
    }
}

/// Auto-register a workspace path in the config if not already present.
fn auto_register_workspace(path: &Path, name: Option<&str>) {
    let Ok(mut cfg) = Config::load() else {
        return;
    };

    let mut reg = cfg.workspace_registry();
    if reg.find_by_path(path).is_some() {
        return;
    }

    let display_name = name.map(|n| n.to_string()).unwrap_or_else(|| {
        path.file_name()
            .map(|n| n.to_string_lossy().into_owned())
            .unwrap_or_else(|| "workspace".into())
    });

    let entry = reg.register(display_name, Some(path.to_path_buf()));
    let id = entry.id.clone();

    // Set as default if it matches the configured default_workspace or is the first
    let default_canonical = std::fs::canonicalize(&cfg.default_workspace).ok();
    if reg.entries.len() == 1 || default_canonical.as_deref() == Some(path) {
        reg.set_default(&id);
    }

    cfg.apply_registry(&reg);
    let _ = cfg.save();
}
