//! Story field update and unblock tools.

use crate::http::context::AppContext;
use crate::http::workflow::update_story_in_file;
use crate::slog_warn;
use crate::validation::UpdateStoryRequest;
use serde_json::Value;

pub(crate) fn tool_update_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let story_id = args
        .get("story_id")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: story_id")?;

    let req = UpdateStoryRequest::from_json(args)?;

    // Explicit top-level args map onto typed CRDT registers directly (story 929:
    // no YAML front-matter writes). The `front_matter` object is the legacy
    // escape hatch; every known key is recognised and routed below, and any
    // unknown key is rejected loudly rather than silently flushed to disk.
    if let Some(ref name) = req.name
        && !crate::crdt_state::set_name(story_id, Some(name.as_ref()))
    {
        return Err(format!(
            "Story '{story_id}' not found in CRDT — name was not updated. \
             The story may not exist or may not yet be registered."
        ));
    }
    if let Some(agent) = args.get("agent").and_then(|v| v.as_str()) {
        crate::crdt_state::set_agent(story_id, agent.parse::<crate::config::AgentName>().ok());
    }
    if let Some(epic) = args.get("epic").and_then(|v| v.as_str()) {
        crate::crdt_state::set_epic(story_id, crate::crdt_state::EpicId::from_crdt_str(epic));
    }

    if let Some(obj) = args.get("front_matter").and_then(|v| v.as_object()) {
        for (key, value) in obj {
            match key.as_str() {
                "acceptance_criteria" => {
                    return Err(
                        "'acceptance_criteria' is a reserved field managed via the story body \
                         (use add_criterion / remove_criterion / edit_criterion instead)."
                            .to_string(),
                    );
                }
                "name" => {
                    let raw = value.as_str().unwrap_or("");
                    let validated = crate::validation::StoryName::parse(raw)
                        .map_err(|errs| crate::validation::format_errors_as_json(&errs))?;
                    if !crate::crdt_state::set_name(story_id, Some(validated.as_ref())) {
                        return Err(format!(
                            "Story '{story_id}' not found in CRDT — name was not updated."
                        ));
                    }
                }
                "agent" => {
                    let parsed = value
                        .as_str()
                        .and_then(|s| s.parse::<crate::config::AgentName>().ok());
                    crate::crdt_state::set_agent(story_id, parsed);
                }
                "qa" => {
                    let mode = value
                        .as_str()
                        .and_then(crate::io::story_metadata::QaMode::from_str);
                    crate::crdt_state::set_qa_mode(story_id, mode);
                }
                "epic" => {
                    let parsed = value
                        .as_str()
                        .and_then(crate::crdt_state::EpicId::from_crdt_str);
                    crate::crdt_state::set_epic(story_id, parsed);
                }
                "type" => {
                    let parsed = value
                        .as_str()
                        .and_then(crate::io::story_metadata::ItemType::from_str);
                    crate::crdt_state::set_item_type(story_id, parsed);
                }
                "depends_on" => {
                    if let Some(arr) = value.as_array() {
                        let nums: Vec<u32> = arr
                            .iter()
                            .filter_map(|v| v.as_u64().map(|n| n as u32))
                            .collect();
                        crate::crdt_state::set_depends_on(story_id, &nums);
                    } else if value.is_null() {
                        crate::crdt_state::set_depends_on(story_id, &[]);
                    }
                }
                "blocked" => {
                    // Story 934, stage 5: blocked is now a stage transition,
                    // not a raw register write. Route through the state
                    // machine so invalid sources (Done/Archived/Upcoming) are
                    // rejected and downstream subscribers see a TransitionFired.
                    let want_blocked = match value {
                        Value::Bool(b) => Some(*b),
                        Value::String(s) if s == "true" => Some(true),
                        Value::String(s) if s == "false" => Some(false),
                        _ => None,
                    };
                    if let Some(true) = want_blocked {
                        crate::agents::lifecycle::transition_to_blocked(
                            story_id,
                            "Set via update_story",
                        )?;
                    } else if let Some(false) = want_blocked {
                        crate::agents::lifecycle::transition_to_unblocked(story_id)?;
                    }
                }
                "review_hold" => {
                    let want_hold = match value {
                        Value::Bool(b) => Some(*b),
                        Value::String(s) if s == "true" => Some(true),
                        Value::String(s) if s == "false" => Some(false),
                        _ => None,
                    };
                    match want_hold {
                        Some(true) => {
                            crate::pipeline_state::apply_transition_str(
                                story_id,
                                crate::pipeline_state::PipelineEvent::ReviewHold {
                                    reason: "Set via update_story".to_string(),
                                },
                                None,
                            )?;
                        }
                        Some(false) => {
                            crate::pipeline_state::apply_transition_str(
                                story_id,
                                crate::pipeline_state::PipelineEvent::ReviewHoldCleared,
                                None,
                            )?;
                        }
                        None => {}
                    }
                }
                "frozen" => {
                    // Story 934, stage 4: frozen is an orthogonal CRDT flag.
                    // Route through the state-machine API so callers see the
                    // canonical NotFound behaviour if the story is missing.
                    let want_frozen = match value {
                        Value::Bool(b) => Some(*b),
                        Value::String(s) if s == "true" => Some(true),
                        Value::String(s) if s == "false" => Some(false),
                        _ => None,
                    };
                    match want_frozen {
                        Some(true) => crate::pipeline_state::transition_to_frozen(story_id)
                            .map_err(|e| e.to_string())?,
                        Some(false) => crate::pipeline_state::transition_to_unfrozen(story_id)
                            .map_err(|e| e.to_string())?,
                        None => {}
                    }
                }
                "retry_count" => {
                    let n = value
                        .as_i64()
                        .or_else(|| value.as_str().and_then(|s| s.parse().ok()));
                    if let Some(n) = n {
                        crate::crdt_state::set_retry_count(story_id, n);
                    }
                }
                "mergemaster_attempted" => {
                    if let Some(true) = value.as_bool() {
                        crate::pipeline_state::apply_transition_str(
                            story_id,
                            crate::pipeline_state::PipelineEvent::MergemasterAttempted,
                            None,
                        )?;
                    }
                }
                other => {
                    return Err(format!(
                        "Unknown front_matter field '{other}'. Story 929 removed the generic \
                         YAML pass-through; supported keys: name, agent, qa, epic, type, \
                         depends_on, blocked, frozen, review_hold, retry_count, \
                         mergemaster_attempted."
                    ));
                }
            }
        }
    }

    let root = ctx.state.get_project_root()?;

    // Only call update_story_in_file when there is body content to update.
    if req.user_story.is_some() || req.description.is_some() {
        update_story_in_file(
            &root,
            story_id,
            req.user_story.as_ref().map(|d| d.as_str()),
            req.description.as_ref().map(|d| d.as_str()),
        )?;
    }

    // Bug 503: warn if any depends_on in the (now updated) story points at an archived story.
    // Story 929: reads from the CRDT (was a FS-yaml scan).
    let archived_deps = crate::crdt_state::check_archived_deps_crdt(story_id);
    if !archived_deps.is_empty() {
        slog_warn!(
            "[update-story] Story '{story_id}' depends_on {archived_deps:?} which \
             are already in 6_archived. The dep will be treated as satisfied on the \
             next promotion tick. If these deps were abandoned (not cleanly completed), \
             consider removing the depends_on or keeping the story in backlog manually."
        );
        return Ok(format!(
            "Updated story '{story_id}'.\n\n\
             WARNING: depends_on {archived_deps:?} point at stories already in \
             6_archived. These deps are treated as satisfied (archived = satisfied \
             semantics), so this story may be auto-promoted from backlog immediately. \
             If the archived deps were abandoned rather than completed, remove the \
             depends_on or move the story back to backlog manually after promotion."
        ));
    }

    Ok(format!("Updated story '{story_id}'."))
}

pub(crate) fn tool_unblock_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let req = crate::validation::UnblockStoryRequest::from_json(args)?;
    let root = ctx.state.get_project_root()?;

    let result =
        crate::chat::commands::unblock::unblock_by_number(&root, req.story_id.numeric_prefix());
    if result.contains("not blocked")
        || result.contains("not found")
        || result.contains("Error")
        || result.contains("error")
    {
        Err(result)
    } else {
        Ok(result)
    }
}
