//! Handler for the `freeze` and `unfreeze` commands.
//!
//! `freeze <number>` transitions the story to `Stage::Frozen`, halting pipeline
//! advancement and auto-assign until `unfreeze <number>` restores the prior stage.

use super::CommandContext;
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 None;
    }
    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 story_name = resolve_story_name(story_id);

    match crate::service::work_item::freeze::freeze(story_id) {
        Ok(crate::service::work_item::FreezeStatus::AlreadyFrozen) => {
            format!("**{story_name}** ({story_id}) is already frozen.")
        }
        Ok(crate::service::work_item::FreezeStatus::Frozen) => format!(
            "Frozen **{story_name}** ({story_id}). Pipeline advancement and auto-assign suppressed until unfrozen."
        ),
        Err(e) => format!("Failed to freeze **{story_name}** ({story_id}): {e}"),
    }
}

/// 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 None;
    }
    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 story_name = resolve_story_name(story_id);

    match crate::service::work_item::freeze::unfreeze(story_id) {
        Ok(crate::service::work_item::UnfreezeStatus::NotFrozen) => {
            format!("**{story_name}** ({story_id}) is not frozen. Nothing to unfreeze.")
        }
        Ok(crate::service::work_item::UnfreezeStatus::Unfrozen) => {
            format!("Unfrozen **{story_name}** ({story_id}). Normal pipeline behaviour resumed.")
        }
        Err(e) => format!("Failed to unfreeze **{story_name}** ({story_id}): {e}"),
    }
}

/// Look up the display name for a story from the CRDT name register
/// (story 929 — CRDT is the sole source of story metadata).
///
/// Falls back to `story_id` if no CRDT entry exists.
fn resolve_story_name(story_id: &str) -> String {
    crate::crdt_state::read_item(story_id)
        .map(|w| w.name().to_string())
        .unwrap_or_else(|| story_id.to_string())
}

// ---------------------------------------------------------------------------
// 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_routes_to_timmy() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = freeze_cmd_with_root(tmp.path(), "");
        assert!(
            result.is_none(),
            "no args should route to LLM (None): {result:?}"
        );
    }

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

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

    #[test]
    fn unfreeze_command_no_args_routes_to_timmy() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = unfreeze_cmd_with_root(tmp.path(), "");
        assert!(
            result.is_none(),
            "no args should route to LLM (None): {result:?}"
        );
    }

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

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

    #[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_stage_to_frozen() {
        let tmp = tempfile::TempDir::new().unwrap();
        crate::crdt_state::init_for_test();
        crate::db::ensure_content_store();
        write_story_file(
            tmp.path(),
            "2_current",
            "9940_story_freezeme.md",
            "# Story\n",
            Some("Freeze Me"),
        );
        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 item = crate::pipeline_state::read_typed("9940_story_freezeme")
            .expect("read_typed should succeed")
            .expect("item should be present");
        assert!(
            matches!(item.stage, crate::pipeline_state::Stage::Frozen { .. }),
            "stage should be Frozen after freeze: {:?}",
            item.stage
        );
    }

    #[test]
    fn unfreeze_command_restores_prior_stage() {
        let tmp = tempfile::TempDir::new().unwrap();
        crate::crdt_state::init_for_test();
        crate::db::ensure_content_store();
        write_story_file(
            tmp.path(),
            "2_current",
            "9941_story_frozen.md",
            "# Story\n",
            Some("Frozen Story"),
        );
        // Freeze first.
        let freeze_out = freeze_cmd_with_root(tmp.path(), "9941").unwrap();
        assert!(
            freeze_out.contains("Frozen"),
            "should confirm freeze: {freeze_out}"
        );
        // Now unfreeze.
        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 item = crate::pipeline_state::read_typed("9941_story_frozen")
            .expect("read_typed should succeed")
            .expect("item should be present");
        assert!(
            matches!(item.stage, crate::pipeline_state::Stage::Coding { .. }),
            "stage should be restored to Coding: {:?}",
            item.stage
        );
    }

    #[test]
    fn unfreeze_command_not_frozen_returns_error() {
        let tmp = tempfile::TempDir::new().unwrap();
        crate::crdt_state::init_for_test();
        crate::db::ensure_content_store();
        write_story_file(
            tmp.path(),
            "2_current",
            "9942_story_notfrozen.md",
            "---\nname: Not Frozen\n---\n# Story\n",
            None,
        );
        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();
        crate::crdt_state::init_for_test();
        crate::db::ensure_content_store();
        write_story_file(
            tmp.path(),
            "2_current",
            "9943_story_alreadyfrozen.md",
            "---\nname: Already Frozen\n---\n# Story\n",
            Some("Already Frozen"),
        );
        // Freeze it first.
        freeze_cmd_with_root(tmp.path(), "9943").unwrap();
        // Try to freeze again.
        let output = freeze_cmd_with_root(tmp.path(), "9943").unwrap();
        assert!(
            output.contains("already frozen"),
            "should say already frozen: {output}"
        );
    }
}
