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

/// Arbiter: structured code review — given a diff or file range, return findings.
pub struct ArbiterSkill;

impl Skill for ArbiterSkill {
    fn id(&self) -> SkillId {
        SkillId("arbiter")
    }
    fn display_name(&self) -> &str {
        "Arbiter"
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "content": { "type": "string", "description": "Diff or file content to review" },
                "path": { "type": "string", "description": "File path (alternative to content)" },
                "focus": {
                    "type": "string",
                    "enum": ["all", "security", "performance", "correctness", "style"],
                    "default": "all"
                }
            }
        })
    }

    fn output_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "findings": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "severity": { "type": "string" },
                            "category": { "type": "string" },
                            "line": { "type": "integer" },
                            "message": { "type": "string" }
                        }
                    }
                },
                "summary": { "type": "string" },
                "tokens": { "type": "integer" }
            }
        })
    }

    fn execute(&self, input: SkillInput, _ctx: &SkillContext) -> anyhow::Result<SkillOutcome> {
        let content = if let Some(c) = input.get_str("content") {
            c.to_string()
        } else if let Some(p) = input.get_str("path") {
            std::fs::read_to_string(p).unwrap_or_default()
        } else {
            return Ok(SkillOutcome::failure("arbiter", "content or path required"));
        };
        let focus = input.get_str("focus").unwrap_or("all");

        let findings = analyze_content(&content, focus);
        let count = findings.len();
        let high = findings.iter().filter(|f| f["severity"] == "high").count();
        let summary = format!("{count} findings ({high} high severity)");
        let tokens = TokenEstimator::count_nonblocking(&summary);

        Ok(SkillOutcome::success(
            "arbiter",
            json!({ "findings": findings, "summary": summary, "tokens": tokens }),
        )
        .with_tokens(tokens))
    }
}

pub fn analyze_content(content: &str, focus: &str) -> Vec<serde_json::Value> {
    let mut findings = Vec::new();

    for (i, line) in content.lines().enumerate() {
        let t = line.trim();
        let line_no = (i + 1) as i64;

        if t.contains("unwrap()") && !t.starts_with("//") {
            findings.push(json!({
                "severity": "medium",
                "category": "correctness",
                "line": line_no,
                "message": "unwrap() panics on None/Err — consider ? or explicit handling"
            }));
        }
        if t.contains("TODO") || t.contains("FIXME") || t.contains("HACK") {
            findings.push(json!({
                "severity": "low",
                "category": "style",
                "line": line_no,
                "message": format!("Unresolved marker: {}", t.chars().take(80).collect::<String>())
            }));
        }
        if t.contains("unsafe {") || t.contains("unsafe{") {
            findings.push(json!({
                "severity": "high",
                "category": "security",
                "line": line_no,
                "message": "unsafe block — verify invariants and document why safety is upheld"
            }));
        }
        if t.contains(".clone().clone()") || t.contains("clone(); let") {
            findings.push(json!({
                "severity": "low",
                "category": "performance",
                "line": line_no,
                "message": "possible unnecessary clone — consider borrowing instead"
            }));
        }
        if t.contains("for") && t.contains(".clone()") {
            findings.push(json!({
                "severity": "medium",
                "category": "performance",
                "line": line_no,
                "message": "clone inside loop may be avoidable"
            }));
        }
        // process.env.X! or process.env["X"]! — non-null assertion on env var with no
        // runtime validation. If the var is missing at runtime this panics/throws.
        if (t.contains("process.env.") || t.contains("process.env["))
            && t.contains('!')
            && !t.starts_with("//")
        {
            findings.push(json!({
                "severity": "medium",
                "category": "security",
                "line": line_no,
                "message": "process.env value asserted non-null (!) without runtime validation — will throw if the variable is missing"
            }));
        }
        // Hardcoded secret patterns: assignment of a long all-caps string or base64-ish value
        if (t.contains("secret")
            || t.contains("password")
            || t.contains("api_key")
            || t.contains("apikey"))
            && (t.contains(" = \"") || t.contains(" = '"))
            && !t.starts_with("//")
        {
            findings.push(json!({
                "severity": "high",
                "category": "security",
                "line": line_no,
                "message": "possible hardcoded secret — use environment variables or a secrets manager"
            }));
        }

        // Diff-specific: added lines with potential issues
        if t.starts_with('+') {
            let added = t.strip_prefix('+').unwrap_or("").trim();
            if added.contains("println!") || added.contains("dbg!") {
                findings.push(json!({
                    "severity": "low",
                    "category": "style",
                    "line": line_no,
                    "message": "debug output left in — remove before merge"
                }));
            }
        }
    }

    // Apply focus filter: keep everything for "all", otherwise keep only matching category.
    if focus != "all" {
        findings.retain(|f| f["category"].as_str() == Some(focus));
    }

    findings
}

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

    #[test]
    fn no_findings_in_clean_code() {
        let src = "fn foo(x: u32) -> u32 {\n    x + 1\n}\n";
        assert!(analyze_content(src, "all").is_empty());
    }

    #[test]
    fn unwrap_flagged_medium_correctness() {
        let findings = analyze_content("let val = opt.unwrap();", "all");
        assert_eq!(findings.len(), 1);
        assert_eq!(findings[0]["severity"], "medium");
        assert_eq!(findings[0]["category"], "correctness");
    }

    #[test]
    fn unwrap_in_comment_not_flagged() {
        let findings = analyze_content("// safe: foo.unwrap() always returns Some", "all");
        assert!(findings.is_empty());
    }

    #[test]
    fn todo_marker_flagged_low_style() {
        let findings = analyze_content("// TODO: fix this later", "all");
        assert!(!findings.is_empty());
        assert_eq!(findings[0]["severity"], "low");
        assert_eq!(findings[0]["category"], "style");
    }

    #[test]
    fn unsafe_block_flagged_high_security() {
        let findings = analyze_content("    unsafe { transmute(x) }", "all");
        let f = findings.iter().find(|x| x["severity"] == "high").unwrap();
        assert_eq!(f["category"], "security");
    }

    #[test]
    fn debug_println_in_diff_line_flagged() {
        let findings = analyze_content("+    println!(\"debug: {}\", val);", "all");
        let f = findings
            .iter()
            .find(|x| x["message"].as_str().unwrap_or("").contains("debug"))
            .expect("debug finding missing");
        assert_eq!(f["severity"], "low");
    }

    #[test]
    fn performance_focus_skips_unsafe_findings() {
        let findings = analyze_content("unsafe { bad() }", "performance");
        assert!(findings.iter().all(|x| x["category"] != "security"));
    }

    #[test]
    fn line_number_reported_correctly() {
        let src = "let x = 1;\nlet y = opt.unwrap();";
        let findings = analyze_content(src, "all");
        assert_eq!(findings[0]["line"], 2);
    }
}
