'''
Copyright (C) 2024-2026 Mo Zhou <lumin@debian.org>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
'''
import difflib
import os
import pty
import re
import select
import shlex
import subprocess
import tempfile
from collections import deque

from rich.console import Console
from rich.live import Live
from rich.panel import Panel
from rich.text import Text

class Tool:
    """Base class for all tools."""
    name = ""
    description = ""

    def __call__(self, *args, **kwargs):
        raise NotImplementedError

    parameters = {}

    def to_openai_tool(self) -> dict:
        return {
            "type": "function",
            "function": {
                "name": self.name,
                "description": self.description,
                "parameters": self.parameters,
            }
        }

    def needs_confirmation(self, args_dict: dict) -> tuple:
        """Return (needs_confirm: bool, reason: str) for the given call args.

        Subclasses should override this to require user confirmation for
        dangerous or destructive operations.  The default is no confirmation.
        """
        return False, ""

    def confirmation_context(self, args_dict: dict) -> str:
        """Return extra context to display when asking for confirmation.

        Subclasses can override this to show, e.g., the existing content
        that will be overwritten.  Return an empty string for no extra context.
        """
        return ""


class ManPageReader(Tool):
    """Tool to read man pages."""
    name = "man_page_reader"
    description = "Reads a man page for a given command."

    def __call__(self, command: str) -> str:
        try:
            # We use PAGER=cat to avoid interactive pagination
            result = subprocess.run(
                ["man", command],
                env={"PAGER": "cat", "MANPAGER": "cat", "LC_ALL": "C"},
                capture_output=True,
                text=True,
            )
            if result.returncode != 0:
                return f"Error reading man page for {command}: {result.stderr.strip()}"
            
            # Remove overstriking (e.g., '_\x08x' -> 'x', 'x\x08x' -> 'x')
            text = re.sub(r'.\x08', '', result.stdout)
            return text
        except Exception as e:
            return f"Error: {str(e)}"

    parameters = {
    "type": "object",
    "properties": {
        "command": {
            "type": "string",
            "description": "The command to read the manual page for.",
        }
    },
    "required": ["command"],
        }


class FileReader(Tool):
    """Tool to read file contents, optionally within a line number range."""
    name = "file_reader"
    description = ("Read the contents of a file. Optionally specify "
                    "start_line and/or end_line (1-indexed, inclusive) "
                    "to read only a range of lines.")

    def __call__(self, path: str, start_line: int = None,
                 end_line: int = None) -> str:
        try:
            path = os.path.expanduser(path)
            if not os.path.isfile(path):
                return f"Error: '{path}' is not a file or does not exist."

            with open(path, "r") as f:
                lines = f.readlines()

            total = len(lines)

            # Validate / default the range (1-indexed, inclusive)
            if start_line is not None:
                start_line = int(start_line)
                if start_line < 1:
                    return "Error: start_line must be >= 1."
            else:
                start_line = 1

            if end_line is not None:
                end_line = int(end_line)
                if end_line < start_line:
                    return "Error: end_line must be >= start_line."
            else:
                end_line = total

            if start_line > total:
                return (f"Error: start_line ({start_line}) is beyond the "
                        f"end of the file ({total} lines).")

            # Clamp end_line to the actual file length
            end_line = min(end_line, total)

            selected = lines[start_line - 1 : end_line]
            numbered = [
                f"{start_line + i}: {line.rstrip()}"
                for i, line in enumerate(selected)
            ]
            header = (f"File: {path}  (showing lines {start_line}-{end_line}"
                      f" of {total})")
            return header + "\n" + "\n".join(numbered)
        except Exception as e:
            return f"Error: {str(e)}"

    parameters = {
    "type": "object",
    "properties": {
        "path": {
            "type": "string",
            "description": "Absolute or relative path to "
                           "the file to read.",
        },
        "start_line": {
            "type": "integer",
            "description": "First line to read (1-indexed, "
                           "inclusive). Defaults to 1.",
        },
        "end_line": {
            "type": "integer",
            "description": "Last line to read (1-indexed, "
                           "inclusive). Defaults to the last "
                           "line of the file.",
        },
    },
    "required": ["path"],
        }


class FileWriter(Tool):
    """Tool to write or replace content in a file, optionally within a
    line range."""
    name = "file_writer"
    description = ("Write content to a file. When start_line and end_line "
                    "are omitted the entire file is overwritten (or created). "
                    "When start_line and/or end_line are provided, only that "
                    "range of lines (1-indexed, inclusive) is replaced with "
                    "the new content. You should skip and continue working "
                    "on other files if the user rejects the write on a file.")

    def needs_confirmation(self, args_dict: dict) -> tuple:
        path = os.path.expanduser(args_dict.get("path", ""))
        start = args_dict.get("start_line")
        end = args_dict.get("end_line")
        if start is None and end is None:
            if os.path.isfile(path):
                return True, f"Overwrite entire file: {path}"
            else:
                return True, f"Create new file: {path}"
        else:
            return True, f"Replace lines in: {path}"

    def confirmation_context(self, args_dict: dict) -> str:
        """Build a unified diff between old and new content, highlighted with
        Pygments, and return it as a pre-formatted string for the terminal."""

        try:
            from pygments import highlight as _pyg_highlight
            from pygments.lexers import DiffLexer
            from pygments.formatters import TerminalTrueColorFormatter
            _has_pygments = True
        except ImportError:
            _has_pygments = False

        path = os.path.expanduser(args_dict.get("path", ""))
        new_content = args_dict.get("content", "")
        start = args_dict.get("start_line")
        end = args_dict.get("end_line")

        # --- Determine old lines ----
        if os.path.isfile(path):
            try:
                with open(path, "r") as f:
                    old_lines = [l.rstrip("\n") for l in f.readlines()]
            except Exception as e:
                return f"(unable to read existing file: {e})"
        else:
            old_lines = []

        # --- Determine new lines (simulate what __call__ would produce) ---
        if start is None and end is None:
            # Full-file write / create
            new_lines = new_content.splitlines()
            old_for_diff = old_lines
            new_for_diff = new_lines
            label_old = f"a/{os.path.basename(path)}"
            label_new = f"b/{os.path.basename(path)}"
        else:
            # Range replacement
            total = len(old_lines)
            s = int(start) if start is not None else 1
            e = int(end) if end is not None else total
            e = min(e, total)

            replacement = new_content.splitlines()

            new_all = list(old_lines)
            new_all[s - 1 : e] = replacement

            old_for_diff = old_lines
            new_for_diff = new_all
            label_old = f"a/{os.path.basename(path)}"
            label_new = f"b/{os.path.basename(path)}"

        # --- Generate unified diff ---
        diff_lines = list(difflib.unified_diff(
            old_for_diff, new_for_diff,
            fromfile=label_old, tofile=label_new,
            lineterm="",
        ))

        if not diff_lines:
            return "(no changes)"

        diff_text = "\n".join(diff_lines)
        # Truncate very long diffs
        if len(diff_text) > 4000:
            diff_text = diff_text[:4000] + "\n... (diff truncated)"

        if _has_pygments:
            highlighted = _pyg_highlight(
                diff_text,
                DiffLexer(),
                TerminalTrueColorFormatter(style="monokai"),
            )
            return highlighted.rstrip()
        else:
            return diff_text

    def __call__(self, path: str, content: str, start_line: int = None,
                 end_line: int = None) -> str:
        try:
            path = os.path.expanduser(path)

            # Full-file write / create
            if start_line is None and end_line is None:
                os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
                with open(path, "w") as f:
                    f.write(content)
                line_count = content.count("\n") + (
                    0 if content.endswith("\n") else 1
                )
                return (f"Successfully wrote {line_count} lines to {path}.")

            # Range replacement
            if not os.path.isfile(path):
                return (f"Error: '{path}' does not exist. Cannot do a "
                        "range write on a non-existent file.")

            with open(path, "r") as f:
                lines = f.readlines()

            total = len(lines)

            if start_line is not None:
                start_line = int(start_line)
                if start_line < 1:
                    return "Error: start_line must be >= 1."
            else:
                start_line = 1

            if end_line is not None:
                end_line = int(end_line)
                if end_line < start_line:
                    return "Error: end_line must be >= start_line."
            else:
                end_line = total

            if start_line > total + 1:
                return (f"Error: start_line ({start_line}) is beyond the "
                        f"end of the file ({total} lines).")

            # Clamp end_line
            end_line = min(end_line, total)

            # Build the replacement lines, ensuring each ends with '\n'
            new_lines = content.splitlines(True)
            if new_lines and not new_lines[-1].endswith("\n"):
                new_lines[-1] += "\n"

            lines[start_line - 1 : end_line] = new_lines

            with open(path, "w") as f:
                f.writelines(lines)

            new_total = len(lines)
            return (f"Successfully replaced lines {start_line}-{end_line} "
                    f"in {path}. File now has {new_total} lines.")
        except Exception as e:
            return f"Error: {str(e)}"

    parameters = {
    "type": "object",
    "properties": {
        "path": {
            "type": "string",
            "description": "Absolute or relative path to "
                           "the file to write.",
        },
        "content": {
            "type": "string",
            "description": "The text content to write.",
        },
        "start_line": {
            "type": "integer",
            "description": "First line of the range to "
                           "replace (1-indexed, inclusive). "
                           "Omit for a full-file write.",
        },
        "end_line": {
            "type": "integer",
            "description": "Last line of the range to "
                           "replace (1-indexed, inclusive). "
                           "Omit for a full-file write.",
        },
    },
    "required": ["path", "content"],
        }


class DirectoryLister(Tool):
    """Tool to list directory contents (like the ls command)."""
    name = "directory_lister"
    description = ("List the contents of a directory. Supports options for "
                    "showing hidden files, long format with details, and "
                    "recursive listing.")

    def __call__(self, path: str = ".", all: bool = False,
                 long: bool = False, recursive: bool = False) -> str:
        try:
            path = os.path.expanduser(path)
            if not os.path.exists(path):
                return f"Error: '{path}' does not exist."
            if not os.path.isdir(path):
                return f"Error: '{path}' is not a directory."

            cmd = ["ls"]
            if all:
                cmd.append("-a")
            if long:
                cmd.append("-lh")
            if recursive:
                cmd.append("-R")
            cmd.append(path)

            result = subprocess.run(
                cmd, capture_output=True, text=True
            )
            if result.returncode != 0:
                return f"Error: {result.stderr.strip()}"
            return result.stdout
        except Exception as e:
            return f"Error: {str(e)}"

    parameters = {
    "type": "object",
    "properties": {
        "path": {
            "type": "string",
            "description": "Path to the directory to list. "
                           "Defaults to the current directory.",
        },
        "all": {
            "type": "boolean",
            "description": "If true, include hidden files "
                           "(those starting with '.'). "
                           "Defaults to false.",
        },
        "long": {
            "type": "boolean",
            "description": "If true, use long listing format "
                           "showing permissions, size, and "
                           "modification time. Defaults to "
                           "false.",
        },
        "recursive": {
            "type": "boolean",
            "description": "If true, list subdirectories "
                           "recursively. Defaults to false.",
        },
    },
    "required": [],
        }


class SbuildRunner(Tool):
    """Tool to build a Debian package using sbuild."""
    name = "sbuild_runner"
    description = ("Builds a Debian package using sbuild. By default it runs "
                   "sbuild in the current directory. You should prefer --no-clean-source by default.")

    def needs_confirmation(self, args_dict: dict) -> tuple:
        return False, ""

    def __call__(self, path: str = ".", dist: str = None, arch: str = None, extra_args: str = None) -> str:

        
        try:
            path = os.path.expanduser(path)
            if not os.path.exists(path):
                return f"Error: '{path}' does not exist."
            
            cmd = ["sbuild"]
            if dist:
                cmd.extend(["-d", dist])
            if arch:
                cmd.extend(["--arch", arch])
            
            if extra_args:
                cmd.extend(shlex.split(extra_args))
            
            if path != ".":
                cmd.append(path)
                
            log_fd, log_path = tempfile.mkstemp(prefix="sbuild_", suffix=".log")
            os.close(log_fd)
            

            master_fd, slave_fd = pty.openpty()
            
            with open(log_path, "w") as log_file:
                process = subprocess.Popen(
                    cmd, stdout=slave_fd, stderr=slave_fd, stdin=slave_fd, close_fds=True
                )
                os.close(slave_fd)
                

                console = Console()
                term_height = console.size.height
                max_lines = max(10, term_height - 2)
                
                output_buffer = ""
                output_lines = deque(maxlen=max_lines)
                
                with Live(
                    Panel("Starting sbuild...", title="sbuild", border_style="blue"),
                    refresh_per_second=10,
                    transient=True,
                    console=console
                ) as live:
                    while True:
                        try:
                            r, _, _ = select.select([master_fd], [], [], 0.1)
                        except Exception:
                            break
                            
                        if master_fd in r:
                            try:
                                chunk = os.read(master_fd, 4096)
                            except OSError:
                                break
                                
                            if not chunk:
                                break
                                
                            chunk_str = chunk.decode('utf-8', errors='replace')
                            log_file.write(chunk_str)
                            log_file.flush()
                            
                            output_buffer += chunk_str
                            lines = output_buffer.split('\n')
                            if len(lines) > 1:
                                for line in lines[:-1]:
                                    line = line.rstrip('\r')
                                    if '\r' in line:
                                        line = line.split('\r')[-1]
                                    output_lines.append(line)
                                output_buffer = lines[-1]
                                
                                display_text = "\n".join(list(output_lines) + [output_buffer])
                                live.update(Panel(Text.from_ansi(display_text), title="sbuild", border_style="blue"))
                        elif process.poll() is not None:
                            break
                            
                os.close(master_fd)
                process.wait()
            
            with open(log_path, "r") as f:
                content = f.read()
                
            out = content
            if len(out) > 4000:
                out = "...(truncated log)...\n" + out[-4000:]
                
            ret = f"Status: {process.returncode}\n"
            ret += f"Log path: {log_path}\n"
            if out:
                ret += f"STDOUT:\n{out}\n"
            return ret
        except Exception as e:
            return f"Error: {str(e)}"

    parameters = {
    "type": "object",
    "properties": {
        "path": {
            "type": "string",
            "description": "Path to the package source directory or .dsc file. Defaults to '.'.",
        },
        "dist": {
            "type": "string",
            "description": "Distribution to build for (e.g. 'unstable'). Maps to sbuild -d.",
        },
        "arch": {
            "type": "string",
            "description": "Architecture to build for (e.g. 'amd64'). Maps to sbuild --arch.",
        },
        "extra_args": {
            "type": "string",
            "description": "Additional arguments passed directly to sbuild.",
        },
    },
    "required": [],
        }


class FileFinder(Tool):
    """Tool to find files and directories (wraps the Unix find command)."""
    name = "file_finder"
    description = ("Search for files and directories using the Unix find "
                    "command. Supports filtering by name pattern, file type, "
                    "and max depth. An extra_args parameter is available for "
                    "any additional find options.")

    # Flags that can modify or execute on the filesystem
    _DANGEROUS_FLAGS = {
        "-delete", "-exec", "-execdir", "-ok", "-okdir",
    }

    def needs_confirmation(self, args_dict: dict) -> tuple:
        extra = args_dict.get("extra_args", "") or ""

        tokens = shlex.split(extra)
        found = [t for t in tokens if t in self._DANGEROUS_FLAGS]
        if found:
            flags = ", ".join(found)
            return True, f"Dangerous find flag(s) detected: {flags}"
        return False, ""

    def __call__(self, path: str = ".", name: str = None,
                 type: str = None, maxdepth: int = None,
                 extra_args: str = None) -> str:
        try:
            path = os.path.expanduser(path)
            if not os.path.exists(path):
                return f"Error: '{path}' does not exist."

            cmd = ["find", path]

            if maxdepth is not None:
                cmd.extend(["-maxdepth", str(int(maxdepth))])
            if type is not None:
                cmd.extend(["-type", str(type)])
            if name is not None:
                cmd.extend(["-name", str(name)])
            if extra_args is not None:

                cmd.extend(shlex.split(extra_args))

            result = subprocess.run(
                cmd, capture_output=True, text=True, timeout=30
            )
            if result.returncode != 0 and result.stderr.strip():
                return f"Error: {result.stderr.strip()}"
            return result.stdout if result.stdout else "(no results)"
        except subprocess.TimeoutExpired:
            return "Error: find command timed out after 30 seconds."
        except Exception as e:
            return f"Error: {str(e)}"

    parameters = {
    "type": "object",
    "properties": {
        "path": {
            "type": "string",
            "description": "Starting directory for the "
                           "search. Defaults to '.'.",
        },
        "name": {
            "type": "string",
            "description": "Filename pattern to match "
                           "(supports shell wildcards, e.g. "
                           "'*.py'). Maps to find -name.",
        },
        "type": {
            "type": "string",
            "enum": ["f", "d", "l"],
            "description": "File type filter: 'f' for "
                           "regular files, 'd' for "
                           "directories, 'l' for symbolic "
                           "links.",
        },
        "maxdepth": {
            "type": "integer",
            "description": "Maximum directory depth to "
                           "descend. 0 means only the "
                           "starting path itself.",
        },
        "extra_args": {
            "type": "string",
            "description": "Additional arguments passed "
                           "directly to find (e.g. "
                           "'-mtime -1 -size +1M').",
        },
    },
    "required": [],
        }

class LintianRunner(Tool):
    """Tool to run lintian on a Debian package."""
    name = "lintian_runner"
    description = ("Run lintian, a static analysis tool for Debian packages, "
                   "to dissect packages and report bugs and policy violations. "
                   "By default it checks the package in the current directory.")

    def __call__(self, path: str = ".", info: bool = False, display_info: bool = False,
                 pedantic: bool = False, extra_args: str = None) -> str:

        try:
            path = os.path.expanduser(path)
            if not os.path.exists(path):
                return f"Error: '{path}' does not exist."

            cmd = ["lintian"]
            if info:
                cmd.append("-i")
            if display_info:
                cmd.append("-I")
            if pedantic:
                cmd.append("--pedantic")
            
            if extra_args:
                cmd.extend(shlex.split(extra_args))
                
            cmd.append(path)

            console = Console()
            with console.status(f"Running lintian on {path}..."):
                result = subprocess.run(
                    cmd, capture_output=True, text=True
                )
            
            out = result.stdout
            if result.stderr:
                out += "\nSTDERR:\n" + result.stderr
                
            if len(out) > 4000:
                out = out[:2000] + "\n...(truncated output)...\n" + out[-2000:]
                
            ret = f"Status: {result.returncode}\n"
            if out.strip():
                ret += f"Output:\n{out}\n"
            else:
                ret += "No output (no issues found).\n"
            return ret
        except Exception as e:
            return f"Error: {str(e)}"

    parameters = {
    "type": "object",
    "properties": {
        "path": {
            "type": "string",
            "description": "Path to the .changes, .deb, .dsc file or package source directory. Defaults to '.'.",
        },
        "info": {
            "type": "boolean",
            "description": "If true, print explanatory information about each problem (lintian -i).",
        },
        "display_info": {
            "type": "boolean",
            "description": "If true, display informational tags (lintian -I).",
        },
        "pedantic": {
            "type": "boolean",
            "description": "If true, display pedantic tags (lintian --pedantic).",
        },
        "extra_args": {
            "type": "string",
            "description": "Additional arguments passed directly to lintian.",
        },
    },
    "required": [],
        }


class BashRunner(Tool):
    """Tool to execute arbitrary bash commands."""
    name = "bash_runner"
    description = "Execute an arbitrary bash command. This always requires user confirmation."

    def needs_confirmation(self, args_dict: dict) -> tuple:
        cmd = args_dict.get("command", "")
        return True, f"Execute bash command: {cmd}"

    def __call__(self, command: str, cwd: str = ".") -> str:

        try:
            cwd = os.path.expanduser(cwd)
            if not os.path.exists(cwd):
                return f"Error: '{cwd}' does not exist."

            result = subprocess.run(
                command, shell=True, executable="/bin/bash",
                cwd=cwd, capture_output=True, text=True
            )
            
            out = result.stdout
            if result.stderr:
                out += "\nSTDERR:\n" + result.stderr
                
            if len(out) > 4000:
                out = out[:2000] + "\n...(truncated output)...\n" + out[-2000:]
                
            ret = f"Status: {result.returncode}\n"
            if out.strip():
                ret += f"Output:\n{out}\n"
            else:
                ret += "No output.\n"
            return ret
        except Exception as e:
            return f"Error: {str(e)}"

    parameters = {
    "type": "object",
    "properties": {
        "command": {
            "type": "string",
            "description": "The bash command to execute.",
        },
        "cwd": {
            "type": "string",
            "description": "The current working directory. Defaults to '.'.",
        },
    },
    "required": ["command"],
        }


class CurlRunner(Tool):
    """Tool to execute curl commands."""
    name = "curl_runner"
    description = "Execute a curl command to make HTTP requests."

    def __call__(self, url: str, method: str = "GET", headers: list = None, data: str = None, max_output_chars: int = 4000, extra_args: str = None) -> str:

        try:
            cmd = ["curl", "-s", "-S"]
            if method != "GET":
                cmd.extend(["-X", method])
            if headers:
                for header in headers:
                    cmd.extend(["-H", header])
            if data is not None:
                cmd.extend(["-d", data])
            if extra_args:
                cmd.extend(shlex.split(extra_args))
            cmd.append(url)

            result = subprocess.run(
                cmd, capture_output=True, text=True
            )
            
            out = result.stdout
            if result.stderr:
                out += "\nSTDERR:\n" + result.stderr
                
            limit = int(max_output_chars) if max_output_chars else 4000
            if len(out) > limit:
                half = limit // 2
                out = out[:half] + "\n...(truncated output)...\n" + out[-half:]
                
            ret = f"Status: {result.returncode}\n"
            if out.strip():
                ret += f"Output:\n{out}\n"
            else:
                ret += "No output.\n"
            return ret
        except Exception as e:
            return f"Error: {str(e)}"

    parameters = {
    "type": "object",
    "properties": {
        "url": {
            "type": "string",
            "description": "The URL to request.",
        },
        "method": {
            "type": "string",
            "description": "The HTTP method to use. Defaults to 'GET'.",
        },
        "headers": {
            "type": "array",
            "items": {"type": "string"},
            "description": "A list of HTTP headers to include (e.g. ['Content-Type: application/json']).",
        },
        "data": {
            "type": "string",
            "description": "Data to send in the request body.",
        },
        "max_output_chars": {
            "type": "integer",
            "description": "Maximum number of characters to "
                           "return before truncating the "
                           "output. Defaults to 4000.",
        },
        "extra_args": {
            "type": "string",
            "description": "Additional arguments passed directly to curl.",
        },
    },
    "required": ["url"],
        }


class RipgrepSearcher(Tool):
    """Tool to search file contents using ripgrep (rg)."""
    name = "ripgrep_searcher"
    description = ("Search file contents for a pattern using ripgrep (rg). "
                    "Supports literal and regex patterns, glob filtering, "
                    "case-insensitive search, context lines, file type "
                    "filters, and a files-only mode that lists matching "
                    "filenames without showing the matched lines.")

    def __call__(self, query: str, path: str = ".",
                 glob: str = None, case_insensitive: bool = False,
                 regex: bool = True, files_only: bool = False,
                 context_lines: int = None, max_count: int = None,
                 type_filter: str = None,
                 max_output_chars: int = 4000,
                 extra_args: str = None) -> str:

        try:
            path = os.path.expanduser(path)
            if not os.path.exists(path):
                return f"Error: '{path}' does not exist."

            cmd = ["rg", "--no-heading", "--line-number", "--color=never"]

            if not regex:
                cmd.append("--fixed-strings")
            if case_insensitive:
                cmd.append("--ignore-case")
            if files_only:
                cmd.append("--files-with-matches")
            if context_lines is not None:
                cmd.extend(["--context", str(int(context_lines))])
            if max_count is not None:
                cmd.extend(["--max-count", str(int(max_count))])
            if glob is not None:
                cmd.extend(["--glob", str(glob)])
            if type_filter is not None:
                cmd.extend(["--type", str(type_filter)])
            if extra_args is not None:
                cmd.extend(shlex.split(extra_args))

            cmd.append(query)
            cmd.append(path)

            result = subprocess.run(
                cmd, capture_output=True, text=True, timeout=30
            )

            # rg returns 1 for "no matches" (not an error)
            if result.returncode == 2:
                return f"Error: {result.stderr.strip()}"
            if result.returncode == 1:
                return "(no matches)"

            out = result.stdout
            if not out.strip():
                return "(no matches)"

            limit = int(max_output_chars) if max_output_chars else 4000
            if len(out) > limit:
                out = out[:limit] + "\n...(output truncated)..."
            return out
        except subprocess.TimeoutExpired:
            return "Error: ripgrep timed out after 30 seconds."
        except FileNotFoundError:
            return ("Error: 'rg' (ripgrep) is not installed. "
                    "Install it with: apt install ripgrep")
        except Exception as e:
            return f"Error: {str(e)}"

    parameters = {
    "type": "object",
    "properties": {
        "query": {
            "type": "string",
            "description": "The search pattern (regex by "
                           "default, or literal if regex "
                           "is false).",
        },
        "path": {
            "type": "string",
            "description": "File or directory to search in. "
                           "Defaults to '.'.",
        },
        "glob": {
            "type": "string",
            "description": "Glob pattern to filter files "
                           "(e.g. '*.py'). Prefix with '!' "
                           "to exclude (e.g. '!*.min.js').",
        },
        "case_insensitive": {
            "type": "boolean",
            "description": "If true, search case-"
                           "insensitively. Defaults to false.",
        },
        "regex": {
            "type": "boolean",
            "description": "If true (default), treat query "
                           "as a regex. If false, treat it "
                           "as a literal string.",
        },
        "files_only": {
            "type": "boolean",
            "description": "If true, only list filenames "
                           "that contain a match, without "
                           "showing the matched lines. "
                           "Defaults to false.",
        },
        "context_lines": {
            "type": "integer",
            "description": "Number of lines of context to "
                           "show before and after each match.",
        },
        "max_count": {
            "type": "integer",
            "description": "Maximum number of matches to "
                           "show per file.",
        },
        "type_filter": {
            "type": "string",
            "description": "Restrict search to a known file "
                           "type (e.g. 'py', 'js', 'c', "
                           "'html'). Maps to rg --type.",
        },
        "max_output_chars": {
            "type": "integer",
            "description": "Maximum number of characters to "
                           "return before truncating the "
                           "output. Defaults to 4000.",
        },
        "extra_args": {
            "type": "string",
            "description": "Additional arguments passed "
                           "directly to rg.",
        },
    },
    "required": ["query"],
        }


AVAILABLE_TOOLS = [
    ManPageReader(),
    FileReader(),
    FileWriter(),
    DirectoryLister(),
    FileFinder(),
    SbuildRunner(),
    LintianRunner(),
    BashRunner(),
    CurlRunner(),
    RipgrepSearcher(),
]

def get_tool_by_name(name: str) -> Tool:
    for tool in AVAILABLE_TOOLS:
        if tool.name == name:
            return tool
    return None

def get_openai_tools() -> list:
    return [tool.to_openai_tool() for tool in AVAILABLE_TOOLS]

