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

/// Pathfinder: extract HTTP routes from web framework source files.
/// Supports Axum, Actix, Express, FastAPI, Flask, Django, Gin, Echo, Rails, Rocket, Hono.
pub struct PathfinderSkill;

#[derive(Debug)]
struct Route {
    method: String,
    path: String,
    handler: String,
    file: String,
    line: usize,
    framework: String,
}

impl Skill for PathfinderSkill {
    fn id(&self) -> SkillId {
        SkillId("pathfinder")
    }
    fn display_name(&self) -> &str {
        "Pathfinder"
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "root": {
                    "type": "string",
                    "default": ".",
                    "description": "Project root directory to scan for route definitions"
                },
                "framework": {
                    "type": "string",
                    "enum": ["auto", "axum", "actix", "express", "fastapi", "flask", "django", "gin", "echo", "rails", "rocket", "hono"],
                    "default": "auto",
                    "description": "Force a specific framework. auto detects based on file extension and patterns."
                },
                "methods": {
                    "type": "array",
                    "items": { "type": "string" },
                    "description": "Filter to specific HTTP methods (GET, POST, PUT, DELETE, PATCH). Empty = all."
                },
                "path_prefix": {
                    "type": "string",
                    "description": "Filter routes whose path starts with this prefix (e.g. /api)"
                }
            },
            "required": []
        })
    }

    fn output_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "routes": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "method":    { "type": "string" },
                            "path":      { "type": "string" },
                            "handler":   { "type": "string" },
                            "file":      { "type": "string" },
                            "line":      { "type": "integer" },
                            "framework": { "type": "string" }
                        }
                    }
                },
                "total":      { "type": "integer" },
                "frameworks": { "type": "array", "items": { "type": "string" } },
                "files_scanned": { "type": "integer" }
            }
        })
    }

    fn execute(&self, input: SkillInput, _ctx: &SkillContext) -> anyhow::Result<SkillOutcome> {
        let root = input.get_str("root").unwrap_or(".").to_string();
        let framework_hint = input.get_str("framework").unwrap_or("auto").to_string();
        let method_filter: Vec<String> = input
            .get_str_array("methods")
            .unwrap_or_default()
            .into_iter()
            .map(|s| s.to_uppercase())
            .collect();
        let path_prefix = input.get_str("path_prefix").unwrap_or("").to_string();

        let mut all_routes: Vec<Route> = Vec::new();
        let mut files_scanned = 0usize;

        collect_routes(
            Path::new(&root),
            &framework_hint,
            &mut all_routes,
            &mut files_scanned,
        );

        // Apply filters
        if !method_filter.is_empty() {
            all_routes.retain(|r| method_filter.contains(&r.method.to_uppercase()));
        }
        if !path_prefix.is_empty() {
            all_routes.retain(|r| r.path.starts_with(&path_prefix));
        }

        let frameworks: Vec<String> = {
            let mut fs: Vec<String> = all_routes.iter().map(|r| r.framework.clone()).collect();
            fs.sort();
            fs.dedup();
            fs
        };

        let total = all_routes.len();
        let routes_json: Vec<serde_json::Value> = all_routes
            .iter()
            .map(|r| {
                json!({
                    "method": r.method,
                    "path": r.path,
                    "handler": r.handler,
                    "file": r.file,
                    "line": r.line,
                    "framework": r.framework
                })
            })
            .collect();

        Ok(SkillOutcome::success(
            "pathfinder",
            json!({
                "routes": routes_json,
                "total": total,
                "frameworks": frameworks,
                "files_scanned": files_scanned
            }),
        ))
    }
}

fn collect_routes(dir: &Path, framework: &str, routes: &mut Vec<Route>, scanned: &mut usize) {
    let Ok(rd) = std::fs::read_dir(dir) else {
        return;
    };
    for entry in rd.flatten() {
        let path = entry.path();
        let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
        if name.starts_with('.')
            || name == "target"
            || name == "node_modules"
            || name == "__pycache__"
        {
            continue;
        }
        if path.is_dir() {
            collect_routes(&path, framework, routes, scanned);
        } else if path.is_file() {
            let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
            let should_scan = match framework {
                "axum" | "actix" | "rocket" => ext == "rs",
                "express" | "hono" => matches!(ext, "js" | "ts" | "mjs"),
                "fastapi" | "flask" | "django" => ext == "py",
                "gin" | "echo" => ext == "go",
                "rails" => {
                    name == "routes.rb"
                        || (ext == "rb" && path.to_string_lossy().contains("routes"))
                }
                _ => matches!(ext, "rs" | "py" | "ts" | "js" | "go" | "rb"),
            };
            if !should_scan {
                continue;
            }
            if let Ok(content) = std::fs::read_to_string(&path) {
                *scanned += 1;
                let file_str = path.to_string_lossy().to_string();
                extract_routes(&content, &file_str, framework, routes);
            }
        }
    }
}

fn extract_routes(content: &str, file: &str, framework: &str, routes: &mut Vec<Route>) {
    for (i, line) in content.lines().enumerate() {
        let line_no = i + 1;
        let trimmed = line.trim();

        // ── Rust / Axum ──────────────────────────────────────────────────────
        if matches!(framework, "axum" | "auto") && file.ends_with(".rs") {
            // .route("/path", get(handler)) or .route("/path", post(handler))
            if let Some(r) = parse_axum_route(trimmed, file, line_no) {
                routes.push(r);
                continue;
            }
        }

        // ── Rust / Actix ─────────────────────────────────────────────────────
        if matches!(framework, "actix" | "auto") && file.ends_with(".rs") {
            if let Some(r) = parse_actix_route(trimmed, file, line_no) {
                routes.push(r);
                continue;
            }
        }

        // ── Rust / Rocket ────────────────────────────────────────────────────
        if matches!(framework, "rocket" | "auto") && file.ends_with(".rs") {
            if let Some(r) = parse_rocket_route(trimmed, file, line_no) {
                routes.push(r);
                continue;
            }
        }

        // ── Python / FastAPI ─────────────────────────────────────────────────
        if matches!(framework, "fastapi" | "auto") && file.ends_with(".py") {
            if let Some(r) = parse_fastapi_route(trimmed, file, line_no) {
                routes.push(r);
                continue;
            }
        }

        // ── Python / Flask ───────────────────────────────────────────────────
        if matches!(framework, "flask" | "auto") && file.ends_with(".py") {
            if let Some(r) = parse_flask_route(trimmed, file, line_no) {
                routes.push(r);
                continue;
            }
        }

        // ── Python / Django ──────────────────────────────────────────────────
        if matches!(framework, "django" | "auto") && file.ends_with(".py") {
            if let Some(r) = parse_django_route(trimmed, file, line_no) {
                routes.push(r);
                continue;
            }
        }

        // ── Go / Gin ─────────────────────────────────────────────────────────
        if matches!(framework, "gin" | "auto") && file.ends_with(".go") {
            if let Some(r) = parse_gin_route(trimmed, file, line_no) {
                routes.push(r);
                continue;
            }
        }

        // ── Go / Echo ────────────────────────────────────────────────────────
        if matches!(framework, "echo" | "auto") && file.ends_with(".go") {
            if let Some(r) = parse_echo_route(trimmed, file, line_no) {
                routes.push(r);
                continue;
            }
        }

        // ── JS/TS / Express ──────────────────────────────────────────────────
        if matches!(framework, "express" | "auto")
            && (file.ends_with(".ts") || file.ends_with(".js"))
        {
            if let Some(r) = parse_express_route(trimmed, file, line_no) {
                routes.push(r);
                continue;
            }
        }

        // ── JS/TS / Hono ─────────────────────────────────────────────────────
        if matches!(framework, "hono" | "auto") && (file.ends_with(".ts") || file.ends_with(".js"))
        {
            if let Some(r) = parse_hono_route(trimmed, file, line_no) {
                routes.push(r);
                continue;
            }
        }

        // ── Ruby / Rails ─────────────────────────────────────────────────────
        if matches!(framework, "rails" | "auto") && file.ends_with(".rb") {
            if let Some(r) = parse_rails_route(trimmed, file, line_no) {
                routes.push(r);
            }
        }
    }
}

// ── Per-framework parsers ─────────────────────────────────────────────────────

fn parse_axum_route(line: &str, file: &str, line_no: usize) -> Option<Route> {
    // .route("/path", get(handler)) or .route("/path", post(handler))
    if !line.contains(".route(\"") {
        return None;
    }
    // Extract path: first quoted string after .route(
    let path = extract_between(line, ".route(\"", "\"")?.to_string();
    let method = ["get", "post", "put", "delete", "patch", "options", "head"]
        .iter()
        .find(|&&m| line.contains(&format!("{m}(")))?;
    let handler = extract_between(line, &format!("{method}("), ")")
        .unwrap_or("unknown")
        .to_string();
    Some(Route {
        method: method.to_uppercase(),
        path,
        handler,
        file: file.to_string(),
        line: line_no,
        framework: "axum".into(),
    })
}

fn parse_actix_route(line: &str, file: &str, line_no: usize) -> Option<Route> {
    // web::get().to(handler) or .route("/path", web::get().to(handler))
    if !line.contains("web::") {
        return None;
    }
    let method = ["get", "post", "put", "delete", "patch"]
        .iter()
        .find(|&&m| line.contains(&format!("web::{m}(")))?;
    let path = extract_between(line, "\"", "\"").unwrap_or("/").to_string();
    let handler = extract_between(line, ".to(", ")")
        .unwrap_or("handler")
        .to_string();
    Some(Route {
        method: method.to_uppercase(),
        path,
        handler,
        file: file.to_string(),
        line: line_no,
        framework: "actix".into(),
    })
}

fn parse_rocket_route(line: &str, file: &str, line_no: usize) -> Option<Route> {
    // #[get("/path")] or #[post("/path")]
    let method = ["get", "post", "put", "delete", "patch", "options", "head"]
        .iter()
        .find(|&&m| line.starts_with(&format!("#[{m}(")))?;
    let path = extract_between(line, "(\"", "\")")
        .unwrap_or("/")
        .to_string();
    Some(Route {
        method: method.to_uppercase(),
        path,
        handler: String::new(),
        file: file.to_string(),
        line: line_no,
        framework: "rocket".into(),
    })
}

fn parse_fastapi_route(line: &str, file: &str, line_no: usize) -> Option<Route> {
    // @app.get("/path") or @router.post("/path")
    if !line.starts_with('@') {
        return None;
    }
    let method = ["get", "post", "put", "delete", "patch", "options", "head"]
        .iter()
        .find(|&&m| line.contains(&format!(".{m}(")))?;
    let path = extract_between(line, "(\"", "\")")
        .unwrap_or("/")
        .to_string();
    Some(Route {
        method: method.to_uppercase(),
        path,
        handler: String::new(),
        file: file.to_string(),
        line: line_no,
        framework: "fastapi".into(),
    })
}

fn parse_flask_route(line: &str, file: &str, line_no: usize) -> Option<Route> {
    // @app.route("/path", methods=["GET", "POST"])
    if !line.starts_with('@') || !line.contains(".route(") {
        return None;
    }
    let path = extract_between(line, "\"", "\"").unwrap_or("/").to_string();
    let methods_str = extract_between(line, "methods=[", "]").unwrap_or("GET");
    let methods: Vec<&str> = methods_str
        .split(',')
        .map(|s| s.trim().trim_matches('"').trim_matches('\''))
        .filter(|s| !s.is_empty())
        .collect();
    // One route entry per method
    if methods.len() > 1 {
        // Return first; caller will see only one but that's fine for display
    }
    let method = methods.first().copied().unwrap_or("GET").to_string();
    Some(Route {
        method: method.to_uppercase(),
        path,
        handler: String::new(),
        file: file.to_string(),
        line: line_no,
        framework: "flask".into(),
    })
}

fn parse_django_route(line: &str, file: &str, line_no: usize) -> Option<Route> {
    // path("route/", view, name="name") or re_path(r"^route", view)
    if !line.trim_start().starts_with("path(") && !line.trim_start().starts_with("re_path(") {
        return None;
    }
    let path = extract_between(line, "\"", "\"")
        .or_else(|| extract_between(line, "r\"", "\""))
        .unwrap_or("/")
        .to_string();
    Some(Route {
        method: "ANY".into(),
        path,
        handler: String::new(),
        file: file.to_string(),
        line: line_no,
        framework: "django".into(),
    })
}

fn parse_gin_route(line: &str, file: &str, line_no: usize) -> Option<Route> {
    // r.GET("/path", handler) or engine.POST("/path", handler)
    let method = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]
        .iter()
        .find(|&&m| line.contains(&format!(".{m}(\"")))?;
    let path = extract_between(line, "(\"", "\"")
        .unwrap_or("/")
        .to_string();
    let handler = extract_between(line, ", ", ")")
        .unwrap_or("handler")
        .to_string();
    Some(Route {
        method: method.to_string(),
        path,
        handler,
        file: file.to_string(),
        line: line_no,
        framework: "gin".into(),
    })
}

fn parse_echo_route(line: &str, file: &str, line_no: usize) -> Option<Route> {
    // e.GET("/path", handler) or g.POST("/path", handler)
    let method = ["GET", "POST", "PUT", "DELETE", "PATCH"]
        .iter()
        .find(|&&m| {
            let pat = format!(".{m}(\"");
            line.contains(&pat)
        })?;
    let path = extract_between(line, "(\"", "\"")
        .unwrap_or("/")
        .to_string();
    Some(Route {
        method: method.to_string(),
        path,
        handler: String::new(),
        file: file.to_string(),
        line: line_no,
        framework: "echo".into(),
    })
}

fn parse_express_route(line: &str, file: &str, line_no: usize) -> Option<Route> {
    // app.get('/path', handler) or router.post('/path', handler)
    let method = ["get", "post", "put", "delete", "patch", "options", "head"]
        .iter()
        .find(|&&m| {
            line.contains(&format!(".{m}('"))
                || line.contains(&format!(".{m}(\""))
                || line.contains(&format!(".{m}(`"))
        })?;
    let path = extract_between(line, "(\"", "\"")
        .or_else(|| extract_between(line, "('", "'"))
        .unwrap_or("/")
        .to_string();
    Some(Route {
        method: method.to_uppercase(),
        path,
        handler: String::new(),
        file: file.to_string(),
        line: line_no,
        framework: "express".into(),
    })
}

fn parse_hono_route(line: &str, file: &str, line_no: usize) -> Option<Route> {
    // app.get('/path', (c) => ...) — same pattern as express but framework label differs
    let method = ["get", "post", "put", "delete", "patch"]
        .iter()
        .find(|&&m| line.contains(&format!(".{m}('")) || line.contains(&format!(".{m}(\"")))?;
    if !line.contains("=> ") && !line.contains("async") {
        return None; // Distinguish from Express by requiring arrow/async
    }
    let path = extract_between(line, "(\"", "\"")
        .or_else(|| extract_between(line, "('", "'"))
        .unwrap_or("/")
        .to_string();
    Some(Route {
        method: method.to_uppercase(),
        path,
        handler: String::new(),
        file: file.to_string(),
        line: line_no,
        framework: "hono".into(),
    })
}

fn parse_rails_route(line: &str, file: &str, line_no: usize) -> Option<Route> {
    // get "/path", to: "controller#action"
    // resources :items
    let method = ["get", "post", "put", "patch", "delete"]
        .iter()
        .find(|&&m| line.starts_with(m) && line[m.len()..].starts_with(' '))?;
    let path = extract_between(line, "\"", "\"")
        .or_else(|| extract_between(line, "'", "'"))
        .unwrap_or("/")
        .to_string();
    let handler = extract_between(line, "to: \"", "\"")
        .or_else(|| extract_between(line, "to: '", "'"))
        .unwrap_or("")
        .to_string();
    Some(Route {
        method: method.to_uppercase(),
        path,
        handler,
        file: file.to_string(),
        line: line_no,
        framework: "rails".into(),
    })
}

fn extract_between<'a>(s: &'a str, start: &str, end: &str) -> Option<&'a str> {
    let i = s.find(start)? + start.len();
    let j = s[i..].find(end).map(|j| i + j)?;
    Some(&s[i..j])
}

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

    #[test]
    fn axum_get_route_parsed() {
        let r = parse_axum_route(r#".route("/users", get(list_users))"#, "src/main.rs", 10);
        assert!(r.is_some());
        let r = r.unwrap();
        assert_eq!(r.method, "GET");
        assert_eq!(r.path, "/users");
        assert_eq!(r.framework, "axum");
    }

    #[test]
    fn axum_post_route_parsed() {
        let r = parse_axum_route(r#".route("/users", post(create_user))"#, "src/main.rs", 20);
        assert_eq!(r.unwrap().method, "POST");
    }

    #[test]
    fn fastapi_get_parsed() {
        let r = parse_fastapi_route(r#"@app.get("/items")"#, "main.py", 5);
        assert!(r.is_some());
        let r = r.unwrap();
        assert_eq!(r.method, "GET");
        assert_eq!(r.path, "/items");
        assert_eq!(r.framework, "fastapi");
    }

    #[test]
    fn express_post_parsed() {
        let r = parse_express_route(r#"app.post("/login", handleLogin)"#, "routes.ts", 3);
        assert!(r.is_some());
        assert_eq!(r.unwrap().method, "POST");
    }

    #[test]
    fn gin_delete_parsed() {
        let r = parse_gin_route(r#"r.DELETE("/users/:id", deleteUser)"#, "main.go", 15);
        assert!(r.is_some());
        assert_eq!(r.unwrap().method, "DELETE");
    }

    #[test]
    fn rocket_get_parsed() {
        let r = parse_rocket_route(r#"#[get("/health")]"#, "src/routes.rs", 1);
        assert!(r.is_some());
        assert_eq!(r.unwrap().method, "GET");
    }

    #[test]
    fn flask_route_parsed() {
        let r = parse_flask_route(
            r#"@app.route("/api/v1", methods=["GET", "POST"])"#,
            "views.py",
            8,
        );
        assert!(r.is_some());
        assert_eq!(r.unwrap().method, "GET");
    }

    #[test]
    fn django_path_parsed() {
        let r = parse_django_route(
            r#"    path("api/users/", UserView.as_view(), name="users"),"#,
            "urls.py",
            5,
        );
        assert!(r.is_some());
        assert_eq!(r.unwrap().framework, "django");
    }

    #[test]
    fn rails_get_parsed() {
        let r = parse_rails_route(r#"get "/home", to: "pages#home""#, "routes.rb", 3);
        assert!(r.is_some());
        let r = r.unwrap();
        assert_eq!(r.method, "GET");
        assert_eq!(r.handler, "pages#home");
    }

    #[test]
    fn extract_between_works() {
        assert_eq!(extract_between("hello (world)", "(", ")"), Some("world"));
    }

    #[test]
    fn extract_between_not_found() {
        assert!(extract_between("no brackets here", "(", ")").is_none());
    }

    #[test]
    fn non_route_line_returns_none() {
        assert!(parse_axum_route("let x = 5;", "main.rs", 1).is_none());
        assert!(parse_fastapi_route("def get_items():", "main.py", 1).is_none());
    }

    #[test]
    fn actix_get_parsed() {
        let r = parse_actix_route(
            r#".route("/api/items", web::get().to(list_items))"#,
            "src/main.rs",
            8,
        );
        assert!(r.is_some());
        let r = r.unwrap();
        assert_eq!(r.method, "GET");
        assert_eq!(r.framework, "actix");
    }

    #[test]
    fn actix_post_parsed() {
        let r = parse_actix_route(
            r#".route("/api/users", web::post().to(create_user))"#,
            "src/main.rs",
            12,
        );
        assert_eq!(r.unwrap().method, "POST");
    }

    #[test]
    fn echo_get_parsed() {
        let r = parse_echo_route(r#"e.GET("/users", listUsers)"#, "main.go", 10);
        assert!(r.is_some());
        let r = r.unwrap();
        assert_eq!(r.method, "GET");
        assert_eq!(r.path, "/users");
        assert_eq!(r.framework, "echo");
    }

    #[test]
    fn echo_post_parsed() {
        let r = parse_echo_route(r#"g.POST("/login", handleLogin)"#, "main.go", 15);
        assert_eq!(r.unwrap().method, "POST");
    }

    #[test]
    fn hono_get_parsed() {
        let r = parse_hono_route(
            r#"app.get('/api/items', async (c) => c.json({}))"#,
            "index.ts",
            5,
        );
        assert!(r.is_some());
        let r = r.unwrap();
        assert_eq!(r.method, "GET");
        assert_eq!(r.path, "/api/items");
        assert_eq!(r.framework, "hono");
    }

    #[test]
    fn hono_post_parsed() {
        let r = parse_hono_route(
            r#"app.post('/auth/login', async (c) => { const body = await c.req.json(); })"#,
            "routes.ts",
            20,
        );
        assert_eq!(r.unwrap().method, "POST");
    }

    #[test]
    fn hono_without_arrow_returns_none() {
        // Express-style without arrow/async should NOT parse as hono
        let r = parse_hono_route(r#"app.get('/path', handler)"#, "index.ts", 1);
        assert!(r.is_none());
    }

    #[test]
    fn execute_finds_axum_routes_in_temp_dir() {
        use crate::invocation::{SkillContext, SkillInput};
        use tempfile::tempdir;

        let dir = tempdir().unwrap();
        std::fs::write(
            dir.path().join("main.rs"),
            r#"
let app = Router::new()
    .route("/health", get(health_handler))
    .route("/users", post(create_user))
    .route("/users/:id", get(get_user));
"#,
        )
        .unwrap();

        let skill = PathfinderSkill;
        let out = skill
            .execute(
                SkillInput::new(json!({
                    "root": dir.path().to_str().unwrap(),
                    "framework": "axum"
                })),
                &SkillContext::new("test", "."),
            )
            .unwrap();

        let total = out.result["total"].as_u64().unwrap_or(0);
        assert!(total >= 3, "expected at least 3 axum routes, got {total}");
    }

    #[test]
    fn execute_filters_by_method() {
        use crate::invocation::{SkillContext, SkillInput};
        use tempfile::tempdir;

        let dir = tempdir().unwrap();
        std::fs::write(
            dir.path().join("routes.rs"),
            r#"
    .route("/a", get(handler_a))
    .route("/b", post(handler_b))
    .route("/c", get(handler_c))
"#,
        )
        .unwrap();

        let skill = PathfinderSkill;
        let out = skill
            .execute(
                SkillInput::new(json!({
                    "root": dir.path().to_str().unwrap(),
                    "framework": "axum",
                    "methods": ["GET"]
                })),
                &SkillContext::new("test", "."),
            )
            .unwrap();

        let routes = out.result["routes"].as_array().unwrap();
        assert!(routes.iter().all(|r| r["method"] == "GET"));
        let total_get = routes.len();
        assert_eq!(total_get, 2);
    }

    #[test]
    fn execute_filters_by_path_prefix() {
        use crate::invocation::{SkillContext, SkillInput};
        use tempfile::tempdir;

        let dir = tempdir().unwrap();
        std::fs::write(
            dir.path().join("main.py"),
            "@app.get(\"/api/users\")\ndef list_users(): pass\n@app.post(\"/auth/login\")\ndef login(): pass\n",
        )
        .unwrap();

        let skill = PathfinderSkill;
        let out = skill
            .execute(
                SkillInput::new(json!({
                    "root": dir.path().to_str().unwrap(),
                    "framework": "fastapi",
                    "path_prefix": "/api"
                })),
                &SkillContext::new("test", "."),
            )
            .unwrap();

        let routes = out.result["routes"].as_array().unwrap();
        assert_eq!(routes.len(), 1);
        assert!(routes[0]["path"].as_str().unwrap().starts_with("/api"));
    }
}
