//! Shared story-lookup helper for chat commands.
//!
//! All chat commands that need to find a work item by its numeric prefix
//! use [`find_story_by_number`].  The lookup reads from:
//!
//! 1. **CRDT** — authoritative in-memory state.
//! 2. **Content store / pipeline_items** — in-memory mirror of the
//!    `pipeline_items` table; catches items that are in the DB but whose
//!    CRDT entry hasn't been synced yet.

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

/// Locate a work item by its numeric ID prefix.
///
/// Returns `(story_id, stage_dir, path, content)` where:
/// - `story_id` — full stem, e.g. `"503_story_some_feature"`
/// - `stage_dir` — pipeline directory name, e.g. `"4_merge"`
/// - `path` — canonical path under `.huskies/work/`; may not exist on disk
///   for CRDT-only stories
/// - `content` — markdown body from the content store when available;
///   for the filesystem fallback this is the file contents read from disk;
///   `None` only when the story was found in CRDT but has no content entry
///   (callers may fall back to `fs::read_to_string(&path)` in that case)
///
/// Returns `None` when no matching work item is found by any mechanism.
pub(crate) fn find_story_by_number(
    project_root: &Path,
    number: &str,
) -> Option<(String, String, PathBuf, Option<String>)> {
    // ── 1. CRDT (authoritative) ──────────────────────────────────────────
    // `read_all_items` returns None only when the CRDT layer is not yet
    // initialised (e.g. in unit tests or very early startup).
    if let Some(items) = crate::crdt_state::read_all_items() {
        for item in items {
            if item.story_id().split('_').next().unwrap_or("") == number {
                let stage_dir = item.stage().dir_name().to_string();
                let path = project_root
                    .join(".huskies")
                    .join("work")
                    .join(&stage_dir)
                    .join(format!("{}.md", item.story_id()));
                let content =
                    crate::db::read_content(crate::db::ContentKey::Story(item.story_id()));
                return Some((item.story_id().to_string(), stage_dir, path, content));
            }
        }
    }

    // ── 2. Content store + CRDT stage lookup ────────────────────────────
    // Handles the edge case where an item is in the content store but was
    // somehow missing from the CRDT iteration above (e.g. concurrent write
    // or CRDT not yet initialised, such as in unit tests).
    for id in crate::db::all_content_ids() {
        if id.split('_').next().unwrap_or("") != number {
            continue;
        }
        let stage_dir = crate::crdt_state::read_item(&id)
            .map(|v| v.stage().dir_name().to_string())
            .unwrap_or_else(|| "backlog".to_string());
        let path = project_root
            .join(".huskies")
            .join("work")
            .join(&stage_dir)
            .join(format!("{id}.md"));
        let content = crate::db::read_content(crate::db::ContentKey::Story(&id));
        return Some((id, stage_dir, path, content));
    }

    None
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::chat::test_helpers::write_story_file;

    #[test]
    fn not_found_returns_none() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = find_story_by_number(tmp.path(), "999");
        assert!(
            result.is_none(),
            "should return None when story is not found"
        );
    }

    #[test]
    fn finds_story_in_content_store() {
        let tmp = tempfile::TempDir::new().unwrap();
        write_story_file(
            tmp.path(),
            "1_backlog",
            "9970_story_some_feature.md",
            "---\nname: Some Feature\n---\n\n# Story 9970\n",
            None,
        );
        let (story_id, _stage_dir, path, content) =
            find_story_by_number(tmp.path(), "9970").expect("should find story 9970");
        assert_eq!(story_id, "9970_story_some_feature");
        assert!(
            path.ends_with("9970_story_some_feature.md"),
            "unexpected path: {path:?}"
        );
        assert!(
            content.as_deref().unwrap_or("").contains("Some Feature"),
            "content should include story text"
        );
    }

    #[test]
    fn finds_bug_by_number() {
        let tmp = tempfile::TempDir::new().unwrap();
        write_story_file(
            tmp.path(),
            "2_current",
            "7_bug_crash_on_login.md",
            "---\nname: Crash on login\n---\n",
            None,
        );
        let (story_id, _stage_dir, _, _) =
            find_story_by_number(tmp.path(), "7").expect("should find bug 7");
        assert_eq!(story_id, "7_bug_crash_on_login");
    }

    #[test]
    fn numeric_prefix_must_match_exactly() {
        let tmp = tempfile::TempDir::new().unwrap();
        // Story 9971 exists; searching for "99710" must not match "9971_story_foo".
        write_story_file(
            tmp.path(),
            "1_backlog",
            "9971_story_foo.md",
            "---\nname: Foo\n---\n",
            None,
        );
        let result = find_story_by_number(tmp.path(), "99710");
        assert!(result.is_none(), "number 99710 should not match story 9971");
    }

    #[test]
    fn returned_path_points_into_project_root() {
        let tmp = tempfile::TempDir::new().unwrap();
        write_story_file(
            tmp.path(),
            "4_merge",
            "503_story_migration.md",
            "---\nname: Migration\n---\n",
            None,
        );
        let (_, _, path, _) =
            find_story_by_number(tmp.path(), "503").expect("should find story 503");
        assert!(
            path.starts_with(tmp.path()),
            "path should be under the project root"
        );
    }
}
