//! Handler for the `show` command.

use super::CommandContext;
use crate::io::story_metadata::QaMode;

/// Strip a leading YAML front-matter block (`---\n...\n---`) and return the
/// remaining body text.
///
/// The front-matter block itself is no longer parsed for display — story
/// 1195 found that field extracted here (agent, depends_on, blocked,
/// retry_count, qa) can go stale relative to the CRDT registers that are the
/// actual source of truth. Metadata display now comes exclusively from
/// [`crdt_metadata_summary`].
#[allow(clippy::string_slice)] // indices from find("\n---") on ASCII delimiter; "---" and "\n---" are ASCII-only
fn strip_front_matter(text: &str) -> String {
    let trimmed = text.trim_start();
    if !trimmed.starts_with("---") {
        return text.to_string();
    }

    // Find the closing ---
    if let Some(end) = trimmed[3..].find("\n---") {
        trimmed[3 + end + 4..].to_string() // skip past closing ---
    } else {
        // No closing ---, return as-is
        text.to_string()
    }
}

/// Build the metadata summary line from CRDT registers: agent, depends_on,
/// blocked, retry count, QA mode, and merge failure detail.
///
/// Register values are the sole source here — no fallback to content-text
/// YAML, which can go stale relative to the CRDT (story 1195). Mirrors the
/// CRDT-first pattern already used by `status <number>` (`triage.rs`).
fn crdt_metadata_summary(story_id: &str) -> String {
    let Some(item) = crate::crdt_state::read_item(story_id) else {
        return String::new();
    };

    let mut parts = Vec::new();

    if matches!(
        item.stage(),
        crate::pipeline_state::Stage::Blocked { .. }
            | crate::pipeline_state::Stage::MergeFailure { .. }
            | crate::pipeline_state::Stage::MergeFailureFinal { .. }
            | crate::pipeline_state::Stage::Archived {
                reason: crate::pipeline_state::ArchiveReason::Blocked { .. },
                ..
            }
    ) {
        parts.push("**Blocked:** yes".to_string());
    }

    if let Some(agent) = item.agent() {
        parts.push(format!("**Agent:** {agent}"));
    }

    let deps = item.depends_on();
    if !deps.is_empty() {
        let nums: Vec<String> = deps.iter().map(|n| format!("#{n}")).collect();
        parts.push(format!("**Depends on:** {}", nums.join(", ")));
    }

    let rc = item.retry_count();
    if rc > 0 {
        parts.push(format!("**Retries:** {rc}"));
    }

    if let Some(QaMode::Human) = item.qa_mode() {
        parts.push("**QA:** human review required".to_string());
    }

    if let Some(job) = crate::crdt_state::read_merge_job(story_id)
        && let Some(err) = job.error
    {
        parts.push(format!("**Merge failure:** {err}"));
    }

    parts.join(" · ")
}

/// Display the full markdown text of a work item identified by its numeric ID.
///
/// Lookup priority: CRDT → content store → filesystem (Story 512).
/// Returns a friendly message when no match is found.
pub(super) fn handle_show(ctx: &CommandContext) -> Option<String> {
    let num_str = ctx.args.trim();
    if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
        return None;
    }

    // Find the story by numeric prefix: CRDT → content store.
    let (story_id, _stage_dir, _path, content) =
        match crate::chat::lookup::find_story_by_number(ctx.effective_root(), num_str) {
            Some(found) => found,
            None => {
                return Some(format!(
                    "No story, bug, or spike with number **{num_str}** found."
                ));
            }
        };

    // `content` comes from the CRDT / content store.  If unavailable, report
    // it rather than silently reading a stale on-disk copy.
    let text = content.unwrap_or_else(|| {
        format!("Story {story_id} found in pipeline but its content is unavailable.")
    });

    // Strip front matter block from the displayed body; source the metadata
    // summary separately from CRDT registers, which win over any stale
    // content-text YAML (story 1195).
    let body = strip_front_matter(&text);
    let front_matter_summary = crdt_metadata_summary(&story_id);

    // Convert markdown headings to bold text for consistent rendering across
    // Matrix clients. Element X doesn't style <h2> tags distinctly, but bold
    // text renders consistently everywhere.
    let formatted = body
        .lines()
        .map(|line| {
            let trimmed = line.trim_start();
            if let Some(rest) = trimmed.strip_prefix("### ") {
                format!("\n**{}**", rest)
            } else if let Some(rest) = trimmed.strip_prefix("## ") {
                format!("\n**{}**", rest)
            } else if let Some(rest) = trimmed.strip_prefix("# ") {
                format!("\n**{}**", rest)
            } else {
                line.to_string()
            }
        })
        .collect::<Vec<_>>()
        .join("\n");

    if front_matter_summary.is_empty() {
        Some(formatted.trim().to_string())
    } else {
        Some(format!("{front_matter_summary}\n{}", formatted.trim()))
    }
}

#[cfg(test)]
mod tests {
    use super::super::{CommandDispatch, try_handle_command};

    fn show_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
        let services = crate::services::Services::new_test(root.to_path_buf(), "Timmy".to_string());
        let room_id = "!test:example.com".to_string();
        let dispatch = CommandDispatch {
            services: &services,
            project_root: &services.project_root,
            bot_user_id: "@timmy:homeserver.local",
            room_id: &room_id,
        };
        try_handle_command(&dispatch, &format!("@timmy show {args}"))
    }

    use crate::chat::test_helpers::write_story_file;

    #[test]
    fn show_command_is_registered() {
        use super::super::commands;
        let found = commands().iter().any(|c| c.name == "show");
        assert!(found, "show command must be in the registry");
    }

    #[test]
    fn show_command_appears_in_help() {
        let result = super::super::tests::try_cmd_addressed(
            "Timmy",
            "@timmy:homeserver.local",
            "@timmy help",
        );
        let output = result.unwrap();
        assert!(
            output.contains("show"),
            "help should list show command: {output}"
        );
    }

    #[test]
    fn show_command_no_args_routes_to_timmy() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = show_cmd_with_root(tmp.path(), "");
        assert!(
            result.is_none(),
            "no args should route to LLM (None), not return a usage error: {result:?}"
        );
    }

    #[test]
    fn show_command_non_numeric_args_routes_to_timmy() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = show_cmd_with_root(tmp.path(), "me the story about the login bug");
        assert!(
            result.is_none(),
            "natural-language args should route to LLM (None): {result:?}"
        );
    }

    #[test]
    fn show_command_well_formed_runs_handler() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = show_cmd_with_root(tmp.path(), "999");
        assert!(
            result.is_some(),
            "well-formed numeric arg should run the command handler: {result:?}"
        );
    }

    #[test]
    fn show_command_not_found_returns_friendly_message() {
        let tmp = tempfile::TempDir::new().unwrap();
        let output = show_cmd_with_root(tmp.path(), "999").unwrap();
        assert!(
            output.contains("999"),
            "not-found message should include the queried number: {output}"
        );
        assert!(
            output.contains("found"),
            "not-found message should say not found: {output}"
        );
    }

    #[test]
    fn show_command_finds_story_in_backlog() {
        let tmp = tempfile::TempDir::new().unwrap();
        write_story_file(
            tmp.path(),
            "1_backlog",
            "305_story_show_command.md",
            "---\nname: Show command\n---\n\n# Story 305\n\nFull story text here.",
            None,
        );
        let output = show_cmd_with_root(tmp.path(), "305").unwrap();
        assert!(
            output.contains("Full story text here."),
            "show should return full story content: {output}"
        );
    }

    #[test]
    fn show_command_finds_story_in_current() {
        let tmp = tempfile::TempDir::new().unwrap();
        write_story_file(
            tmp.path(),
            "2_current",
            "42_story_do_something.md",
            "---\nname: Do something\n---\n\n# Story 42\n\nIn progress.",
            None,
        );
        let output = show_cmd_with_root(tmp.path(), "42").unwrap();
        assert!(
            output.contains("In progress."),
            "show should return story from current stage: {output}"
        );
    }

    #[test]
    fn show_command_finds_bug() {
        let tmp = tempfile::TempDir::new().unwrap();
        write_story_file(
            tmp.path(),
            "1_backlog",
            "7_bug_crash_on_login.md",
            "---\nname: Crash on login\n---\n\n## Symptom\n\nCrashes.",
            None,
        );
        let output = show_cmd_with_root(tmp.path(), "7").unwrap();
        assert!(
            output.contains("Symptom"),
            "show should return bug content: {output}"
        );
    }

    /// Story 1195, AC 1 + 2: `show` must source metadata from the CRDT
    /// registers, and a CRDT-only write (e.g. an agent pin via `update_story`)
    /// must be visible immediately — no content-text rewrite required.
    #[test]
    fn show_command_reflects_crdt_only_agent_pin_immediately() {
        crate::crdt_state::init_for_test();
        let tmp = tempfile::TempDir::new().unwrap();
        // Use a high story number to avoid collisions with other tests in the
        // global content store.
        write_story_file(
            tmp.path(),
            "2_current",
            "9904_story_pin_test.md",
            "---\nname: Pin Test\n---\n\n# Story\n\nBody text.",
            None,
        );
        crate::crdt_state::write_item_str(
            "9904_story_pin_test",
            "2_current",
            Some("Pin Test"),
            None,
            None,
            None,
        );

        // Set the pin via the same CRDT-only register write `update_story`
        // uses (crdt_state::set_agent) — the content body is never touched.
        crate::crdt_state::set_agent("9904_story_pin_test", "coder-1".parse().ok());

        let output = show_cmd_with_root(tmp.path(), "9904").unwrap();
        assert!(
            output.contains("**Agent:** coder-1"),
            "show should reflect the CRDT-only agent pin immediately: {output}"
        );
    }

    /// Story 1195, AC 1: register values must win over stale content-text
    /// YAML — a front-matter `agent:` value that no longer matches the CRDT
    /// register must not leak into the displayed summary.
    #[test]
    fn show_command_prefers_crdt_agent_over_stale_yaml_front_matter() {
        crate::crdt_state::init_for_test();
        let tmp = tempfile::TempDir::new().unwrap();
        write_story_file(
            tmp.path(),
            "2_current",
            "9905_story_stale_yaml.md",
            "---\nname: Stale YAML\nagent: coder-1\n---\n\n# Story\n\nBody text.",
            None,
        );
        crate::crdt_state::write_item_str(
            "9905_story_stale_yaml",
            "2_current",
            Some("Stale YAML"),
            None,
            None,
            None,
        );
        crate::crdt_state::set_agent("9905_story_stale_yaml", "coder-2".parse().ok());

        let output = show_cmd_with_root(tmp.path(), "9905").unwrap();
        assert!(
            output.contains("**Agent:** coder-2"),
            "show should display the live CRDT agent: {output}"
        );
        assert!(
            !output.contains("**Agent:** coder-1"),
            "show must not surface the stale YAML agent value: {output}"
        );
    }

    #[test]
    fn show_command_case_insensitive() {
        let result = super::super::tests::try_cmd_addressed(
            "Timmy",
            "@timmy:homeserver.local",
            "@timmy SHOW 1",
        );
        assert!(result.is_some(), "SHOW should match case-insensitively");
    }
}
