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

/// Blueprint: emit a compact structural outline of a file — sections, headings, exports.
pub struct BlueprintSkill;

impl Skill for BlueprintSkill {
    fn id(&self) -> SkillId {
        SkillId("blueprint")
    }
    fn display_name(&self) -> &str {
        "Blueprint"
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "path": { "type": "string", "description": "File to outline" },
                "content": { "type": "string", "description": "Content to outline (alternative to path)" },
                "include_line_numbers": { "type": "boolean", "default": true }
            }
        })
    }

    fn output_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "outline": { "type": "string" },
                "sections": { "type": "integer" },
                "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(path) = input.get_str("path") {
            std::fs::read_to_string(path).unwrap_or_default()
        } else {
            return Ok(SkillOutcome::failure(
                "blueprint",
                "path or content required",
            ));
        };

        let include_lines = input.get_bool("include_line_numbers").unwrap_or(true);
        let outline = build_outline(&content, include_lines);
        let sections = outline.lines().count();
        let tokens = TokenEstimator::count(&outline);

        Ok(SkillOutcome::success(
            "blueprint",
            json!({
                "outline": outline,
                "sections": sections,
                "tokens": tokens
            }),
        )
        .with_tokens(tokens))
    }
}

fn build_outline(content: &str, include_lines: bool) -> String {
    let mut out = Vec::new();
    for (i, line) in content.lines().enumerate() {
        let trimmed = line.trim();

        // Markdown headings
        if trimmed.starts_with('#') {
            let entry = if include_lines {
                format!("{:>4}: {trimmed}", i + 1)
            } else {
                trimmed.to_string()
            };
            out.push(entry);
            continue;
        }

        // Rust/Go/TS/JS/Python definition patterns
        let is_def = trimmed.starts_with("pub fn ")
            || trimmed.starts_with("pub async fn ")
            || trimmed.starts_with("pub struct ")
            || trimmed.starts_with("pub enum ")
            || trimmed.starts_with("pub trait ")
            || trimmed.starts_with("pub type ")
            || trimmed.starts_with("pub const ")
            || trimmed.starts_with("fn ")
            || trimmed.starts_with("async fn ")
            || trimmed.starts_with("struct ")
            || trimmed.starts_with("enum ")
            || trimmed.starts_with("trait ")
            || trimmed.starts_with("type ")
            || trimmed.starts_with("const ")
            || trimmed.starts_with("def ")
            || trimmed.starts_with("class ")
            || trimmed.starts_with("interface ")
            || trimmed.starts_with("export function ")
            || trimmed.starts_with("export const ")
            || trimmed.starts_with("export class ")
            || trimmed.starts_with("export default ")
            || trimmed.starts_with("export type ")
            || trimmed.starts_with("func ")
            || trimmed.starts_with("impl ");

        if is_def {
            // Truncate at opening brace for brevity
            let sig = trimmed
                .split('{')
                .next()
                .unwrap_or(trimmed)
                .split(':')
                .next()
                .unwrap_or(trimmed)
                .trim_end_matches([' ', '{'])
                .trim();
            let entry = if include_lines {
                format!("{:>4}: {sig}", i + 1)
            } else {
                sig.to_string()
            };
            out.push(entry);
        }
    }
    out.join("\n")
}

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

    #[test]
    fn outline_rust_fn() {
        let src = "pub fn hello(x: u32) -> u32 {\n    x + 1\n}\n";
        let out = build_outline(src, false);
        assert!(out.contains("pub fn hello"), "got: {out}");
        assert!(!out.contains("x + 1"), "body should be stripped");
    }

    #[test]
    fn outline_includes_line_numbers() {
        let src = "fn foo() {}\nfn bar() {}";
        let out = build_outline(src, true);
        assert!(out.contains("1:"), "should have line 1 for foo");
        assert!(out.contains("2:"), "should have line 2 for bar");
    }

    #[test]
    fn outline_markdown_headings() {
        let src = "# Title\n## Section\nsome text\n### Sub";
        let out = build_outline(src, false);
        assert!(out.contains("# Title"));
        assert!(out.contains("## Section"));
        assert!(out.contains("### Sub"));
        assert!(!out.contains("some text"));
    }

    #[test]
    fn outline_python_def() {
        let src = "def my_func(a, b):\n    return a + b\n";
        let out = build_outline(src, false);
        assert!(out.contains("def my_func"), "got: {out}");
    }

    #[test]
    fn outline_empty_file_returns_empty() {
        assert_eq!(build_outline("", false), "");
    }

    #[test]
    fn outline_skips_plain_code_lines() {
        let src = "let x = 1;\nprintln!(\"hi\");\n";
        let out = build_outline(src, false);
        assert!(out.is_empty(), "plain lines should be skipped, got: {out}");
    }
}
