//! 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, AgentStatus};
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_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 {
    const STAGES: &[&str] = &[
        "1_backlog",
        "2_current",
        "3_qa",
        "4_merge",
        "5_done",
        "6_archived",
    ];

    // Find the story file across all pipeline stages.
    let mut found: Option<(std::path::PathBuf, &str, String)> = None; // (path, stage, story_id)
    'outer: for stage in STAGES {
        let dir = project_root.join(".storkit").join("work").join(stage);
        if !dir.exists() {
            continue;
        }
        if let Ok(entries) = std::fs::read_dir(&dir) {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.extension().and_then(|e| e.to_str()) != Some("md") {
                    continue;
                }
                if let Some(stem) = path
                    .file_stem()
                    .and_then(|s| s.to_str())
                    .map(|s| s.to_string())
                {
                    let file_num = stem
                        .split('_')
                        .next()
                        .filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()))
                        .unwrap_or("")
                        .to_string();
                    if file_num == story_number {
                        found = Some((path, stage, stem));
                        break 'outer;
                    }
                }
            }
        }
    }

    let (path, stage, story_id) = match found {
        Some(f) => f,
        None => {
            return format!("No story, bug, or spike with number **{story_number}** found.");
        }
    };

    // Read the human-readable name from front matter for the confirmation message.
    let story_name = std::fs::read_to_string(&path)
        .ok()
        .and_then(|contents| {
            crate::io::story_metadata::parse_front_matter(&contents)
                .ok()
                .and_then(|m| m.name)
        })
        .unwrap_or_else(|| story_id.clone());

    // Stop any running or pending agents for this story.
    let running_agents: Vec<(String, String)> = agents
        .list_agents()
        .unwrap_or_default()
        .into_iter()
        .filter(|a| {
            a.story_id == story_id
                && matches!(a.status, AgentStatus::Running | AgentStatus::Pending)
        })
        .map(|a| (a.story_id.clone(), a.agent_name.clone()))
        .collect();

    let mut stopped_agents: Vec<String> = Vec::new();
    for (sid, agent_name) in &running_agents {
        if let Err(e) = agents.stop_agent(project_root, sid, agent_name).await {
            return format!("Failed to stop agent '{agent_name}' for story {story_number}: {e}");
        }
        stopped_agents.push(agent_name.clone());
    }

    // Remove the worktree if one exists (best-effort; ignore errors).
    let _ = crate::worktree::prune_worktree_sync(project_root, &story_id);

    // Delete the story file.
    if let Err(e) = std::fs::remove_file(&path) {
        return format!("Failed to delete story {story_number}: {e}");
    }

    // Commit the deletion to git.
    let commit_msg = format!("storkit: delete {story_id}");
    let work_rel = std::path::PathBuf::from(".storkit").join("work");
    let _ = std::process::Command::new("git")
        .args(["add", "-A"])
        .arg(&work_rel)
        .current_dir(project_root)
        .output();
    let _ = std::process::Command::new("git")
        .args(["commit", "-m", &commit_msg])
        .current_dir(project_root)
        .output();

    // Build the response.
    let stage_label = stage_display_name(stage);
    let mut response = format!("Deleted **{story_name}** from **{stage_label}**.");
    if !stopped_agents.is_empty() {
        let agent_list = stopped_agents.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 pipeline stage directory name.
fn stage_display_name(stage: &str) -> &str {
    match stage {
        "1_backlog" => "backlog",
        "2_current" => "in-progress",
        "3_qa" => "QA",
        "4_merge" => "merge",
        "5_done" => "done",
        "6_archived" => "archived",
        other => other,
    }
}

/// Strip the bot mention prefix from a raw Matrix message body.
///
/// Mirrors the logic in `commands::strip_bot_mention` and `htop::strip_mention`
/// so delete detection works without depending on private symbols.
fn strip_mention<'a>(message: &'a str, bot_name: &str, bot_user_id: &str) -> &'a str {
    let trimmed = message.trim();
    if let Some(rest) = strip_prefix_ci(trimmed, bot_user_id) {
        return rest;
    }
    if let Some(localpart) = bot_user_id.split(':').next()
        && let Some(rest) = strip_prefix_ci(trimmed, localpart)
    {
        return rest;
    }
    if let Some(rest) = strip_prefix_ci(trimmed, bot_name) {
        return rest;
    }
    trimmed
}

fn strip_prefix_ci<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
    if text.len() < prefix.len() {
        return None;
    }
    if !text[..prefix.len()].eq_ignore_ascii_case(prefix) {
        return None;
    }
    let rest = &text[prefix.len()..];
    match rest.chars().next() {
        None => Some(rest),
        Some(c) if c.is_alphanumeric() || c == '-' || c == '_' => None,
        _ => Some(rest),
    }
}

// ---------------------------------------------------------------------------
// 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(".storkit").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_removes_story_file_and_confirms() {
        let tmp = tempfile::tempdir().unwrap();
        let project_root = tmp.path();

        // Init a bare git repo so the commit step doesn't fail fatally.
        std::process::Command::new("git")
            .args(["init"])
            .current_dir(project_root)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["config", "user.email", "test@test.com"])
            .current_dir(project_root)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["config", "user.name", "Test"])
            .current_dir(project_root)
            .output()
            .unwrap();

        let backlog_dir = project_root.join(".storkit").join("work").join("1_backlog");
        std::fs::create_dir_all(&backlog_dir).unwrap();
        let story_path = backlog_dir.join("42_story_some_feature.md");
        std::fs::write(&story_path, "---\nname: Some Feature\n---\n\n# Story 42\n").unwrap();

        // Initial commit so git doesn't complain about no commits.
        std::process::Command::new("git")
            .args(["add", "-A"])
            .current_dir(project_root)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["commit", "-m", "init"])
            .current_dir(project_root)
            .output()
            .unwrap();

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

        assert!(
            response.contains("Some Feature") && response.contains("backlog"),
            "unexpected response: {response}"
        );
        assert!(!story_path.exists(), "story file should have been deleted");
    }
}
