//! Delete command: remove a story/bug/spike from the pipeline.
//!
//! `{bot_name} delete {number}` finds the work item by number across all pipeline
//! stages, stops any running agent, removes the worktree, deletes the file, and
//! commits the change to git.

use crate::agents::AgentPool;
use crate::chat::util::strip_bot_mention;
use std::path::Path;

/// A parsed delete command from a Matrix message body.
#[derive(Debug, PartialEq)]
pub enum DeleteCommand {
    /// Delete the story with this number (digits only, e.g. `"42"`).
    Delete { story_number: String },
    /// The user typed `delete` but without a valid numeric argument.
    BadArgs,
}

/// Parse a delete command from a raw Matrix message body.
///
/// Strips the bot mention prefix and checks whether the first word is `delete`.
/// Returns `None` when the message is not a delete command at all.
pub fn extract_delete_command(
    message: &str,
    bot_name: &str,
    bot_user_id: &str,
) -> Option<DeleteCommand> {
    let stripped = strip_bot_mention(message, bot_name, bot_user_id);
    let trimmed = stripped
        .trim()
        .trim_start_matches(|c: char| !c.is_alphanumeric());

    let (cmd, args) = match trimmed.split_once(char::is_whitespace) {
        Some((c, a)) => (c, a.trim()),
        None => (trimmed, ""),
    };

    if !cmd.eq_ignore_ascii_case("delete") {
        return None;
    }

    if !args.is_empty() && args.chars().all(|c| c.is_ascii_digit()) {
        Some(DeleteCommand::Delete {
            story_number: args.to_string(),
        })
    } else {
        Some(DeleteCommand::BadArgs)
    }
}

/// Handle a delete command asynchronously.
///
/// Finds the work item by `story_number` across all pipeline stages, stops any
/// running agent, removes the worktree, deletes the file, and commits to git.
/// Returns a markdown-formatted response string.
pub async fn handle_delete(
    bot_name: &str,
    story_number: &str,
    project_root: &Path,
    agents: &AgentPool,
) -> String {
    // Find the story by numeric prefix: CRDT → content store → filesystem.
    let (story_id, stage, _path, _content) =
        match crate::chat::lookup::find_story_by_number(project_root, story_number) {
            Some(found) => found,
            None => {
                return format!("No story, bug, or spike with number **{story_number}** found.");
            }
        };

    // Story name comes from the CRDT name register (story 929).
    let story_name = crate::crdt_state::read_item(&story_id)
        .map(|w| w.name().to_string())
        .unwrap_or_else(|| story_id.clone());

    let outcome = match crate::service::work_item::delete::delete_work_item(
        &story_id,
        project_root,
        agents,
        None,
    )
    .await
    {
        Ok(o) => o,
        Err(e) => return e,
    };

    // Build the response.
    let stage_label = match crate::pipeline_state::Stage::from_dir(&stage) {
        Some(s) => stage_display_label(&s),
        None => stage.as_str(),
    };
    let mut response = format!("Deleted **{story_name}** from **{stage_label}**.");
    if !outcome.agents_stopped.is_empty() {
        let agent_list = outcome.agents_stopped.join(", ");
        response.push_str(&format!(" Stopped agent(s): {agent_list}."));
    }

    crate::slog!("[matrix-bot] delete command: removed {story_id} from {stage} (bot={bot_name})");

    response
}

/// Human-readable label for a typed pipeline [`Stage`].
fn stage_display_label(stage: &crate::pipeline_state::Stage) -> &'static str {
    use crate::pipeline_state::Stage;
    match stage {
        Stage::Upcoming => "upcoming",
        Stage::Backlog => "backlog",
        Stage::Coding { .. } => "in-progress",
        Stage::Blocked { .. } => "blocked",
        Stage::Qa => "QA",
        Stage::Merge { .. } => "merge",
        Stage::Done { .. } => "done",
        Stage::Archived { .. } => "archived",
        Stage::MergeFailure { .. } => "merge-failure",
        Stage::MergeFailureFinal { .. } => "merge-failure-final",
        Stage::Frozen { .. } => "frozen",
        Stage::ReviewHold { .. } => "review-hold",
        Stage::Abandoned { .. } => "abandoned",
        Stage::Superseded { .. } => "superseded",
        Stage::Rejected { .. } => "rejected",
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;

    // -- extract_delete_command ---------------------------------------------

    #[test]
    fn extract_with_full_user_id() {
        let cmd =
            extract_delete_command("@timmy:home.local delete 42", "Timmy", "@timmy:home.local");
        assert_eq!(
            cmd,
            Some(DeleteCommand::Delete {
                story_number: "42".to_string()
            })
        );
    }

    #[test]
    fn extract_with_display_name() {
        let cmd = extract_delete_command("Timmy delete 310", "Timmy", "@timmy:home.local");
        assert_eq!(
            cmd,
            Some(DeleteCommand::Delete {
                story_number: "310".to_string()
            })
        );
    }

    #[test]
    fn extract_with_localpart() {
        let cmd = extract_delete_command("@timmy delete 7", "Timmy", "@timmy:home.local");
        assert_eq!(
            cmd,
            Some(DeleteCommand::Delete {
                story_number: "7".to_string()
            })
        );
    }

    #[test]
    fn extract_case_insensitive_command() {
        let cmd = extract_delete_command("Timmy DELETE 99", "Timmy", "@timmy:home.local");
        assert_eq!(
            cmd,
            Some(DeleteCommand::Delete {
                story_number: "99".to_string()
            })
        );
    }

    #[test]
    fn extract_no_args_is_bad_args() {
        let cmd = extract_delete_command("Timmy delete", "Timmy", "@timmy:home.local");
        assert_eq!(cmd, Some(DeleteCommand::BadArgs));
    }

    #[test]
    fn extract_non_numeric_arg_is_bad_args() {
        let cmd = extract_delete_command("Timmy delete foo", "Timmy", "@timmy:home.local");
        assert_eq!(cmd, Some(DeleteCommand::BadArgs));
    }

    #[test]
    fn extract_non_delete_command_returns_none() {
        let cmd = extract_delete_command("Timmy help", "Timmy", "@timmy:home.local");
        assert_eq!(cmd, None);
    }

    #[test]
    fn extract_no_bot_prefix_returns_none() {
        let cmd = extract_delete_command("delete 42", "Timmy", "@timmy:home.local");
        // Without mention prefix the raw text is "delete 42" — cmd is "delete", args "42"
        // strip_mention returns the full trimmed text when no prefix matches,
        // so this is a valid delete command addressed to no-one (ambient mode).
        assert_eq!(
            cmd,
            Some(DeleteCommand::Delete {
                story_number: "42".to_string()
            })
        );
    }

    // -- handle_delete (integration-style, uses temp filesystem) -----------

    #[tokio::test]
    async fn handle_delete_returns_not_found_for_unknown_number() {
        let tmp = tempfile::tempdir().unwrap();
        let project_root = tmp.path();
        // Create the pipeline directories.
        for stage in &[
            "1_backlog",
            "2_current",
            "3_qa",
            "4_merge",
            "5_done",
            "6_archived",
        ] {
            std::fs::create_dir_all(project_root.join(".huskies").join("work").join(stage))
                .unwrap();
        }
        let agents = std::sync::Arc::new(crate::agents::AgentPool::new_test(3000));
        let response = handle_delete("Timmy", "999", project_root, &agents).await;
        assert!(
            response.contains("No story") && response.contains("999"),
            "unexpected response: {response}"
        );
    }

    #[tokio::test]
    async fn handle_delete_writes_crdt_tombstone() {
        // Initialise the global CRDT singleton (no-op if already done).
        crate::crdt_state::init_for_test();

        let story_id = "9977_story_crdt_tombstone_check";
        let story_number = "9977";

        // Seed in CRDT.
        crate::crdt_state::write_item_str(
            story_id,
            "1_backlog",
            Some("CRDT Tombstone Check"),
            None,
            None,
            None,
        );

        // Seed in content store so find_story_by_number can resolve it.
        crate::db::ensure_content_store();
        crate::db::write_item_with_content(
            story_id,
            "1_backlog",
            "---\nname: CRDT Tombstone Check\n---\n\n# Story 9977\n",
            crate::db::ItemMeta::named("CRDT Tombstone Check"),
        );

        let tmp = tempfile::tempdir().unwrap();
        let project_root = tmp.path();
        let agents = std::sync::Arc::new(crate::agents::AgentPool::new_test(3002));
        handle_delete("Timmy", story_number, project_root, &agents).await;

        // The CRDT dump includes tombstoned entries — verify is_deleted = true.
        let dump = crate::crdt_state::dump_crdt_state(Some(story_id));
        let deleted = dump
            .items
            .iter()
            .any(|i| i.story_id.as_deref() == Some(story_id) && i.is_deleted);
        assert!(
            deleted,
            "CRDT must show is_deleted=true for '{story_id}' after handle_delete"
        );
    }

    #[tokio::test]
    async fn handle_delete_removes_story_file_and_confirms() {
        let tmp = tempfile::tempdir().unwrap();
        let project_root = tmp.path();

        // Seed the story in the content store + CRDT (no filesystem needed).
        crate::db::ensure_content_store();
        crate::db::write_item_with_content(
            "9975_story_some_feature",
            "1_backlog",
            "---\nname: Some Feature\n---\n\n# Story 9975\n",
            crate::db::ItemMeta::named("Some Feature"),
        );

        let agents = std::sync::Arc::new(crate::agents::AgentPool::new_test(3000));
        let response = handle_delete("Timmy", "9975", project_root, &agents).await;

        assert!(
            response.contains("Some Feature") && response.contains("backlog"),
            "unexpected response: {response}"
        );
        assert!(
            crate::db::read_content(crate::db::ContentKey::Story("9975_story_some_feature"))
                .is_none(),
            "content store should no longer contain the deleted story"
        );
    }
}
