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

/// Pinpoint: read a single named symbol from a file by name, return only its span.
pub struct PinpointSkill;

impl Skill for PinpointSkill {
    fn id(&self) -> SkillId {
        SkillId("pinpoint")
    }
    fn display_name(&self) -> &str {
        "Pinpoint"
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "path": { "type": "string", "description": "File path" },
                "symbol": { "type": "string", "description": "Symbol name to extract" },
                "context_lines": { "type": "integer", "default": 0, "description": "Extra lines of context above/below the symbol" }
            },
            "required": ["path", "symbol"]
        })
    }

    fn output_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "content": { "type": "string" },
                "start_line": { "type": "integer" },
                "end_line": { "type": "integer" },
                "tokens": { "type": "integer" },
                "found": { "type": "boolean" }
            }
        })
    }

    fn execute(&self, input: SkillInput, _ctx: &SkillContext) -> anyhow::Result<SkillOutcome> {
        let path = input.get_str("path").unwrap_or("").to_string();
        let symbol = input.get_str("symbol").unwrap_or("").to_string();
        let ctx_lines = input.get_u64("context_lines").unwrap_or(0) as usize;

        let content = match std::fs::read_to_string(&path) {
            Ok(c) => c,
            Err(e) => {
                return Ok(SkillOutcome::failure("pinpoint", &e.to_string()));
            }
        };

        if let Some((start, end, span)) = extract_symbol(&content, &symbol, ctx_lines) {
            let tokens = TokenEstimator::count(&span);
            Ok(SkillOutcome::success(
                "pinpoint",
                json!({
                    "content": span,
                    "start_line": start + 1,
                    "end_line": end + 1,
                    "tokens": tokens,
                    "found": true
                }),
            )
            .with_tokens(tokens))
        } else {
            Ok(SkillOutcome::success(
                "pinpoint",
                json!({
                    "content": "",
                    "found": false,
                    "tokens": 0
                }),
            ))
        }
    }
}

fn extract_symbol(content: &str, symbol: &str, ctx: usize) -> Option<(usize, usize, String)> {
    let lines: Vec<&str> = content.lines().collect();
    let def_patterns = [
        format!("fn {symbol}"),
        format!("fn {symbol}<"),
        format!("fn {symbol}("),
        format!("struct {symbol}"),
        format!("struct {symbol}<"),
        format!("enum {symbol}"),
        format!("enum {symbol}<"),
        format!("trait {symbol}"),
        format!("impl {symbol}"),
        format!("class {symbol}"),
        format!("def {symbol}("),
        format!("function {symbol}("),
        format!("async function {symbol}("),
        format!("async {symbol}("),
        format!("const {symbol} "),
        format!("const {symbol}="),
        format!("type {symbol} "),
        format!("type {symbol}="),
    ];

    let start = lines.iter().position(|l| {
        let t = l.trim();
        def_patterns.iter().any(|p| t.contains(p.as_str()))
    })?;

    // Walk forward to find the end by counting braces
    let mut depth = 0i32;
    let mut end = start;
    let mut found_open = false;

    for (i, line) in lines[start..].iter().enumerate() {
        for ch in line.chars() {
            match ch {
                '{' => {
                    depth += 1;
                    found_open = true;
                }
                '}' => depth -= 1,
                _ => {}
            }
        }
        end = start + i;
        if found_open && depth <= 0 {
            break;
        }
        // For Python-style (no braces), stop at blank line after content
        if !found_open && i > 0 && line.trim().is_empty() {
            end = start + i - 1;
            break;
        }
    }

    let s = start.saturating_sub(ctx);
    let e = (end + ctx).min(lines.len().saturating_sub(1));
    Some((s, e, lines[s..=e].join("\n")))
}

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

    const RUST_SRC: &str = "\
pub struct Foo {
    x: u32,
}

pub fn bar(x: u32) -> u32 {
    x * 2
}

fn baz() {}
";

    #[test]
    fn finds_rust_struct() {
        let (start, _end, span) = extract_symbol(RUST_SRC, "Foo", 0).unwrap();
        assert_eq!(start, 0);
        assert!(span.contains("pub struct Foo"));
        assert!(span.contains("x: u32"));
    }

    #[test]
    fn finds_rust_fn() {
        let (_start, _end, span) = extract_symbol(RUST_SRC, "bar", 0).unwrap();
        assert!(span.contains("pub fn bar"));
        assert!(span.contains("x * 2"));
    }

    #[test]
    fn returns_none_for_unknown_symbol() {
        assert!(extract_symbol(RUST_SRC, "nonexistent", 0).is_none());
    }

    #[test]
    fn context_lines_expand_window() {
        let src = "// comment\npub fn foo() {\n    1\n}\n// after";
        let (start, end, _span) = extract_symbol(src, "foo", 1).unwrap();
        // With ctx=1, start should be 0 (fn is at line 1, 1 line above = line 0)
        assert_eq!(start, 0);
        // end should reach the comment after
        assert!(end >= 3);
    }

    #[test]
    fn single_line_fn_found() {
        let src = "fn baz() {}\n";
        let (start, end, span) = extract_symbol(src, "baz", 0).unwrap();
        assert_eq!(start, end);
        assert!(span.contains("fn baz()"));
    }

    #[test]
    fn finds_typescript_async_class_method() {
        let src = "export class AuthService {\n  constructor() {}\n\n  async login(email: string, password: string): Promise<string> {\n    return \"token\";\n  }\n}\n";
        let (_, _, span) = extract_symbol(src, "login", 0).unwrap();
        assert!(span.contains("async login("), "got: {span}");
        assert!(span.contains("return \"token\""));
    }

    #[test]
    fn finds_js_function_declaration() {
        let src = "function greet(name) {\n  return 'hi ' + name;\n}\n";
        let (_, _, span) = extract_symbol(src, "greet", 0).unwrap();
        assert!(span.contains("function greet("));
    }
}
