//! Start command: start a coder agent on a story.
//!
//! `{bot_name} start {number}` finds the story by number, selects the default
//! coder agent, and starts it.
//!
//! `{bot_name} start {number} opus` starts `coder-opus` (or any agent whose
//! name ends with the supplied hint, e.g. `coder-{hint}`).

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

/// A parsed start command from a Matrix message body.
#[derive(Debug, PartialEq)]
pub enum StartCommand {
    /// Start the story with this number using the (optional) agent hint.
    Start {
        story_number: String,
        /// Optional agent name hint (e.g. `"opus"` → resolved to `"coder-opus"`).
        agent_hint: Option<String>,
    },
    /// The user typed `start` but without a valid numeric argument.
    BadArgs,
}

/// Parse a start command from a raw Matrix message body.
///
/// Strips the bot mention prefix and checks whether the first word is `start`.
/// Returns `None` when the message is not a start command at all.
pub fn extract_start_command(
    message: &str,
    bot_name: &str,
    bot_user_id: &str,
) -> Option<StartCommand> {
    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("start") {
        return None;
    }

    // Split args into story number and optional agent hint.
    let (number_str, hint_str) = match args.split_once(char::is_whitespace) {
        Some((n, h)) => (n.trim(), h.trim()),
        None => (args, ""),
    };

    if !number_str.is_empty() && number_str.chars().all(|c| c.is_ascii_digit()) {
        let agent_hint = if hint_str.is_empty() {
            None
        } else {
            Some(hint_str.to_string())
        };
        Some(StartCommand::Start {
            story_number: number_str.to_string(),
            agent_hint,
        })
    } else {
        Some(StartCommand::BadArgs)
    }
}

/// Handle a start command asynchronously.
///
/// Finds the work item by `story_number` across all pipeline stages, resolves
/// the agent name from `agent_hint`, and calls `agents.start_agent`.
/// Returns a markdown-formatted response string.
pub async fn handle_start(
    bot_name: &str,
    story_number: &str,
    agent_hint: Option<&str>,
    project_root: &Path,
    agents: &AgentPool,
) -> String {
    // Find the story by numeric prefix: CRDT → content store → filesystem.
    let (story_id, _stage_dir, _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());

    // Resolve agent name: try "coder-{hint}" first, then the hint as-is.
    let resolved_agent: Option<String> = agent_hint.map(|hint| {
        let with_prefix = format!("coder-{hint}");
        // We'll pass the prefixed form; start_agent validates against config.
        // If coder- prefix is already there, don't double-prefix.
        if hint.starts_with("coder-") {
            hint.to_string()
        } else {
            with_prefix
        }
    });

    crate::slog!(
        "[matrix-bot] start command: starting story {story_id} with agent={resolved_agent:?} (bot={bot_name})"
    );

    match agents
        .start_agent(
            project_root,
            &story_id,
            resolved_agent.as_deref(),
            None,
            None,
        )
        .await
    {
        Ok(info) => {
            format!(
                "Started **{story_name}** with agent **{}**.",
                info.agent_name
            )
        }
        Err(e) if e.contains("All coder agents are busy") => {
            format!(
                "**{story_name}** has been queued in `work/2_current/` and will start \
                 automatically when a coder becomes available."
            )
        }
        Err(e) => {
            format!("Failed to start **{story_name}**: {e}")
        }
    }
}

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

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

    // -- extract_start_command -----------------------------------------------

    #[test]
    fn extract_with_full_user_id() {
        let cmd =
            extract_start_command("@timmy:home.local start 331", "Timmy", "@timmy:home.local");
        assert_eq!(
            cmd,
            Some(StartCommand::Start {
                story_number: "331".to_string(),
                agent_hint: None
            })
        );
    }

    #[test]
    fn extract_with_display_name() {
        let cmd = extract_start_command("Timmy start 42", "Timmy", "@timmy:home.local");
        assert_eq!(
            cmd,
            Some(StartCommand::Start {
                story_number: "42".to_string(),
                agent_hint: None
            })
        );
    }

    #[test]
    fn extract_with_localpart() {
        let cmd = extract_start_command("@timmy start 7", "Timmy", "@timmy:home.local");
        assert_eq!(
            cmd,
            Some(StartCommand::Start {
                story_number: "7".to_string(),
                agent_hint: None
            })
        );
    }

    #[test]
    fn extract_with_agent_hint() {
        let cmd = extract_start_command("Timmy start 331 opus", "Timmy", "@timmy:home.local");
        assert_eq!(
            cmd,
            Some(StartCommand::Start {
                story_number: "331".to_string(),
                agent_hint: Some("opus".to_string())
            })
        );
    }

    #[test]
    fn extract_case_insensitive_command() {
        let cmd = extract_start_command("Timmy START 99", "Timmy", "@timmy:home.local");
        assert_eq!(
            cmd,
            Some(StartCommand::Start {
                story_number: "99".to_string(),
                agent_hint: None
            })
        );
    }

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

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

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

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

    #[tokio::test]
    async fn handle_start_returns_not_found_for_unknown_number() {
        let tmp = tempfile::tempdir().unwrap();
        let project_root = tmp.path();
        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_start("Timmy", "999", None, project_root, &agents).await;
        assert!(
            response.contains("No story") && response.contains("999"),
            "unexpected response: {response}"
        );
    }

    #[tokio::test]
    async fn handle_start_says_queued_not_error_when_all_coders_busy() {
        use crate::agents::{AgentPool, AgentStatus};
        use std::sync::Arc;

        let tmp = tempfile::tempdir().unwrap();
        let project_root = tmp.path();
        let sk = project_root.join(".huskies");
        std::fs::create_dir_all(&sk).unwrap();
        std::fs::write(
            sk.join("project.toml"),
            "[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n",
        )
        .unwrap();

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

        let agents = Arc::new(AgentPool::new_test(3000));
        agents.inject_test_agent("other-story", "coder-1", AgentStatus::Running);

        let response = handle_start("Timmy", "9976", None, project_root, &agents).await;

        assert!(
            !response.contains("Failed"),
            "response must not say 'Failed' when coders are busy: {response}"
        );
        assert!(
            response.to_lowercase().contains("queue")
                || response.to_lowercase().contains("available"),
            "response must mention queued/available state: {response}"
        );
    }

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

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

    #[test]
    fn start_command_falls_through_to_none_in_registry() {
        // The start handler in the registry returns None (handled async in bot.rs).
        let result = crate::chat::commands::tests::try_cmd_addressed(
            "Timmy",
            "@timmy:homeserver.local",
            "@timmy start 42",
        );
        assert!(
            result.is_none(),
            "start should not produce a sync response (handled async): {result:?}"
        );
    }
}
