use crate::registry::{ToolEntry, ToolPermission};
use crate::substrate::Substrate;
use regex::Regex;
use serde_json::{json, Value};
use std::path::{Path, PathBuf};
use std::sync::Arc;

const MAX_FILE_BYTES: usize = 512 * 1024;

pub fn entries() -> Vec<ToolEntry> {
    vec![
        ToolEntry::builtin(car_ir::builtins::read_file()).with_category("filesystem"),
        ToolEntry::builtin(car_ir::builtins::list_dir()).with_category("filesystem"),
        ToolEntry::builtin(car_ir::builtins::find_files()).with_category("filesystem"),
        ToolEntry::builtin(car_ir::builtins::grep_files()).with_category("filesystem"),
        ToolEntry::builtin(car_ir::builtins::calculate()).with_category("utility"),
        ToolEntry::builtin(car_ir::builtins::write_file())
            .with_permission(ToolPermission::AskUser)
            .with_side_effects(true)
            .with_category("filesystem"),
        ToolEntry::builtin(car_ir::builtins::edit_file())
            .with_permission(ToolPermission::AskUser)
            .with_side_effects(true)
            .with_category("filesystem"),
    ]
}

/// Execute a built-in commodity tool against the runtime's bound `substrate`.
///
/// The side-effecting file tools (`read_file`/`write_file`/`edit_file`/
/// `list_dir`/`find_files`/`grep_files`) resolve and act against `substrate`,
/// so the agent acts within **one** environment. `calculate` is pure — it
/// never consults the substrate. Returns `None` for unknown tools (so the
/// caller can fall through), `Some(result)` otherwise.
pub async fn execute(
    substrate: &Arc<dyn Substrate>,
    tool: &str,
    params: &Value,
) -> Option<Result<Value, String>> {
    let result = match tool {
        "read_file" => exec_read_file(substrate, params).await,
        "write_file" => exec_write_file(substrate, params).await,
        "edit_file" => exec_edit_file(substrate, params).await,
        "list_dir" => exec_list_dir(substrate, params).await,
        "find_files" => exec_find_files(substrate, params).await,
        "grep_files" => exec_grep_files(substrate, params).await,
        "calculate" => exec_calculate(params),
        _ => return None,
    };
    Some(result)
}

async fn exec_read_file(substrate: &Arc<dyn Substrate>, params: &Value) -> Result<Value, String> {
    let path = params
        .get("path")
        .and_then(|v| v.as_str())
        .ok_or("missing 'path' parameter")?;
    let offset = params.get("offset").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
    let limit = params
        .get("limit")
        .and_then(|v| v.as_u64())
        .map(|v| v as usize);

    let content = substrate.read_text(path).await?;
    let size_bytes = content.len();
    let total_lines = content.lines().count();

    let returned = if offset > 0 || limit.is_some() {
        let lines: Vec<&str> = content.lines().collect();
        let start = offset.min(lines.len());
        let end = limit
            .map(|line_count| (start + line_count).min(lines.len()))
            .unwrap_or(lines.len());
        lines[start..end].join("\n")
    } else {
        content
    };

    Ok(json!({
        "path": substrate.display_path(path),
        "content": returned,
        "size_bytes": size_bytes,
        "total_lines": total_lines,
    }))
}

async fn exec_write_file(substrate: &Arc<dyn Substrate>, params: &Value) -> Result<Value, String> {
    let path = params
        .get("path")
        .and_then(|v| v.as_str())
        .ok_or("missing 'path' parameter")?;
    let content = params
        .get("content")
        .and_then(|v| v.as_str())
        .ok_or("missing 'content' parameter")?;
    let append = params
        .get("append")
        .and_then(|v| v.as_bool())
        .unwrap_or(false);

    if append {
        // Compose append on top of the substrate's read/write primitives so
        // behavior is environment-agnostic. A missing file starts empty,
        // matching the historic OpenOptions(create=true, append=true).
        let existing = substrate.read_text(path).await.unwrap_or_default();
        let mut combined = existing;
        combined.push_str(content);
        substrate.write_text(path, &combined).await?;
    } else {
        substrate.write_text(path, content).await?;
    }

    Ok(json!({
        "path": substrate.display_path(path),
        "bytes_written": content.len(),
        "append": append,
    }))
}

async fn exec_edit_file(substrate: &Arc<dyn Substrate>, params: &Value) -> Result<Value, String> {
    let path = params
        .get("path")
        .and_then(|v| v.as_str())
        .ok_or("missing 'path' parameter")?;
    let old_text = params
        .get("old_text")
        .and_then(|v| v.as_str())
        .ok_or("missing 'old_text' parameter")?;
    let new_text = params
        .get("new_text")
        .and_then(|v| v.as_str())
        .ok_or("missing 'new_text' parameter")?;

    let content = substrate.read_text(path).await?;
    let count = content.matches(old_text).count();
    if count == 0 {
        return Err(format!(
            "old_text not found in '{}'",
            substrate.display_path(path)
        ));
    }
    if count > 1 {
        return Err(format!(
            "old_text found {count} times in '{}' and must match uniquely",
            substrate.display_path(path)
        ));
    }

    let new_content = content.replacen(old_text, new_text, 1);
    substrate.write_text(path, &new_content).await?;

    Ok(json!({
        "edited": substrate.display_path(path),
        "diff_summary": format!(
            "replaced {} lines with {} lines",
            old_text.lines().count(),
            new_text.lines().count()
        ),
    }))
}

async fn exec_list_dir(substrate: &Arc<dyn Substrate>, params: &Value) -> Result<Value, String> {
    let path = params.get("path").and_then(|v| v.as_str()).unwrap_or(".");

    if !substrate.is_local() {
        return list_dir_via_command(substrate, path).await;
    }

    let full_path = local_resolve(path)?;
    let mut entries = Vec::new();

    let read_dir = std::fs::read_dir(&full_path)
        .map_err(|e| format!("failed to read dir '{}': {e}", full_path.display()))?;
    for entry in read_dir {
        let entry = entry.map_err(|e| format!("failed to read dir entry: {e}"))?;
        let file_name = entry.file_name().to_string_lossy().to_string();
        if should_skip_name(&file_name) {
            continue;
        }
        let metadata = entry
            .metadata()
            .map_err(|e| format!("failed to read metadata for '{}': {e}", file_name))?;
        entries.push(json!({
            "name": file_name,
            "path": entry.path().display().to_string(),
            "is_dir": metadata.is_dir(),
            "size_bytes": if metadata.is_file() { Some(metadata.len()) } else { None::<u64> },
        }));
    }

    Ok(json!({
        "path": full_path.display().to_string(),
        "entries": entries,
    }))
}

async fn exec_find_files(substrate: &Arc<dyn Substrate>, params: &Value) -> Result<Value, String> {
    let pattern = params
        .get("pattern")
        .and_then(|v| v.as_str())
        .ok_or("missing 'pattern' parameter")?;
    let root = params.get("path").and_then(|v| v.as_str()).unwrap_or(".");
    let max_results = params
        .get("max_results")
        .and_then(|v| v.as_u64())
        .unwrap_or(50) as usize;

    if !substrate.is_local() {
        return find_files_via_command(substrate, pattern, root, max_results).await;
    }

    let root_path = local_resolve(root)?;
    let matcher = glob_to_regex(pattern)?;
    let mut files = Vec::new();

    walk_files(&root_path, &mut |path| {
        if files.len() >= max_results {
            return;
        }
        if let Some(name) = path.file_name().and_then(|v| v.to_str()) {
            if matcher.is_match(name) {
                files.push(path.display().to_string());
            }
        }
    })?;

    Ok(json!({
        "files": files,
        "count": files.len(),
        "truncated": files.len() >= max_results,
    }))
}

async fn exec_grep_files(substrate: &Arc<dyn Substrate>, params: &Value) -> Result<Value, String> {
    let pattern = params
        .get("pattern")
        .and_then(|v| v.as_str())
        .ok_or("missing 'pattern' parameter")?;
    let root = params.get("path").and_then(|v| v.as_str()).unwrap_or(".");
    let max_results = params
        .get("max_results")
        .and_then(|v| v.as_u64())
        .unwrap_or(50) as usize;

    if !substrate.is_local() {
        return grep_files_via_command(substrate, pattern, root, max_results).await;
    }

    let root_path = local_resolve(root)?;
    let regex = Regex::new(pattern).map_err(|e| format!("invalid regex pattern: {e}"))?;
    let mut matches = Vec::new();

    walk_files(&root_path, &mut |path| {
        if matches.len() >= max_results || !is_text_file(path) {
            return;
        }
        let Ok(content) = std::fs::read_to_string(path) else {
            return;
        };
        if content.len() > MAX_FILE_BYTES {
            return;
        }
        for (idx, line) in content.lines().enumerate() {
            if regex.is_match(line) {
                matches.push(json!({
                    "path": path.display().to_string(),
                    "line": idx + 1,
                    "text": line,
                }));
                if matches.len() >= max_results {
                    break;
                }
            }
        }
    })?;

    Ok(json!({
        "matches": matches,
        "count": matches.len(),
        "truncated": matches.len() >= max_results,
    }))
}

fn exec_calculate(params: &Value) -> Result<Value, String> {
    let expression = params
        .get("expression")
        .and_then(|v| v.as_str())
        .ok_or("missing 'expression' parameter")?;
    // fasteval ships `sin/cos/abs/log/min/max/pi()/e()` and `^` (exponentiation)
    // as built-ins, but not `sqrt`, `ln`, or the bare `pi`/`e` constants.
    // fasteval consults this namespace only for names it doesn't resolve
    // itself, so the shim fills those gaps without shadowing any built-in —
    // making the `calculate` tool's supported surface explicit here rather
    // than inherited from the dependency.
    let mut ns = |name: &str, args: Vec<f64>| -> Option<f64> {
        match (name, args.as_slice()) {
            ("sqrt", [x]) => Some(x.sqrt()),
            ("ln", [x]) => Some(x.ln()),
            ("pi", []) => Some(std::f64::consts::PI),
            ("e", []) => Some(std::f64::consts::E),
            _ => None,
        }
    };
    let result = fasteval::ez_eval(expression, &mut ns)
        .map_err(|e| format!("failed to evaluate expression: {e}"))?;
    Ok(json!({ "result": result }))
}

// ─── Local-path resolution (host CWD), used only on the local fast path ───
//
// Mirrors `LocalSubstrate::resolve_path` exactly: absolute paths pass through,
// relative paths join the host process current_dir(). The directory-walking
// convenience tools (list/find/grep) use it directly when the bound substrate
// is the host; non-local substrates compose on `run_command` instead.
fn local_resolve(path: &str) -> Result<PathBuf, String> {
    crate::substrate::LocalSubstrate::resolve_path(path)
}

fn should_skip_name(name: &str) -> bool {
    name.starts_with('.') || matches!(name, "node_modules" | "__pycache__" | "target")
}

fn is_text_file(path: &Path) -> bool {
    matches!(
        path.extension().and_then(|v| v.to_str()),
        Some(
            "c" | "cc"
                | "cpp"
                | "cs"
                | "css"
                | "go"
                | "h"
                | "html"
                | "ini"
                | "java"
                | "js"
                | "json"
                | "jsx"
                | "kt"
                | "md"
                | "py"
                | "rb"
                | "rs"
                | "sh"
                | "sql"
                | "toml"
                | "ts"
                | "tsx"
                | "txt"
                | "xml"
                | "yaml"
                | "yml"
        )
    )
}

fn walk_files(root: &Path, visit: &mut dyn FnMut(&Path)) -> Result<(), String> {
    if root.is_file() {
        visit(root);
        return Ok(());
    }

    let read_dir = std::fs::read_dir(root)
        .map_err(|e| format!("failed to read dir '{}': {e}", root.display()))?;
    for entry in read_dir {
        let entry = entry.map_err(|e| format!("failed to read dir entry: {e}"))?;
        let path = entry.path();
        let file_name = entry.file_name().to_string_lossy().to_string();
        if should_skip_name(&file_name) {
            continue;
        }
        let metadata = entry
            .metadata()
            .map_err(|e| format!("failed to read metadata for '{}': {e}", path.display()))?;
        if metadata.is_dir() {
            walk_files(&path, visit)?;
        } else if metadata.is_file() {
            visit(&path);
        }
    }
    Ok(())
}

fn glob_to_regex(pattern: &str) -> Result<Regex, String> {
    let mut escaped = String::from("^");
    for ch in pattern.chars() {
        match ch {
            '*' => escaped.push_str(".*"),
            '?' => escaped.push('.'),
            _ => escaped.push_str(&regex::escape(&ch.to_string())),
        }
    }
    escaped.push('$');
    Regex::new(&escaped).map_err(|e| format!("invalid glob pattern: {e}"))
}

// ─── Non-local composition on run_command ─────────────────────────────────
//
// For non-host substrates (e.g. a VM bridge) the directory-walking convenience
// tools have no host fs to walk, so they compose on the substrate's
// `run_command`, mirroring the bridge's "no ls/find/grep — reduce to a shell
// command" design. These paths are NOT exercised by existing consumers (which
// all default to LocalSubstrate), so they introduce no behavior change there.

fn shell_quote(s: &str) -> String {
    // POSIX single-quote escaping: ' -> '\''
    format!("'{}'", s.replace('\'', "'\\''"))
}

async fn list_dir_via_command(substrate: &Arc<dyn Substrate>, path: &str) -> Result<Value, String> {
    let cmd = format!("ls -1Ap {}", shell_quote(path));
    let out = substrate.run_command(&cmd, Some(30.0)).await?;
    let entries: Vec<Value> = out
        .stdout
        .lines()
        .filter(|l| !l.is_empty())
        .filter(|name| {
            let bare = name.trim_end_matches('/');
            !should_skip_name(bare)
        })
        .map(|name| {
            let is_dir = name.ends_with('/');
            let bare = name.trim_end_matches('/');
            json!({
                "name": bare,
                "path": format!("{}/{}", path.trim_end_matches('/'), bare),
                "is_dir": is_dir,
                "size_bytes": Value::Null,
            })
        })
        .collect();
    Ok(json!({ "path": path, "entries": entries }))
}

async fn find_files_via_command(
    substrate: &Arc<dyn Substrate>,
    pattern: &str,
    root: &str,
    max_results: usize,
) -> Result<Value, String> {
    let cmd = format!(
        "find {} -name {} -type f",
        shell_quote(root),
        shell_quote(pattern)
    );
    let out = substrate.run_command(&cmd, Some(30.0)).await?;
    let mut files: Vec<String> = out
        .stdout
        .lines()
        .filter(|l| !l.is_empty())
        .map(|s| s.to_string())
        .collect();
    let truncated = files.len() > max_results;
    files.truncate(max_results);
    Ok(json!({
        "files": files,
        "count": files.len(),
        "truncated": truncated,
    }))
}

async fn grep_files_via_command(
    substrate: &Arc<dyn Substrate>,
    pattern: &str,
    root: &str,
    max_results: usize,
) -> Result<Value, String> {
    let cmd = format!(
        "grep -rnE {} {}",
        shell_quote(pattern),
        shell_quote(root)
    );
    let out = substrate.run_command(&cmd, Some(30.0)).await?;
    let mut matches = Vec::new();
    for line in out.stdout.lines() {
        if matches.len() >= max_results {
            break;
        }
        // grep -rn format: path:line:text
        let mut parts = line.splitn(3, ':');
        let (Some(p), Some(ln), Some(text)) = (parts.next(), parts.next(), parts.next()) else {
            continue;
        };
        let Ok(line_no) = ln.parse::<usize>() else {
            continue;
        };
        matches.push(json!({ "path": p, "line": line_no, "text": text }));
    }
    let truncated = matches.len() >= max_results;
    Ok(json!({
        "matches": matches,
        "count": matches.len(),
        "truncated": truncated,
    }))
}

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

    #[test]
    fn glob_patterns_match_file_names() {
        let regex = glob_to_regex("*.rs").unwrap();
        assert!(regex.is_match("lib.rs"));
        assert!(!regex.is_match("lib.ts"));
    }

    #[tokio::test]
    async fn read_write_roundtrip_against_local_substrate() {
        let substrate: Arc<dyn Substrate> = Arc::new(crate::substrate::LocalSubstrate::new());
        let dir = std::env::temp_dir().join(format!(
            "car-agent-basics-{}",
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join("note.txt").to_string_lossy().to_string();

        let w = exec_write_file(&substrate, &json!({ "path": path, "content": "abc" }))
            .await
            .unwrap();
        assert_eq!(w["bytes_written"], 3);

        let r = exec_read_file(&substrate, &json!({ "path": path }))
            .await
            .unwrap();
        assert_eq!(r["content"], "abc");
        assert_eq!(r["size_bytes"], 3);

        // append composes correctly through the substrate
        exec_write_file(
            &substrate,
            &json!({ "path": path, "content": "def", "append": true }),
        )
        .await
        .unwrap();
        let r2 = exec_read_file(&substrate, &json!({ "path": path }))
            .await
            .unwrap();
        assert_eq!(r2["content"], "abcdef");

        // edit unique-match
        let e = exec_edit_file(
            &substrate,
            &json!({ "path": path, "old_text": "abc", "new_text": "XYZ" }),
        )
        .await
        .unwrap();
        assert!(e["edited"].is_string());
        let r3 = exec_read_file(&substrate, &json!({ "path": path }))
            .await
            .unwrap();
        assert_eq!(r3["content"], "XYZdef");

        std::fs::remove_dir_all(&dir).ok();
    }

    #[tokio::test]
    async fn calculate_is_pure() {
        let substrate: Arc<dyn Substrate> = Arc::new(crate::substrate::LocalSubstrate::new());
        let out = execute(&substrate, "calculate", &json!({ "expression": "2 + 3 * 4" }))
            .await
            .unwrap()
            .unwrap();
        assert_eq!(out["result"], 14.0);
    }

    #[tokio::test]
    async fn unknown_tool_returns_none() {
        let substrate: Arc<dyn Substrate> = Arc::new(crate::substrate::LocalSubstrate::new());
        assert!(execute(&substrate, "nope", &json!({})).await.is_none());
    }

    /// Evaluate via the public `calculate` tool contract and return the
    /// `result` float, so the test exercises the same path the runtime does.
    fn calc(expr: &str) -> f64 {
        exec_calculate(&json!({ "expression": expr }))
            .unwrap_or_else(|e| panic!("calculate({expr:?}) failed: {e}"))
            .get("result")
            .and_then(|v| v.as_f64())
            .unwrap_or_else(|| panic!("calculate({expr:?}) returned no numeric result"))
    }

    /// Pins the math semantics of the `calculate` builtin independently of the
    /// backend evaluator. The load-bearing case is `^`: this tool is a
    /// calculator, so `^` MUST mean exponentiation (2^3 == 8), not the
    /// bitwise-XOR meaning it carries in C-family languages. The tool
    /// description documents this contract for the model; this test enforces
    /// it for the implementation.
    #[test]
    fn calculate_contract_semantics() {
        assert_eq!(calc("2 + 3 * 4"), 14.0, "operator precedence");
        assert_eq!(calc("(1 + 2) * 3"), 9.0, "parentheses override precedence");
        assert_eq!(calc("2^3"), 8.0, "^ is exponentiation, not XOR");
        assert_eq!(calc("2^10"), 1024.0, "^ is exponentiation");
        assert_eq!(calc("10 % 3"), 1.0, "modulo");
        assert_eq!(calc("-5 + 2"), -3.0, "unary minus");
        // Built-in functions (fasteval-native).
        assert_eq!(calc("sin(0)"), 0.0, "native function: sin");
        assert_eq!(calc("abs(-3)"), 3.0, "native function: abs");
        // Functions/constants filled by our namespace shim.
        assert_eq!(calc("sqrt(16)"), 4.0, "shim function: sqrt");
        assert!((calc("ln(e)") - 1.0).abs() < 1e-12, "shim: ln + e constant");
        assert!((calc("pi") - std::f64::consts::PI).abs() < 1e-12, "shim: pi constant");
    }

    #[test]
    fn calculate_rejects_invalid_expression() {
        assert!(exec_calculate(&json!({ "expression": "2 +" })).is_err());
        assert!(exec_calculate(&json!({})).is_err(), "missing parameter");
    }
}
