//! Handler for the `freeze` and `unfreeze` commands.
//!
//! `freeze <number>` sets `frozen: true` on the story, halting pipeline
//! advancement and auto-assign until `unfreeze <number>` clears the flag.

use super::CommandContext;
use crate::io::story_metadata::{
    clear_front_matter_field_in_content, parse_front_matter, set_front_matter_field,
};
use std::path::Path;

/// Handle the `freeze` command.
///
/// Parses `<number>` from `ctx.args`, locates the work item, and sets
/// `frozen: true` in its front matter.
pub(super) fn handle_freeze(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 Some(format!(
            "Usage: `{} freeze <number>` (e.g. `freeze 42`)",
            ctx.services.bot_name
        ));
    }
    Some(freeze_by_number(ctx.effective_root(), num_str))
}

/// Core freeze logic: find story by numeric prefix and set `frozen: true`.
///
/// Returns a Markdown-formatted response string suitable for all transports.
pub(crate) fn freeze_by_number(project_root: &Path, story_number: &str) -> String {
    let (story_id, _, _, _) =
        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.");
            }
        };

    freeze_by_story_id(&story_id)
}

fn freeze_by_story_id(story_id: &str) -> String {
    let contents = match crate::db::read_content(story_id) {
        Some(c) => c,
        None => return format!("Failed to read story content for **{story_id}**"),
    };

    let meta = match parse_front_matter(&contents) {
        Ok(m) => m,
        Err(e) => return format!("Failed to parse front matter for **{story_id}**: {e}"),
    };

    let story_name = meta.name.as_deref().unwrap_or(story_id).to_string();

    if meta.frozen == Some(true) {
        return format!("**{story_name}** ({story_id}) is already frozen.");
    }

    let updated = set_front_matter_field(&contents, "frozen", "true");

    crate::db::write_content(story_id, &updated);
    let stage = crate::pipeline_state::read_typed(story_id)
        .ok()
        .flatten()
        .map(|i| i.stage.dir_name().to_string())
        .unwrap_or_else(|| "2_current".to_string());
    crate::db::write_item_with_content(story_id, &stage, &updated);

    format!(
        "Frozen **{story_name}** ({story_id}). Pipeline advancement and auto-assign suppressed until unfrozen."
    )
}

/// Handle the `unfreeze` command.
///
/// Parses `<number>` from `ctx.args`, locates the work item, and clears the
/// `frozen` flag to resume normal pipeline behaviour.
pub(super) fn handle_unfreeze(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 Some(format!(
            "Usage: `{} unfreeze <number>` (e.g. `unfreeze 42`)",
            ctx.services.bot_name
        ));
    }
    Some(unfreeze_by_number(ctx.effective_root(), num_str))
}

/// Core unfreeze logic: find story by numeric prefix and clear `frozen` flag.
pub(crate) fn unfreeze_by_number(project_root: &Path, story_number: &str) -> String {
    let (story_id, _, _, _) =
        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.");
            }
        };

    unfreeze_by_story_id(&story_id)
}

fn unfreeze_by_story_id(story_id: &str) -> String {
    let contents = match crate::db::read_content(story_id) {
        Some(c) => c,
        None => return format!("Failed to read story content for **{story_id}**"),
    };

    let meta = match parse_front_matter(&contents) {
        Ok(m) => m,
        Err(e) => return format!("Failed to parse front matter for **{story_id}**: {e}"),
    };

    let story_name = meta.name.as_deref().unwrap_or(story_id).to_string();

    if meta.frozen != Some(true) {
        return format!("**{story_name}** ({story_id}) is not frozen. Nothing to unfreeze.");
    }

    let updated = clear_front_matter_field_in_content(&contents, "frozen");

    crate::db::write_content(story_id, &updated);
    let stage = crate::pipeline_state::read_typed(story_id)
        .ok()
        .flatten()
        .map(|i| i.stage.dir_name().to_string())
        .unwrap_or_else(|| "2_current".to_string());
    crate::db::write_item_with_content(story_id, &stage, &updated);

    format!("Unfrozen **{story_name}** ({story_id}). Normal pipeline behaviour resumed.")
}

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

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

    use super::super::{CommandDispatch, try_handle_command};

    fn freeze_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 freeze {args}"))
    }

    fn unfreeze_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 unfreeze {args}"))
    }

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

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

    #[test]
    fn freeze_command_no_args_returns_usage() {
        let tmp = tempfile::TempDir::new().unwrap();
        let output = freeze_cmd_with_root(tmp.path(), "").unwrap();
        assert!(
            output.contains("Usage"),
            "no args should show usage: {output}"
        );
    }

    #[test]
    fn unfreeze_command_no_args_returns_usage() {
        let tmp = tempfile::TempDir::new().unwrap();
        let output = unfreeze_cmd_with_root(tmp.path(), "").unwrap();
        assert!(
            output.contains("Usage"),
            "no args should show usage: {output}"
        );
    }

    #[test]
    fn freeze_command_not_found_returns_error() {
        let tmp = tempfile::TempDir::new().unwrap();
        let output = freeze_cmd_with_root(tmp.path(), "9988").unwrap();
        assert!(
            output.contains("9988") && output.contains("found"),
            "not-found message should include number and 'found': {output}"
        );
    }

    #[test]
    fn freeze_command_sets_frozen_flag() {
        let tmp = tempfile::TempDir::new().unwrap();
        crate::db::ensure_content_store();
        write_story_file(
            tmp.path(),
            "2_current",
            "9940_story_freezeme.md",
            "---\nname: Freeze Me\n---\n# Story\n",
        );
        let output = freeze_cmd_with_root(tmp.path(), "9940").unwrap();
        assert!(
            output.contains("Frozen") && output.contains("Freeze Me"),
            "should confirm freeze with story name: {output}"
        );
        let contents = crate::db::read_content("9940_story_freezeme")
            .expect("story content should be readable after freeze");
        assert!(
            contents.contains("frozen: true"),
            "frozen flag should be set: {contents}"
        );
    }

    #[test]
    fn unfreeze_command_clears_frozen_flag() {
        let tmp = tempfile::TempDir::new().unwrap();
        crate::db::ensure_content_store();
        write_story_file(
            tmp.path(),
            "2_current",
            "9941_story_frozen.md",
            "---\nname: Frozen Story\nfrozen: true\n---\n# Story\n",
        );
        let output = unfreeze_cmd_with_root(tmp.path(), "9941").unwrap();
        assert!(
            output.contains("Unfrozen") && output.contains("Frozen Story"),
            "should confirm unfreeze with story name: {output}"
        );
        let contents = crate::db::read_content("9941_story_frozen")
            .expect("story content should be readable after unfreeze");
        assert!(
            !contents.contains("frozen:"),
            "frozen flag should be removed: {contents}"
        );
    }

    #[test]
    fn unfreeze_command_not_frozen_returns_error() {
        let tmp = tempfile::TempDir::new().unwrap();
        write_story_file(
            tmp.path(),
            "2_current",
            "9942_story_notfrozen.md",
            "---\nname: Not Frozen\n---\n# Story\n",
        );
        let output = unfreeze_cmd_with_root(tmp.path(), "9942").unwrap();
        assert!(
            output.contains("not frozen"),
            "should return not-frozen error: {output}"
        );
    }

    #[test]
    fn freeze_command_already_frozen_returns_message() {
        let tmp = tempfile::TempDir::new().unwrap();
        write_story_file(
            tmp.path(),
            "2_current",
            "9943_story_alreadyfrozen.md",
            "---\nname: Already Frozen\nfrozen: true\n---\n# Story\n",
        );
        let output = freeze_cmd_with_root(tmp.path(), "9943").unwrap();
        assert!(
            output.contains("already frozen"),
            "should say already frozen: {output}"
        );
    }
}
