//! Agent tools — file operations, shell, search, graph queries.
//!
//! Each tool returns a string result that gets sent back to the LLM.

use crate::git;
use crate::graph;
use crate::provider::{ToolDef, FunctionDef};
use sqlx::PgPool;
use std::path::Path;
use tokio::process::Command;

/// Execute a tool call by name. Returns the output string.
pub async fn execute_tool(
    name: &str,
    args: &serde_json::Value,
    working_dir: &Path,
    pg_pool: Option<&PgPool>,
) -> String {
    let result = match name {
        "read_file" => read_file(args, working_dir).await,
        "write_file" => write_file(args, working_dir).await,
        "edit_file" => edit_file(args, working_dir).await,
        "bash" => bash(args, working_dir).await,
        "grep" => grep(args, working_dir).await,
        "glob" => glob_search(args, working_dir).await,
        "list_files" => list_files(args, working_dir).await,
        "git" => git_tool(args, working_dir).await,
        "synops_query" => synops_query(args, pg_pool).await,
        _ => Err(format!("Unknown tool: {}", name)),
    };

    match result {
        Ok(output) => output,
        Err(e) => format!("Error: {}", e),
    }
}

/// All available tool definitions (JSON Schema for the LLM).
pub fn tool_definitions() -> Vec<ToolDef> {
    vec![
        ToolDef {
            r#type: "function".into(),
            function: FunctionDef {
                name: "read_file".into(),
                description: "Read a file from disk. Returns the file contents. Use offset/limit for large files.".into(),
                parameters: serde_json::json!({
                    "type": "object",
                    "properties": {
                        "file_path": { "type": "string", "description": "Absolute or relative path to the file" },
                        "offset": { "type": "integer", "description": "Line number to start reading from (1-based, optional)" },
                        "limit": { "type": "integer", "description": "Number of lines to read (optional)" }
                    },
                    "required": ["file_path"]
                }),
            },
        },
        ToolDef {
            r#type: "function".into(),
            function: FunctionDef {
                name: "write_file".into(),
                description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does.".into(),
                parameters: serde_json::json!({
                    "type": "object",
                    "properties": {
                        "file_path": { "type": "string", "description": "Path to the file" },
                        "content": { "type": "string", "description": "Content to write" }
                    },
                    "required": ["file_path", "content"]
                }),
            },
        },
        ToolDef {
            r#type: "function".into(),
            function: FunctionDef {
                name: "edit_file".into(),
                description: "Find and replace text in a file. old_string must match exactly and uniquely.".into(),
                parameters: serde_json::json!({
                    "type": "object",
                    "properties": {
                        "file_path": { "type": "string", "description": "Path to the file" },
                        "old_string": { "type": "string", "description": "Exact text to find" },
                        "new_string": { "type": "string", "description": "Text to replace with" },
                        "replace_all": { "type": "boolean", "description": "Replace all occurrences (default: false)" }
                    },
                    "required": ["file_path", "old_string", "new_string"]
                }),
            },
        },
        ToolDef {
            r#type: "function".into(),
            function: FunctionDef {
                name: "bash".into(),
                description: "Execute a shell command. Returns stdout and stderr.".into(),
                parameters: serde_json::json!({
                    "type": "object",
                    "properties": {
                        "command": { "type": "string", "description": "The command to execute" },
                        "timeout_ms": { "type": "integer", "description": "Timeout in milliseconds (default: 30000)" }
                    },
                    "required": ["command"]
                }),
            },
        },
        ToolDef {
            r#type: "function".into(),
            function: FunctionDef {
                name: "grep".into(),
                description: "Search file contents using regex. Returns matching lines with file paths.".into(),
                parameters: serde_json::json!({
                    "type": "object",
                    "properties": {
                        "pattern": { "type": "string", "description": "Regex pattern to search for" },
                        "path": { "type": "string", "description": "Directory or file to search in (default: current dir)" },
                        "include": { "type": "string", "description": "Glob pattern for files to include (e.g. '*.rs')" }
                    },
                    "required": ["pattern"]
                }),
            },
        },
        ToolDef {
            r#type: "function".into(),
            function: FunctionDef {
                name: "glob".into(),
                description: "Find files matching a glob pattern. Returns file paths.".into(),
                parameters: serde_json::json!({
                    "type": "object",
                    "properties": {
                        "pattern": { "type": "string", "description": "Glob pattern (e.g. '**/*.rs', 'src/**/*.ts')" },
                        "path": { "type": "string", "description": "Base directory (default: current dir)" }
                    },
                    "required": ["pattern"]
                }),
            },
        },
        ToolDef {
            r#type: "function".into(),
            function: FunctionDef {
                name: "list_files".into(),
                description: "List files and directories in a path.".into(),
                parameters: serde_json::json!({
                    "type": "object",
                    "properties": {
                        "path": { "type": "string", "description": "Directory to list (default: current dir)" }
                    }
                }),
            },
        },
        ToolDef {
            r#type: "function".into(),
            function: FunctionDef {
                name: "git".into(),
                description: "Run git operations. Subcommands: status, log, diff, blame, commit, push, branch, checkout.".into(),
                parameters: serde_json::json!({
                    "type": "object",
                    "properties": {
                        "subcommand": {
                            "type": "string",
                            "description": "Git operation: status, log, diff, blame, commit, push, branch, checkout",
                            "enum": ["status", "log", "diff", "blame", "commit", "push", "branch", "checkout"]
                        },
                        "message": { "type": "string", "description": "Commit message (for 'commit')" },
                        "file": { "type": "string", "description": "File path (for 'blame')" },
                        "start_line": { "type": "integer", "description": "Start line for blame range" },
                        "end_line": { "type": "integer", "description": "End line for blame range" },
                        "count": { "type": "integer", "description": "Number of log entries (default: 20)" },
                        "branch_name": { "type": "string", "description": "Branch name (for 'branch' or 'checkout')" }
                    },
                    "required": ["subcommand"]
                }),
            },
        },
        ToolDef {
            r#type: "function".into(),
            function: FunctionDef {
                name: "synops_query".into(),
                description: "Query Synops graph (nodes and edges). Use action 'nodes' to list nodes, 'edges' to list edges for a node, 'get' to read a single node.".into(),
                parameters: serde_json::json!({
                    "type": "object",
                    "properties": {
                        "action": {
                            "type": "string",
                            "description": "Query action",
                            "enum": ["nodes", "edges", "get"]
                        },
                        "node_id": { "type": "string", "description": "Node UUID (for 'edges' and 'get')" },
                        "node_kind": { "type": "string", "description": "Filter by node_kind (for 'nodes')" },
                        "status": { "type": "string", "description": "Filter by metadata.status (for 'nodes')" },
                        "edge_type": { "type": "string", "description": "Filter by edge_type (for 'edges')" },
                        "direction": { "type": "string", "description": "Edge direction: outgoing, incoming, both (default: both)", "enum": ["outgoing", "incoming", "both"] },
                        "limit": { "type": "integer", "description": "Max results (default: 20)" }
                    },
                    "required": ["action"]
                }),
            },
        },
    ]
}

// ============================================================================
// Tool implementations
// ============================================================================

async fn read_file(
    args: &serde_json::Value,
    working_dir: &Path,
) -> Result<String, String> {
    let file_path = args["file_path"]
        .as_str()
        .ok_or("file_path is required")?;

    let path = resolve_path(file_path, working_dir);
    let content = tokio::fs::read_to_string(&path)
        .await
        .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;

    let lines: Vec<&str> = content.lines().collect();
    let offset = args["offset"].as_u64().unwrap_or(1).max(1) as usize - 1;
    let limit = args["limit"].as_u64().map(|l| l as usize);

    let end = limit
        .map(|l| (offset + l).min(lines.len()))
        .unwrap_or(lines.len());

    let selected: Vec<String> = lines[offset.min(lines.len())..end]
        .iter()
        .enumerate()
        .map(|(i, line)| format!("{:>5}\t{}", offset + i + 1, line))
        .collect();

    Ok(selected.join("\n"))
}

async fn write_file(
    args: &serde_json::Value,
    working_dir: &Path,
) -> Result<String, String> {
    let file_path = args["file_path"]
        .as_str()
        .ok_or("file_path is required")?;
    let content = args["content"]
        .as_str()
        .ok_or("content is required")?;

    let path = resolve_path(file_path, working_dir);

    // Create parent directories
    if let Some(parent) = path.parent() {
        tokio::fs::create_dir_all(parent)
            .await
            .map_err(|e| format!("Failed to create directories: {}", e))?;
    }

    tokio::fs::write(&path, content)
        .await
        .map_err(|e| format!("Failed to write {}: {}", path.display(), e))?;

    Ok(format!("Wrote {} bytes to {}", content.len(), path.display()))
}

async fn edit_file(
    args: &serde_json::Value,
    working_dir: &Path,
) -> Result<String, String> {
    let file_path = args["file_path"]
        .as_str()
        .ok_or("file_path is required")?;
    let old_string = args["old_string"]
        .as_str()
        .ok_or("old_string is required")?;
    let new_string = args["new_string"]
        .as_str()
        .ok_or("new_string is required")?;
    let replace_all = args["replace_all"].as_bool().unwrap_or(false);

    let path = resolve_path(file_path, working_dir);
    let content = tokio::fs::read_to_string(&path)
        .await
        .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;

    let count = content.matches(old_string).count();
    if count == 0 {
        return Err(format!(
            "old_string not found in {}",
            path.display()
        ));
    }
    if count > 1 && !replace_all {
        return Err(format!(
            "old_string found {} times in {} — use replace_all or provide more context",
            count,
            path.display()
        ));
    }

    let new_content = if replace_all {
        content.replace(old_string, new_string)
    } else {
        content.replacen(old_string, new_string, 1)
    };

    tokio::fs::write(&path, &new_content)
        .await
        .map_err(|e| format!("Failed to write {}: {}", path.display(), e))?;

    Ok(format!(
        "Replaced {} occurrence(s) in {}",
        if replace_all { count } else { 1 },
        path.display()
    ))
}

async fn bash(
    args: &serde_json::Value,
    working_dir: &Path,
) -> Result<String, String> {
    let command = args["command"]
        .as_str()
        .ok_or("command is required")?;
    let timeout_ms = args["timeout_ms"].as_u64().unwrap_or(30_000);

    let output = tokio::time::timeout(
        std::time::Duration::from_millis(timeout_ms),
        Command::new("bash")
            .arg("-c")
            .arg(command)
            .current_dir(working_dir)
            .output(),
    )
    .await
    .map_err(|_| format!("Command timed out after {}ms", timeout_ms))?
    .map_err(|e| format!("Failed to execute: {}", e))?;

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    let mut result = String::new();
    if !stdout.is_empty() {
        result.push_str(&stdout);
    }
    if !stderr.is_empty() {
        if !result.is_empty() {
            result.push('\n');
        }
        result.push_str("STDERR:\n");
        result.push_str(&stderr);
    }
    if !output.status.success() {
        result.push_str(&format!("\nExit code: {}", output.status.code().unwrap_or(-1)));
    }

    // Truncate very long output
    if result.len() > 50_000 {
        result.truncate(50_000);
        result.push_str("\n... (truncated)");
    }

    Ok(result)
}

async fn grep(
    args: &serde_json::Value,
    working_dir: &Path,
) -> Result<String, String> {
    let pattern = args["pattern"]
        .as_str()
        .ok_or("pattern is required")?;
    let path = args["path"]
        .as_str()
        .unwrap_or(".");
    let include = args["include"].as_str();

    let mut cmd_str = format!("rg -n --color=never '{}' {}", pattern, path);
    if let Some(inc) = include {
        cmd_str = format!("rg -n --color=never --glob '{}' '{}' {}", inc, pattern, path);
    }

    bash(
        &serde_json::json!({ "command": cmd_str, "timeout_ms": 10000 }),
        working_dir,
    )
    .await
}

async fn glob_search(
    args: &serde_json::Value,
    working_dir: &Path,
) -> Result<String, String> {
    let pattern = args["pattern"]
        .as_str()
        .ok_or("pattern is required")?;
    let base = args["path"].as_str().unwrap_or(".");

    let cmd_str = format!("find {} -path '{}' -type f 2>/dev/null | sort | head -100", base, pattern);

    bash(
        &serde_json::json!({ "command": cmd_str, "timeout_ms": 10000 }),
        working_dir,
    )
    .await
}

async fn list_files(
    args: &serde_json::Value,
    working_dir: &Path,
) -> Result<String, String> {
    let path = args["path"].as_str().unwrap_or(".");

    bash(
        &serde_json::json!({ "command": format!("ls -la {}", path), "timeout_ms": 5000 }),
        working_dir,
    )
    .await
}

// ============================================================================
// Helpers
// ============================================================================

async fn git_tool(
    args: &serde_json::Value,
    working_dir: &Path,
) -> Result<String, String> {
    let subcommand = args["subcommand"]
        .as_str()
        .ok_or("subcommand is required")?;

    match subcommand {
        "status" => git::status(working_dir).await,
        "log" => {
            let count = args["count"].as_u64().unwrap_or(20) as usize;
            git::log_oneline(working_dir, count).await
        }
        "diff" => git::diff(working_dir).await,
        "blame" => {
            let file = args["file"].as_str().ok_or("file is required for blame")?;
            let start = args["start_line"].as_u64().map(|n| n as u32);
            let end = args["end_line"].as_u64().map(|n| n as u32);
            git::blame(working_dir, file, start, end).await
        }
        "commit" => {
            let message = args["message"]
                .as_str()
                .ok_or("message is required for commit")?;
            git::commit(working_dir, message).await
        }
        "push" => git::push(working_dir).await,
        "branch" => {
            let name = args["branch_name"]
                .as_str()
                .ok_or("branch_name is required")?;
            git::create_branch(working_dir, name).await
        }
        "checkout" => {
            let name = args["branch_name"]
                .as_str()
                .ok_or("branch_name is required")?;
            git::checkout(working_dir, name).await
        }
        _ => Err(format!("Unknown git subcommand: {}", subcommand)),
    }
}

fn resolve_path(file_path: &str, working_dir: &Path) -> std::path::PathBuf {
    let p = Path::new(file_path);
    if p.is_absolute() {
        p.to_path_buf()
    } else {
        working_dir.join(p)
    }
}

async fn synops_query(
    args: &serde_json::Value,
    pg_pool: Option<&PgPool>,
) -> Result<String, String> {
    let pool = pg_pool.ok_or("PG not connected (DATABASE_URL not set)")?;
    let action = args["action"].as_str().ok_or("action is required")?;
    let limit = args["limit"].as_i64().unwrap_or(20);

    match action {
        "nodes" => {
            let node_kind = args["node_kind"].as_str();
            let status = args["status"].as_str();
            let nodes = graph::query_nodes(pool, node_kind, status, limit).await?;

            if nodes.is_empty() {
                return Ok("No nodes found.".into());
            }

            let mut out = String::new();
            for n in &nodes {
                let status = n
                    .metadata
                    .as_ref()
                    .and_then(|m| m.get("status"))
                    .and_then(|s| s.as_str())
                    .unwrap_or("-");
                out.push_str(&format!(
                    "{} | {} | {} | {} | {}\n",
                    n.id,
                    n.node_kind,
                    n.title.as_deref().unwrap_or("(untitled)"),
                    status,
                    n.created_at.format("%Y-%m-%d %H:%M"),
                ));
            }
            Ok(out)
        }
        "edges" => {
            let node_id_str = args["node_id"].as_str().ok_or("node_id is required for 'edges'")?;
            let node_id = uuid::Uuid::parse_str(node_id_str)
                .map_err(|e| format!("Invalid UUID: {e}"))?;
            let edge_type = args["edge_type"].as_str();
            let direction = match args["direction"].as_str().unwrap_or("both") {
                "outgoing" => graph::EdgeDirection::Outgoing,
                "incoming" => graph::EdgeDirection::Incoming,
                _ => graph::EdgeDirection::Both,
            };

            let edges = graph::query_edges(pool, node_id, direction, edge_type, limit).await?;

            if edges.is_empty() {
                return Ok("No edges found.".into());
            }

            let mut out = String::new();
            for e in &edges {
                out.push_str(&format!(
                    "{} --[{}]--> {} | {} ({})\n",
                    e.source_id,
                    e.edge_type,
                    e.target_id,
                    e.other_title.as_deref().unwrap_or("(untitled)"),
                    e.other_kind.as_deref().unwrap_or("?"),
                ));
            }
            Ok(out)
        }
        "get" => {
            let node_id_str = args["node_id"].as_str().ok_or("node_id is required for 'get'")?;
            let node_id = uuid::Uuid::parse_str(node_id_str)
                .map_err(|e| format!("Invalid UUID: {e}"))?;

            let node = graph::get_node(pool, node_id)
                .await?
                .ok_or_else(|| format!("Node not found: {node_id}"))?;

            let meta_str = node
                .metadata
                .as_ref()
                .map(|m| serde_json::to_string_pretty(m).unwrap_or_default())
                .unwrap_or_default();

            Ok(format!(
                "ID: {}\nKind: {}\nTitle: {}\nVisibility: {}\nCreated: {}\nMetadata: {}",
                node.id,
                node.node_kind,
                node.title.as_deref().unwrap_or("(untitled)"),
                node.visibility,
                node.created_at.format("%Y-%m-%d %H:%M"),
                meta_str,
            ))
        }
        _ => Err(format!("Unknown action: {action}. Use 'nodes', 'edges', or 'get'.")),
    }
}
