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

/// Steward: session role management — list/set role, show budget, enforce per-role limits.
pub struct StewardSkill;

#[derive(Debug, Clone, Copy)]
struct RolePolicy {
    token_budget: u64,
    shell_stance: &'static str,
}

static ROLES: &[(&str, RolePolicy)] = &[
    (
        "coder",
        RolePolicy {
            token_budget: 500_000,
            shell_stance: "full",
        },
    ),
    (
        "reviewer",
        RolePolicy {
            token_budget: 200_000,
            shell_stance: "restricted",
        },
    ),
    (
        "debugger",
        RolePolicy {
            token_budget: 1_000_000,
            shell_stance: "full",
        },
    ),
    (
        "ops",
        RolePolicy {
            token_budget: 300_000,
            shell_stance: "full",
        },
    ),
    (
        "admin",
        RolePolicy {
            token_budget: u64::MAX,
            shell_stance: "full",
        },
    ),
];

impl Skill for StewardSkill {
    fn id(&self) -> SkillId {
        SkillId("steward")
    }
    fn display_name(&self) -> &str {
        "Steward"
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["list", "get", "set", "budget"],
                    "description": "list: show all roles · get: current role · set: change role · budget: show token budget"
                },
                "role": { "type": "string", "description": "Role name (for action=set)" },
                "tokens_used": { "type": "integer", "description": "Tokens used this session (for action=budget)" }
            },
            "required": ["action"]
        })
    }

    fn output_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "roles": { "type": "array" },
                "current_role": { "type": "string" },
                "token_budget": { "type": "integer" },
                "tokens_used": { "type": "integer" },
                "tokens_remaining": { "type": "integer" },
                "shell_stance": { "type": "string" },
                "budget_status": { "type": "string" }
            }
        })
    }

    fn execute(&self, input: SkillInput, _ctx: &SkillContext) -> anyhow::Result<SkillOutcome> {
        let action = input.get_str("action").unwrap_or("list");

        match action {
            "list" => {
                let roles: Vec<serde_json::Value> = ROLES
                    .iter()
                    .map(|(name, policy)| {
                        json!({
                            "role": name,
                            "token_budget": if policy.token_budget == u64::MAX { serde_json::Value::String("unlimited".into()) } else { json!(policy.token_budget) },
                            "shell_stance": policy.shell_stance
                        })
                    })
                    .collect();
                Ok(SkillOutcome::success("steward", json!({ "roles": roles })))
            }
            "get" => {
                let role = load_current_role();
                let policy = role_policy(&role);
                Ok(SkillOutcome::success(
                    "steward",
                    json!({
                        "current_role": role,
                        "token_budget": policy.map(|p| if p.token_budget == u64::MAX { "unlimited".to_string() } else { p.token_budget.to_string() }).unwrap_or_default(),
                        "shell_stance": policy.map(|p| p.shell_stance).unwrap_or("unknown")
                    }),
                ))
            }
            "set" => {
                let new_role = input.get_str("role").unwrap_or("coder").to_string();
                if ROLES.iter().any(|(n, _)| *n == new_role.as_str()) {
                    save_current_role(&new_role);
                    let policy = role_policy(&new_role).unwrap();
                    Ok(SkillOutcome::success(
                        "steward",
                        json!({
                            "current_role": new_role,
                            "token_budget": if policy.token_budget == u64::MAX { "unlimited".to_string() } else { policy.token_budget.to_string() },
                            "shell_stance": policy.shell_stance
                        }),
                    ))
                } else {
                    Ok(SkillOutcome::failure(
                        "steward",
                        &format!("unknown role '{new_role}' — use list to see valid roles"),
                    ))
                }
            }
            "budget" => {
                let role = load_current_role();
                let policy = role_policy(&role);
                let tokens_used = input.get_u64("tokens_used").unwrap_or(0);
                let budget = policy.map(|p| p.token_budget).unwrap_or(500_000);
                let remaining = budget.saturating_sub(tokens_used);
                let pct = if budget == u64::MAX {
                    0.0
                } else {
                    tokens_used as f64 / budget as f64 * 100.0
                };
                let status = if budget == u64::MAX {
                    "unlimited"
                } else if pct >= 100.0 {
                    "exhausted"
                } else if pct >= 80.0 {
                    "warning"
                } else {
                    "ok"
                };
                Ok(SkillOutcome::success(
                    "steward",
                    json!({
                        "current_role": role,
                        "token_budget": if budget == u64::MAX { serde_json::Value::String("unlimited".into()) } else { json!(budget) },
                        "tokens_used": tokens_used,
                        "tokens_remaining": if budget == u64::MAX { serde_json::Value::String("unlimited".into()) } else { json!(remaining) },
                        "budget_status": status
                    }),
                ))
            }
            _ => Ok(SkillOutcome::failure("steward", "unknown action")),
        }
    }
}

fn role_policy(role: &str) -> Option<&'static RolePolicy> {
    ROLES.iter().find(|(n, _)| *n == role).map(|(_, p)| p)
}

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

    #[test]
    fn coder_policy_has_500k_budget() {
        let p = role_policy("coder").unwrap();
        assert_eq!(p.token_budget, 500_000);
    }

    #[test]
    fn reviewer_has_restricted_shell() {
        let p = role_policy("reviewer").unwrap();
        assert_eq!(p.shell_stance, "restricted");
    }

    #[test]
    fn debugger_has_1m_budget() {
        let p = role_policy("debugger").unwrap();
        assert_eq!(p.token_budget, 1_000_000);
    }

    #[test]
    fn ops_has_full_shell() {
        let p = role_policy("ops").unwrap();
        assert_eq!(p.shell_stance, "full");
    }

    #[test]
    fn admin_has_unlimited_budget() {
        let p = role_policy("admin").unwrap();
        assert_eq!(p.token_budget, u64::MAX);
    }

    #[test]
    fn unknown_role_returns_none() {
        assert!(role_policy("superuser").is_none());
        assert!(role_policy("").is_none());
    }

    #[test]
    fn all_five_roles_are_defined() {
        for name in &["coder", "reviewer", "debugger", "ops", "admin"] {
            assert!(role_policy(name).is_some(), "missing role: {name}");
        }
    }
}

fn load_current_role() -> String {
    std::env::var("HOME")
        .ok()
        .map(|h| std::path::PathBuf::from(h).join(".bctx").join("role"))
        .and_then(|p| std::fs::read_to_string(p).ok())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
        .unwrap_or_else(|| "coder".to_string())
}

fn save_current_role(role: &str) {
    if let Ok(home) = std::env::var("HOME") {
        let path = std::path::PathBuf::from(home).join(".bctx").join("role");
        let _ = std::fs::create_dir_all(path.parent().unwrap());
        let _ = std::fs::write(path, role);
    }
}
