use crate::commands::expand_tilde;
use crate::search::SearchResponse;
use crate::vault::VaultEntry;
use crate::{search, vault, vault_list};
use std::path::{Path, PathBuf};
use tauri::Emitter;

use super::boundary::{with_validated_path, ValidatedPathMode};

fn collect_registered_vault_roots(vault_list: &vault_list::VaultList) -> Vec<PathBuf> {
    let mut roots = Vec::new();
    for entry in &vault_list.vaults {
        push_unique_vault_root_path(
            &mut roots,
            PathBuf::from(expand_tilde(&entry.path).into_owned()),
        );
    }

    if let Some(active_vault) = &vault_list.active_vault {
        push_unique_vault_root_path(
            &mut roots,
            PathBuf::from(expand_tilde(active_vault).into_owned()),
        );
    }

    for hidden_default in &vault_list.hidden_defaults {
        push_unique_vault_root_path(
            &mut roots,
            PathBuf::from(expand_tilde(hidden_default).into_owned()),
        );
    }

    #[cfg(not(test))]
    if let Ok(default_path) = vault::default_vault_path() {
        push_unique_vault_root_path(&mut roots, default_path);
    }

    if let Some(dev_demo_path) = local_dev_demo_vault_path() {
        push_unique_vault_root_path(&mut roots, dev_demo_path);
    }

    roots
}

fn push_unique_vault_root_path(paths: &mut Vec<PathBuf>, path: PathBuf) {
    if paths.iter().any(|existing| existing == &path) {
        return;
    }
    paths.push(path);
}

#[cfg(all(debug_assertions, not(test)))]
fn local_dev_demo_vault_path() -> Option<PathBuf> {
    let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    manifest_dir.parent().map(|root| root.join("demo-vault-v2"))
}

#[cfg(not(all(debug_assertions, not(test))))]
fn local_dev_demo_vault_path() -> Option<PathBuf> {
    None
}

fn find_registered_vault_root(path: &Path, registered_roots: &[PathBuf]) -> Option<PathBuf> {
    registered_roots
        .iter()
        .filter_map(|root| {
            let canonical_root = root.canonicalize().ok()?;
            path.starts_with(&canonical_root)
                .then_some((canonical_root.components().count(), root.clone()))
        })
        .max_by_key(|(depth, _)| *depth)
        .map(|(_, root)| root)
}

fn resolve_reload_vault_path(
    path: &Path,
    vault_path: Option<&Path>,
) -> Result<Option<PathBuf>, String> {
    if let Some(vault_path) = vault_path {
        return Ok(Some(vault_path.to_path_buf()));
    }

    if !path.is_absolute() {
        return Ok(None);
    }

    let canonical_path = match path.canonicalize() {
        Ok(canonical_path) => canonical_path,
        Err(_) => return Ok(None),
    };

    let vault_list = vault_list::load_vault_list()?;
    let registered_roots = collect_registered_vault_roots(&vault_list);
    Ok(find_registered_vault_root(
        canonical_path.as_path(),
        &registered_roots,
    ))
}

#[tauri::command]
pub fn reload_vault_entry(
    path: PathBuf,
    vault_path: Option<PathBuf>,
) -> Result<VaultEntry, String> {
    let resolved_vault_path = resolve_reload_vault_path(path.as_path(), vault_path.as_deref())?;
    let raw_path = path.to_string_lossy();
    let raw_vault_path = resolved_vault_path
        .as_ref()
        .map(|value| value.to_string_lossy().into_owned());
    with_validated_path(
        &raw_path,
        raw_vault_path.as_deref(),
        ValidatedPathMode::Existing,
        |validated_path| vault::reload_entry(Path::new(validated_path)),
    )
}

#[tauri::command]
pub async fn reload_vault(
    app_handle: tauri::AppHandle,
    path: String,
) -> Result<Vec<crate::vault::VaultEntry>, String> {
    let path = expand_tilde(&path).into_owned();
    let vault_path = Path::new(&path).to_path_buf();

    // asset_protocol scope 是渲染必需, 同步执行 (一次 OK)
    crate::sync_vault_asset_scope(&app_handle, vault_path.as_path())?;

    // ⊢ 光湖世界加载协议 v1.0 · 阶段 1 (同步, < 1 秒)
    //   只扫描必装类型 (世界入口/官方置信灯塔/全局导航地图/唤醒路径/通感语言核/技能包/语言协议)
    //   UI 拿到这些就能立刻渲染 "世界已激活" 视觉
    let path_for_phase1 = vault_path.clone();
    let essential_entries = tokio::task::spawn_blocking(move || {
        vault::scan_vault_essential_only(Path::new(&path_for_phase1))
    })
    .await
    .map_err(|e| format!("Phase 1 (essential scan) panicked: {e}"))??;

    // ⊢ 光湖世界加载协议 v1.0 · 阶段 2 (后台异步, 不阻塞 UI)
    //   完整扫描 + 建索引 + 完成后通过事件推送给前端
    let app_handle_for_phase2 = app_handle.clone();
    let path_for_phase2 = vault_path.clone();
    tokio::task::spawn(async move {
        let phase2_result = tokio::task::spawn_blocking(move || {
            let vp = Path::new(&path_for_phase2);
            vault::invalidate_cache(vp);
            vault::scan_vault_cached(vp)
        })
        .await;

        match phase2_result {
            Ok(Ok(_full_entries)) => {
                // 通知前端: 后台扫描完成, 可重新加载 UI 拿到全部 entries
                if let Err(e) = app_handle_for_phase2.emit("vault-full-scan-complete", ()) {
                    log::warn!("Failed to emit vault-full-scan-complete event: {e}");
                }
            }
            Ok(Err(scan_err)) => {
                log::warn!("Phase 2 (background full scan) failed: {scan_err}");
                if let Err(e) = app_handle_for_phase2
                    .emit("vault-full-scan-error", scan_err.clone())
                {
                    log::warn!("Failed to emit vault-full-scan-error event: {e}");
                }
            }
            Err(join_err) => {
                log::warn!("Phase 2 task panicked: {join_err}");
            }
        }
    });

    // 立刻返回必装 entries → UI < 1 秒可见
    let essential_entries = vault::filter_gitignored_entries(
        vault_path.as_path(),
        essential_entries,
        crate::settings::hide_gitignored_files_enabled(),
    );

    log::info!(
        "Reload vault phase 1 done: {} essential entries returned",
        essential_entries.len()
    );

    Ok(essential_entries)
}

#[tauri::command]
pub async fn search_vault(
    vault_path: String,
    query: String,
    limit: Option<usize>,
    exclude_frontmatter: Option<bool>,
) -> Result<SearchResponse, String> {
    let vault_path = expand_tilde(&vault_path).into_owned();
    let limit = limit.unwrap_or(20);
    let exclude_frontmatter = exclude_frontmatter.unwrap_or(false);
    tokio::task::spawn_blocking(move || {
        search::search_vault_with_options(search::SearchOptions {
            vault_path: &vault_path,
            query: &query,
            mode: "keyword",
            limit,
            hide_gitignored_files: crate::settings::hide_gitignored_files_enabled(),
            exclude_frontmatter,
        })
    })
    .await
    .map_err(|e| format!("Search task failed: {}", e))?
}

#[cfg(test)]
mod tests {
    use super::{
        collect_registered_vault_roots, find_registered_vault_root, reload_vault_entry,
        resolve_reload_vault_path, search_vault,
    };
    use crate::vault_list::{VaultEntry as VaultListEntry, VaultList};
    use std::path::{Path, PathBuf};

    fn write_note(root: &Path, name: &str, content: &str) -> std::path::PathBuf {
        let path = root.join(name);
        std::fs::write(&path, content).unwrap();
        path
    }

    #[test]
    fn finds_registered_vault_root_for_an_absolute_note_path() {
        let dir = tempfile::TempDir::new().unwrap();
        let vault_root = dir.path().join("vault");
        let note_path = vault_root.join("note.md");
        std::fs::create_dir_all(&vault_root).unwrap();
        std::fs::write(&note_path, "# Note\n").unwrap();

        let vault_list = VaultList {
            vaults: vec![VaultListEntry {
                label: "Test".to_string(),
                path: vault_root.to_string_lossy().into_owned(),
                ..Default::default()
            }],
            active_vault: None,
            default_workspace_path: None,
            hidden_defaults: vec![],
        };

        let registered_roots = collect_registered_vault_roots(&vault_list);
        let canonical_note_path = note_path.canonicalize().unwrap();

        assert_eq!(
            find_registered_vault_root(canonical_note_path.as_path(), &registered_roots),
            Some(vault_root),
        );
    }

    #[test]
    fn prefers_the_deepest_registered_vault_root() {
        let dir = tempfile::TempDir::new().unwrap();
        let parent_root = dir.path().join("vault");
        let nested_root = parent_root.join("projects");
        let note_path = nested_root.join("note.md");
        std::fs::create_dir_all(&nested_root).unwrap();
        std::fs::write(&note_path, "# Note\n").unwrap();

        let vault_list = VaultList {
            vaults: vec![
                VaultListEntry {
                    label: "Parent".to_string(),
                    path: parent_root.to_string_lossy().into_owned(),
                    ..Default::default()
                },
                VaultListEntry {
                    label: "Nested".to_string(),
                    path: nested_root.to_string_lossy().into_owned(),
                    ..Default::default()
                },
            ],
            active_vault: None,
            default_workspace_path: None,
            hidden_defaults: vec![],
        };

        let registered_roots = collect_registered_vault_roots(&vault_list);
        let canonical_note_path = note_path.canonicalize().unwrap();

        assert_eq!(
            find_registered_vault_root(canonical_note_path.as_path(), &registered_roots),
            Some(nested_root),
        );
    }

    #[test]
    fn find_registered_vault_root_ignores_missing_registered_roots() {
        let dir = tempfile::TempDir::new().unwrap();
        let vault_root = dir.path().join("vault");
        std::fs::create_dir_all(&vault_root).unwrap();
        let note_path = write_note(&vault_root, "note.md", "# Note\n");
        let registered_roots = vec![dir.path().join("missing"), vault_root.clone()];
        let canonical_note_path = note_path.canonicalize().unwrap();

        assert_eq!(
            find_registered_vault_root(canonical_note_path.as_path(), &registered_roots),
            Some(vault_root),
        );
    }

    #[test]
    fn collect_registered_vault_roots_includes_active_vault() {
        let vault_list = VaultList {
            vaults: vec![VaultListEntry {
                label: "Listed".to_string(),
                path: "/listed".to_string(),
                ..Default::default()
            }],
            active_vault: Some("/active".to_string()),
            default_workspace_path: None,
            hidden_defaults: vec![],
        };

        let roots = collect_registered_vault_roots(&vault_list);

        assert_eq!(
            roots,
            vec![PathBuf::from("/listed"), PathBuf::from("/active")]
        );
    }

    #[test]
    fn collect_registered_vault_roots_includes_hidden_defaults() {
        let vault_list = VaultList {
            vaults: vec![],
            active_vault: None,
            default_workspace_path: None,
            hidden_defaults: vec!["/hidden-default".to_string()],
        };

        let roots = collect_registered_vault_roots(&vault_list);

        assert_eq!(roots, vec![PathBuf::from("/hidden-default")]);
    }

    #[test]
    fn resolve_reload_vault_path_uses_explicit_vault_path() {
        let explicit = Path::new("/tmp/vault");

        assert_eq!(
            resolve_reload_vault_path(Path::new("note.md"), Some(explicit)).unwrap(),
            Some(explicit.to_path_buf()),
        );
    }

    #[test]
    fn resolve_reload_vault_path_skips_relative_note_paths() {
        assert_eq!(
            resolve_reload_vault_path(Path::new("note.md"), None).unwrap(),
            None,
        );
    }

    #[test]
    fn reload_vault_entry_command_reads_note_inside_vault() {
        let dir = tempfile::TempDir::new().unwrap();
        let note_path = write_note(dir.path(), "note.md", "# Reloaded Title\n\nBody");

        let entry = reload_vault_entry(note_path, Some(dir.path().to_path_buf())).unwrap();

        assert_eq!(entry.title, "Reloaded Title");
    }

    #[tokio::test]
    async fn search_vault_command_uses_default_limit_and_returns_results() {
        let dir = tempfile::Builder::new()
            .prefix("scan-search-")
            .tempdir_in(std::env::current_dir().unwrap())
            .unwrap();
        write_note(dir.path(), "search.md", "# Searchable\n\nneedle");

        let response = search_vault(
            dir.path().to_string_lossy().into_owned(),
            "needle".to_string(),
            None,
            None,
        )
        .await
        .unwrap();

        assert_eq!(response.results.len(), 1);
        assert_eq!(response.results[0].title, "Searchable");
        assert_eq!(response.mode, "keyword");
    }

    #[tokio::test]
    async fn search_vault_command_honors_explicit_limit() {
        let dir = tempfile::Builder::new()
            .prefix("scan-search-limit-")
            .tempdir_in(std::env::current_dir().unwrap())
            .unwrap();
        write_note(dir.path(), "first.md", "# First\n\nneedle");
        write_note(dir.path(), "second.md", "# Second\n\nneedle");

        let response = search_vault(
            dir.path().to_string_lossy().into_owned(),
            "needle".to_string(),
            Some(1),
            None,
        )
        .await
        .unwrap();

        assert_eq!(response.results.len(), 1);
    }
}
