//! The hephaes tool set (plan §10). Used by raw-model backends to read the repo
//! and write the wiki; the sandbox confines all writes to the wiki dir. Tool
//! results are returned to the model as plain text (errors prefixed `ERROR:`).

use crate::agent::sandbox::{safe_join, Sandbox};
use crate::wiki::{index, log};
use ignore::WalkBuilder;
use serde_json::{json, Value};
use std::path::{Path, PathBuf};

/// Execution context shared by all tool invocations within a run.
pub struct ToolCtx {
    pub repo_root: PathBuf,
    pub wiki_dir: PathBuf,
    pub sandbox: Sandbox,
    pub repo_map: String,
    pub read_only: bool,
}

impl ToolCtx {
    pub fn new(repo_root: &Path, wiki_dir: &Path, repo_map: String, read_only: bool) -> ToolCtx {
        ToolCtx {
            repo_root: repo_root.to_path_buf(),
            wiki_dir: wiki_dir.to_path_buf(),
            sandbox: Sandbox::new(wiki_dir),
            repo_map,
            read_only,
        }
    }
}

/// A tool definition exposed to the model (name + description + JSON Schema).
#[derive(Debug, Clone)]
pub struct ToolSchema {
    pub name: &'static str,
    pub description: &'static str,
    pub parameters: Value,
}

const READ_CAP: usize = 24_000;
const MAP_CAP: usize = 18_000;
const GREP_MATCHES: usize = 80;
const WALK_FILES: usize = 5_000;

/// The tools available for a run. Write tools are omitted when `read_only`.
pub fn schemas(read_only: bool) -> Vec<ToolSchema> {
    let mut v = vec![
        ToolSchema {
            name: "read_repo_map",
            description: "Return the precomputed repository map (tree + top-level symbols).",
            parameters: json!({"type":"object","properties":{}}),
        },
        ToolSchema {
            name: "list_dir",
            description: "List entries of a directory within the repository.",
            parameters: json!({
                "type":"object",
                "properties":{"path":{"type":"string","description":"Repo-relative dir (default '.')"}}
            }),
        },
        ToolSchema {
            name: "glob",
            description: "Find repository files matching a glob pattern (e.g. 'src/**/*.rs').",
            parameters: json!({
                "type":"object",
                "properties":{"pattern":{"type":"string"}},
                "required":["pattern"]
            }),
        },
        ToolSchema {
            name: "grep",
            description: "Search repository text files for a regex (falls back to substring).",
            parameters: json!({
                "type":"object",
                "properties":{
                    "pattern":{"type":"string"},
                    "path":{"type":"string","description":"Optional repo-relative subdir to limit search"}
                },
                "required":["pattern"]
            }),
        },
        ToolSchema {
            name: "read_file",
            description: "Read a source file (optionally a line range).",
            parameters: json!({
                "type":"object",
                "properties":{
                    "path":{"type":"string"},
                    "start_line":{"type":"integer"},
                    "end_line":{"type":"integer"}
                },
                "required":["path"]
            }),
        },
        ToolSchema {
            name: "list_pages",
            description: "List existing wiki pages with their titles.",
            parameters: json!({"type":"object","properties":{}}),
        },
        ToolSchema {
            name: "read_page",
            description: "Read a wiki page (path relative to the wiki dir).",
            parameters: json!({
                "type":"object",
                "properties":{"path":{"type":"string"}},
                "required":["path"]
            }),
        },
    ];

    if !read_only {
        v.push(ToolSchema {
            name: "write_page",
            description: "Create or overwrite a wiki page. Path is relative to the wiki dir \
                          (e.g. 'modules/auth.md'). Include YAML frontmatter per HEPHAES.md.",
            parameters: json!({
                "type":"object",
                "properties":{"path":{"type":"string"},"content":{"type":"string"}},
                "required":["path","content"]
            }),
        });
        v.push(ToolSchema {
            name: "append_log",
            description: "Append a dated entry to log.md.",
            parameters: json!({
                "type":"object",
                "properties":{
                    "kind":{"type":"string","description":"e.g. ingest, update, query"},
                    "title":{"type":"string"},
                    "details":{"type":"string"}
                },
                "required":["title"]
            }),
        });
        v.push(ToolSchema {
            name: "update_index",
            description:
                "Rebuild index.md from current page frontmatter. Call after editing pages.",
            parameters: json!({"type":"object","properties":{}}),
        });
    }
    v
}

/// Execute a tool call, returning the result text the model will see.
pub fn execute(ctx: &ToolCtx, name: &str, input: &Value) -> String {
    let result = match name {
        "read_repo_map" => Ok(crate::util::truncate_chars(&ctx.repo_map, MAP_CAP)),
        "list_dir" => tool_list_dir(ctx, input),
        "glob" => tool_glob(ctx, input),
        "grep" => tool_grep(ctx, input),
        "read_file" => tool_read_file(ctx, input),
        "list_pages" => Ok(tool_list_pages(ctx)),
        "read_page" => tool_read_page(ctx, input),
        "write_page" => tool_write_page(ctx, input),
        "append_log" => tool_append_log(ctx, input),
        "update_index" => tool_update_index(ctx),
        other => Err(anyhow::anyhow!("unknown tool: {other}")),
    };
    match result {
        Ok(s) if s.is_empty() => "(empty result)".to_string(),
        Ok(s) => s,
        Err(e) => format!("ERROR: {e:#}"),
    }
}

fn arg_str<'a>(input: &'a Value, key: &str) -> Option<&'a str> {
    input
        .get(key)
        .and_then(|v| v.as_str())
        .filter(|s| !s.is_empty())
}

fn tool_list_dir(ctx: &ToolCtx, input: &Value) -> anyhow::Result<String> {
    let rel = arg_str(input, "path").unwrap_or(".");
    let dir = safe_join(&ctx.repo_root, rel)?;
    let mut names: Vec<String> = Vec::new();
    for entry in std::fs::read_dir(&dir)?.flatten() {
        let n = entry.file_name().to_string_lossy().to_string();
        if n.starts_with('.') {
            continue;
        }
        let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
        names.push(if is_dir { format!("{n}/") } else { n });
    }
    names.sort();
    names.truncate(300);
    Ok(names.join("\n"))
}

fn tool_glob(ctx: &ToolCtx, input: &Value) -> anyhow::Result<String> {
    let pattern = arg_str(input, "pattern").ok_or_else(|| anyhow::anyhow!("'pattern' required"))?;
    let glob = globset::Glob::new(pattern)?.compile_matcher();
    let mut out = Vec::new();
    for path in walk_files(&ctx.repo_root, WALK_FILES) {
        let rel = rel_to(&ctx.repo_root, &path);
        if glob.is_match(&rel) {
            out.push(rel);
            if out.len() >= 200 {
                break;
            }
        }
    }
    out.sort();
    if out.is_empty() {
        Ok(format!("No files match '{pattern}'"))
    } else {
        Ok(out.join("\n"))
    }
}

fn tool_grep(ctx: &ToolCtx, input: &Value) -> anyhow::Result<String> {
    let pattern = arg_str(input, "pattern").ok_or_else(|| anyhow::anyhow!("'pattern' required"))?;
    let base = match arg_str(input, "path") {
        Some(p) => safe_join(&ctx.repo_root, p)?,
        None => ctx.repo_root.clone(),
    };
    let re = regex::Regex::new(pattern).ok();
    let mut out = String::new();
    let mut matches = 0usize;
    'outer: for path in walk_files(&base, WALK_FILES) {
        let content = match std::fs::read_to_string(&path) {
            Ok(c) => c,
            Err(_) => continue,
        };
        let rel = rel_to(&ctx.repo_root, &path);
        for (i, line) in content.lines().enumerate() {
            let hit = match &re {
                Some(r) => r.is_match(line),
                None => line.contains(pattern),
            };
            if hit {
                out.push_str(&format!("{}:{}: {}\n", rel, i + 1, line.trim()));
                matches += 1;
                if matches >= GREP_MATCHES {
                    out.push_str("… (more matches truncated)\n");
                    break 'outer;
                }
            }
        }
    }
    if matches == 0 {
        Ok(format!("No matches for /{pattern}/"))
    } else {
        Ok(out)
    }
}

fn tool_read_file(ctx: &ToolCtx, input: &Value) -> anyhow::Result<String> {
    let rel = arg_str(input, "path").ok_or_else(|| anyhow::anyhow!("'path' required"))?;
    let path = safe_join(&ctx.repo_root, rel)?;
    let content = std::fs::read_to_string(&path)?;
    let start = input.get("start_line").and_then(|v| v.as_u64());
    let end = input.get("end_line").and_then(|v| v.as_u64());
    let sliced = if start.is_some() || end.is_some() {
        let s = start.unwrap_or(1).max(1) as usize;
        let e = end.unwrap_or(u64::MAX) as usize;
        content
            .lines()
            .enumerate()
            .filter(|(i, _)| {
                let ln = i + 1;
                ln >= s && ln <= e
            })
            .map(|(i, l)| format!("{}: {}", i + 1, l))
            .collect::<Vec<_>>()
            .join("\n")
    } else {
        content
    };
    Ok(crate::util::truncate_chars(&sliced, READ_CAP))
}

fn tool_list_pages(ctx: &ToolCtx) -> String {
    let pages = index::list_pages(&ctx.wiki_dir);
    if pages.is_empty() {
        return "(no wiki pages yet)".to_string();
    }
    pages
        .iter()
        .map(|p| format!("{} — {} [{}]", p.rel, p.title, p.kind))
        .collect::<Vec<_>>()
        .join("\n")
}

fn tool_read_page(ctx: &ToolCtx, input: &Value) -> anyhow::Result<String> {
    let rel = arg_str(input, "path").ok_or_else(|| anyhow::anyhow!("'path' required"))?;
    let rel = norm_wiki_rel(ctx, rel);
    let content = ctx.sandbox.read(&rel)?;
    Ok(crate::util::truncate_chars(&content, READ_CAP))
}

fn tool_write_page(ctx: &ToolCtx, input: &Value) -> anyhow::Result<String> {
    if ctx.read_only {
        anyhow::bail!("write_page is unavailable in read-only mode");
    }
    let rel = arg_str(input, "path").ok_or_else(|| anyhow::anyhow!("'path' required"))?;
    let content = input
        .get("content")
        .and_then(|v| v.as_str())
        .ok_or_else(|| anyhow::anyhow!("'content' required"))?;
    let rel = norm_wiki_rel(ctx, rel);
    ctx.sandbox.write(&rel, content)?;
    Ok(format!("wrote {rel} ({} bytes)", content.len()))
}

fn tool_append_log(ctx: &ToolCtx, input: &Value) -> anyhow::Result<String> {
    if ctx.read_only {
        anyhow::bail!("append_log is unavailable in read-only mode");
    }
    let kind = arg_str(input, "kind").unwrap_or("note");
    let title = arg_str(input, "title").ok_or_else(|| anyhow::anyhow!("'title' required"))?;
    let details = arg_str(input, "details").unwrap_or("");
    log::append(&ctx.wiki_dir, kind, title, details)?;
    Ok("appended log entry".to_string())
}

fn tool_update_index(ctx: &ToolCtx) -> anyhow::Result<String> {
    if ctx.read_only {
        anyhow::bail!("update_index is unavailable in read-only mode");
    }
    index::rebuild(&ctx.wiki_dir)?;
    Ok("index.md rebuilt".to_string())
}

/// Normalize a wiki-relative path: strip a leading `./` or wiki-dir prefix and
/// ensure a `.md` extension.
fn norm_wiki_rel(ctx: &ToolCtx, rel: &str) -> String {
    let mut r = rel.trim_start_matches("./").to_string();
    if let Some(base) = ctx.wiki_dir.file_name().and_then(|s| s.to_str()) {
        let prefix = format!("{base}/");
        if r.starts_with(&prefix) {
            r = r[prefix.len()..].to_string();
        }
    }
    if !r.ends_with(".md") {
        r.push_str(".md");
    }
    r
}

fn rel_to(root: &Path, path: &Path) -> String {
    path.strip_prefix(root)
        .unwrap_or(path)
        .to_string_lossy()
        .replace('\\', "/")
}

/// Walk text-ish files under `base`, capped, honoring ignore rules.
fn walk_files(base: &Path, cap: usize) -> Vec<PathBuf> {
    let mut out = Vec::new();
    let walker = WalkBuilder::new(base)
        .standard_filters(true)
        .hidden(true)
        .build();
    for entry in walker.flatten() {
        if entry.file_type().map(|t| t.is_file()).unwrap_or(false) {
            out.push(entry.into_path());
            if out.len() >= cap {
                break;
            }
        }
    }
    out
}
