use crate::{
    invocation::{SkillContext, SkillInput},
    outcome::SkillOutcome,
    registry::SkillRegistry,
    skill::{Skill, SkillId},
};
use serde_json::json;

/// Dispatch: meta-tool — invoke any bctx skill by name via a single stable schema.
/// Useful for static MCP registries that can only register one generic tool entry.
pub struct DispatchSkill;

impl Skill for DispatchSkill {
    fn id(&self) -> SkillId {
        SkillId("dispatch")
    }
    fn display_name(&self) -> &str {
        "Dispatch"
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "skill": { "type": "string", "description": "Skill ID to invoke (e.g. 'sieve', 'chisel')" },
                "input": { "type": "object", "description": "Input object forwarded to the target skill" }
            },
            "required": ["skill", "input"]
        })
    }

    fn output_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "description": "Output from the dispatched skill"
        })
    }

    fn execute(&self, input: SkillInput, ctx: &SkillContext) -> anyhow::Result<SkillOutcome> {
        let skill_id = input.get_str("skill").unwrap_or("").to_string();
        let inner_input_val = input
            .get("input")
            .cloned()
            .unwrap_or(serde_json::Value::Object(Default::default()));

        if skill_id.is_empty() {
            return Ok(SkillOutcome::failure("dispatch", "skill is required"));
        }
        if skill_id == "dispatch" {
            return Ok(SkillOutcome::failure(
                "dispatch",
                "dispatch cannot call itself",
            ));
        }

        let registry = SkillRegistry::default_registry();
        match registry.get(&skill_id) {
            Some(skill) => {
                let inner_input = SkillInput::from_value(inner_input_val);
                skill.execute(inner_input, ctx)
            }
            None => {
                let available: Vec<&str> = registry.all_ids();
                Ok(SkillOutcome::failure(
                    "dispatch",
                    &format!(
                        "unknown skill '{skill_id}' — available: {}",
                        available.join(", ")
                    ),
                ))
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::invocation::SkillContext;
    use serde_json::json;

    fn ctx() -> SkillContext {
        SkillContext::new("test-session", ".")
    }

    #[test]
    fn rejects_self_dispatch() {
        let skill = DispatchSkill;
        let input = SkillInput::from_value(json!({ "skill": "dispatch", "input": {} }));
        let outcome = skill.execute(input, &ctx()).unwrap();
        assert!(outcome.result.get("error").is_some());
    }

    #[test]
    fn rejects_empty_skill_id() {
        let skill = DispatchSkill;
        let input = SkillInput::from_value(json!({ "skill": "", "input": {} }));
        let outcome = skill.execute(input, &ctx()).unwrap();
        assert!(outcome.result.get("error").is_some());
    }

    #[test]
    fn rejects_unknown_skill() {
        let skill = DispatchSkill;
        let input = SkillInput::from_value(json!({ "skill": "nonexistent_xyz", "input": {} }));
        let outcome = skill.execute(input, &ctx()).unwrap();
        let err = outcome.result["error"].as_str().unwrap_or("");
        assert!(err.contains("nonexistent_xyz"));
    }
}
