"""
doit-fm tools — 20 real, working tools.
Every function is a genuine implementation, zero mocks, zero placeholders.
All return {"error": "..."} on failure, never raise to the caller.
"""
from __future__ import annotations
import asyncio, hashlib, json, os, platform, re, shutil
import subprocess, sys, tarfile, tempfile, time, zipfile
from datetime import datetime
from pathlib import Path
from typing import Any

import psutil

from doit_fm.config import BLOCKED_PATHS, DATA_DIR

logger = __import__("logging").getLogger("doit.tools")


# ── Safety helpers ────────────────────────────────────────────────────────────

def _safe_path(p: str) -> tuple[bool, str]:
    """Return (is_safe, reason). Blocks obviously dangerous paths."""
    resolved = str(Path(p).expanduser().resolve())
    for blocked in BLOCKED_PATHS:
        if resolved.startswith(blocked):
            return False, f"Blocked path: {blocked}"
    return True, ""

def _run_sync(cmd: list[str], timeout: int = 30, input_text: str = None) -> dict:
    """Run a subprocess, return stdout/stderr/returncode dict."""
    try:
        r = subprocess.run(
            cmd, capture_output=True, text=True,
            timeout=timeout, input=input_text,
        )
        return {"stdout": r.stdout.strip(), "stderr": r.stderr.strip(),
                "returncode": r.returncode, "success": r.returncode == 0}
    except FileNotFoundError:
        return {"error": f"Command not found: {cmd[0]}", "success": False}
    except subprocess.TimeoutExpired:
        return {"error": f"Timed out after {timeout}s", "success": False}
    except Exception as e:
        return {"error": str(e), "success": False}

def _has(cmd: str) -> bool:
    return shutil.which(cmd) is not None


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 1 — fs_list: List directory contents
# ═══════════════════════════════════════════════════════════════════════════════
async def fs_list(path: str = "~", **_) -> dict:
    p = Path(path).expanduser().resolve()
    safe, reason = _safe_path(str(p))
    if not safe:
        return {"error": reason}
    if not p.exists():
        return {"error": f"Path not found: {p}"}
    if p.is_file():
        s = p.stat()
        return {"type": "file", "path": str(p), "size": s.st_size,
                "modified": datetime.fromtimestamp(s.st_mtime).isoformat()}
    entries = []
    try:
        for item in sorted(p.iterdir()):
            try:
                s = item.stat()
                entries.append({
                    "name": item.name,
                    "type": "dir" if item.is_dir() else "file",
                    "size": s.st_size if item.is_file() else None,
                    "modified": datetime.fromtimestamp(s.st_mtime).isoformat()[:10],
                })
            except PermissionError:
                entries.append({"name": item.name, "type": "?", "error": "permission denied"})
    except PermissionError:
        return {"error": f"Permission denied: {p}"}
    return {"path": str(p), "entries": entries, "count": len(entries)}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 2 — fs_read: Read a text file
# ═══════════════════════════════════════════════════════════════════════════════
async def fs_read(path: str, **_) -> dict:
    p = Path(path).expanduser().resolve()
    safe, reason = _safe_path(str(p))
    if not safe:
        return {"error": reason}
    if not p.exists():
        return {"error": f"File not found: {p}"}
    if p.stat().st_size > 5 * 1024 * 1024:
        return {"error": "File too large (>5 MB). Use a text editor or shell."}
    try:
        content = p.read_text(encoding="utf-8", errors="replace")
        return {"path": str(p), "content": content, "lines": content.count("\n") + 1}
    except Exception as e:
        return {"error": str(e)}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 3 — fs_write: Write (create or overwrite) a file
# ═══════════════════════════════════════════════════════════════════════════════
async def fs_write(path: str, content: str, **_) -> dict:
    p = Path(path).expanduser().resolve()
    safe, reason = _safe_path(str(p))
    if not safe:
        return {"error": reason}
    try:
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(content, encoding="utf-8")
        return {"path": str(p), "bytes": len(content.encode()), "success": True}
    except Exception as e:
        return {"error": str(e)}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 4 — fs_delete: Delete a file or directory
# ═══════════════════════════════════════════════════════════════════════════════
async def fs_delete(path: str, **_) -> dict:
    p = Path(path).expanduser().resolve()
    safe, reason = _safe_path(str(p))
    if not safe:
        return {"error": reason}
    if not p.exists():
        return {"error": f"Not found: {p}"}
    try:
        if p.is_dir():
            shutil.rmtree(p)
        else:
            p.unlink()
        return {"deleted": str(p), "success": True}
    except Exception as e:
        return {"error": str(e)}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 5 — fs_search: Find files by name or content
# ═══════════════════════════════════════════════════════════════════════════════
async def fs_search(path: str = "~", pattern: str = "*", content: str = None, **_) -> dict:
    p = Path(path).expanduser().resolve()
    safe, reason = _safe_path(str(p))
    if not safe:
        return {"error": reason}
    if not p.exists():
        return {"error": f"Path not found: {p}"}
    results = []
    try:
        for match in p.rglob(pattern):
            entry = {
                "path": str(match),
                "type": "dir" if match.is_dir() else "file",
                "size": match.stat().st_size if match.is_file() else None,
            }
            if content and match.is_file():
                try:
                    text = match.read_text(errors="replace")
                    if content.lower() in text.lower():
                        lines = [l for l in text.splitlines() if content.lower() in l.lower()][:3]
                        entry["matching_lines"] = lines
                    else:
                        continue
                except Exception:
                    pass
            results.append(entry)
            if len(results) >= 100:
                break
    except Exception as e:
        return {"error": str(e)}
    return {"pattern": pattern, "root": str(p), "results": results, "count": len(results)}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 6 — sys_info: Full system information
# ═══════════════════════════════════════════════════════════════════════════════
async def sys_info(**_) -> dict:
    try:
        vm = psutil.virtual_memory()
        disk = psutil.disk_usage("/")
        freq = psutil.cpu_freq()
        boot = datetime.fromtimestamp(psutil.boot_time()).strftime("%Y-%m-%d %H:%M")
        return {
            "os":            f"{platform.system()} {platform.release()}",
            "hostname":      platform.node(),
            "python":        platform.python_version(),
            "cpu_cores":     psutil.cpu_count(logical=False),
            "cpu_threads":   psutil.cpu_count(logical=True),
            "cpu_freq_mhz":  round(freq.current) if freq else None,
            "ram_total_gb":  round(vm.total / 1e9, 1),
            "ram_used_gb":   round(vm.used / 1e9, 1),
            "ram_percent":   vm.percent,
            "disk_total_gb": round(disk.total / 1e9, 1),
            "disk_used_gb":  round(disk.used / 1e9, 1),
            "disk_percent":  disk.percent,
            "uptime_since":  boot,
            "user":          os.environ.get("USER") or os.environ.get("USERNAME", "?"),
            "arch":          platform.machine(),
        }
    except Exception as e:
        return {"error": str(e)}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 7 — sys_monitor: Live CPU / RAM / disk usage
# ═══════════════════════════════════════════════════════════════════════════════
async def sys_monitor(**_) -> dict:
    try:
        cpu = psutil.cpu_percent(interval=0.5)
        vm  = psutil.virtual_memory()
        disk = psutil.disk_usage("/")
        net  = psutil.net_io_counters()
        return {
            "cpu_percent":   cpu,
            "cpu_per_core":  psutil.cpu_percent(interval=0.1, percpu=True),
            "ram_used_gb":   round(vm.used / 1e9, 2),
            "ram_total_gb":  round(vm.total / 1e9, 2),
            "ram_percent":   vm.percent,
            "disk_used_gb":  round(disk.used / 1e9, 2),
            "disk_total_gb": round(disk.total / 1e9, 2),
            "disk_percent":  disk.percent,
            "net_sent_mb":   round(net.bytes_sent / 1e6, 1),
            "net_recv_mb":   round(net.bytes_recv / 1e6, 1),
        }
    except Exception as e:
        return {"error": str(e)}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 8 — sys_processes: Top processes by memory
# ═══════════════════════════════════════════════════════════════════════════════
async def sys_processes(**_) -> dict:
    try:
        procs = []
        for p in psutil.process_iter(["pid", "name", "status", "cpu_percent", "memory_info"]):
            try:
                mi = p.info["memory_info"]
                procs.append({
                    "pid":    p.info["pid"],
                    "name":   p.info["name"],
                    "status": p.info["status"],
                    "cpu":    p.info["cpu_percent"],
                    "mem_mb": round(mi.rss / 1e6, 1) if mi else 0,
                })
            except (psutil.NoSuchProcess, psutil.AccessDenied):
                pass
        procs.sort(key=lambda x: x["mem_mb"], reverse=True)
        return {"processes": procs[:25], "total_visible": len(procs)}
    except Exception as e:
        return {"error": str(e)}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 9 — shell_exec: Run any shell command
# ═══════════════════════════════════════════════════════════════════════════════
BLOCKED_COMMANDS = ["rm -rf /", "mkfs", ":(){:|:&};:", "dd if=/dev/zero of=/dev/"]

async def shell_exec(command: str, cwd: str = None, timeout: int = 60, **_) -> dict:
    for bad in BLOCKED_COMMANDS:
        if bad in command:
            return {"error": f"Blocked command pattern: {bad!r}"}
    cwd_path = Path(cwd).expanduser() if cwd else Path.home()
    try:
        proc = await asyncio.create_subprocess_shell(
            command,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=str(cwd_path),
        )
        try:
            stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
        except asyncio.TimeoutError:
            proc.kill()
            return {"error": f"Command timed out after {timeout}s"}
        return {
            "command":    command,
            "stdout":     stdout.decode(errors="replace")[:4000],
            "stderr":     stderr.decode(errors="replace")[:1000],
            "returncode": proc.returncode,
            "success":    proc.returncode == 0,
        }
    except Exception as e:
        return {"error": str(e)}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 10 — web_search: DuckDuckGo instant answers
# ═══════════════════════════════════════════════════════════════════════════════
async def web_search(query: str, limit: int = 6, **_) -> dict:
    import urllib.request, urllib.parse
    url = "https://api.duckduckgo.com/?" + urllib.parse.urlencode({
        "q": query, "format": "json", "no_redirect": "1", "no_html": "1",
    })
    try:
        try:
            import aiohttp
            async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as sess:
                async with sess.get(url) as resp:
                    data = await resp.json(content_type=None)
        except ImportError:
            req = urllib.request.Request(url, headers={"User-Agent": "doit-fm/3.0"})
            with urllib.request.urlopen(req, timeout=10) as r:
                data = json.loads(r.read())

        results = []
        if data.get("AbstractText"):
            results.append({
                "title":   data.get("Heading", query),
                "snippet": data["AbstractText"],
                "url":     data.get("AbstractURL", ""),
            })
        for item in data.get("RelatedTopics", [])[:limit]:
            if isinstance(item, dict) and item.get("Text"):
                results.append({
                    "title":   item.get("Text", "")[:100],
                    "snippet": item.get("Text", ""),
                    "url":     item.get("FirstURL", ""),
                })
        return {"query": query, "results": results[:limit]}
    except Exception as e:
        return {"error": f"Search failed: {e}"}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 11 — weather: Current weather for any city
# ═══════════════════════════════════════════════════════════════════════════════
async def weather(city: str, **_) -> dict:
    import urllib.request
    url = f"https://wttr.in/{urllib.parse.quote(city)}?format=j1"
    try:
        try:
            import aiohttp
            async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as sess:
                async with sess.get(url, headers={"User-Agent": "doit-fm/3.0"}) as resp:
                    data = await resp.json()
        except ImportError:
            import urllib.parse
            req = urllib.request.Request(url, headers={"User-Agent": "doit-fm/3.0"})
            with urllib.request.urlopen(req, timeout=10) as r:
                data = json.loads(r.read())

        cur = data["current_condition"][0]
        return {
            "city":        city,
            "temp_c":      cur["temp_C"],
            "temp_f":      cur["temp_F"],
            "feels_c":     cur["FeelsLikeC"],
            "humidity":    cur["humidity"],
            "description": cur["weatherDesc"][0]["value"],
            "wind_kmph":   cur["windspeedKmph"],
            "visibility":  cur["visibility"],
        }
    except Exception as e:
        return {"error": f"Weather fetch failed: {e}"}


# Need urllib.parse at module level for weather
import urllib.parse


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 12 — calc: Evaluate a math expression safely
# ═══════════════════════════════════════════════════════════════════════════════
async def calc(expression: str, **_) -> dict:
    import math as _math
    ns = {k: getattr(_math, k) for k in dir(_math) if not k.startswith("_")}
    ns.update({"round": round, "abs": abs, "min": min, "max": max,
               "sum": sum, "len": len, "int": int, "float": float})
    try:
        result = eval(expression, {"__builtins__": {}}, ns)  # noqa: S307
        return {"expression": expression, "result": result}
    except Exception as e:
        return {"error": f"Could not evaluate: {e}"}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 13 — memory_store / memory_recall / memory_delete  (3 tools in one file)
# Backed by a simple JSON file in the data directory.
# ═══════════════════════════════════════════════════════════════════════════════
_MEMORY_FILE = DATA_DIR / "memory.json"

def _load_mem() -> dict:
    try:
        return json.loads(_MEMORY_FILE.read_text()) if _MEMORY_FILE.exists() else {}
    except Exception:
        return {}

def _save_mem(d: dict):
    _MEMORY_FILE.parent.mkdir(parents=True, exist_ok=True)
    _MEMORY_FILE.write_text(json.dumps(d, indent=2))

async def memory_store(key: str, value: str, **_) -> dict:
    m = _load_mem()
    m[key] = {"value": value, "stored": datetime.now().isoformat()[:16]}
    _save_mem(m)
    return {"key": key, "stored": True}

async def memory_recall(key: str = None, **_) -> dict:
    m = _load_mem()
    if key:
        e = m.get(key)
        if not e:
            return {"error": f"Nothing stored for '{key}'"}
        return {"key": key, "value": e["value"], "stored": e.get("stored")}
    return {"memories": {k: v["value"] for k, v in m.items()}, "count": len(m)}

async def memory_delete(key: str, **_) -> dict:
    m = _load_mem()
    if key not in m:
        return {"error": f"Key not found: {key}"}
    del m[key]
    _save_mem(m)
    return {"key": key, "deleted": True}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 14 — note_create / note_list / note_read  (notes as markdown files)
# ═══════════════════════════════════════════════════════════════════════════════
_NOTES_DIR = DATA_DIR / "notes"

async def note_create(title: str, content: str, **_) -> dict:
    _NOTES_DIR.mkdir(parents=True, exist_ok=True)
    safe_title = re.sub(r"[^\w\s\-]", "", title).strip()[:60] or "note"
    p = _NOTES_DIR / f"{safe_title}.md"
    p.write_text(f"# {title}\n\n{content}", encoding="utf-8")
    return {"title": title, "path": str(p), "success": True}

async def note_list(**_) -> dict:
    _NOTES_DIR.mkdir(parents=True, exist_ok=True)
    notes = [
        {"title": f.stem, "size": f.stat().st_size,
         "modified": datetime.fromtimestamp(f.stat().st_mtime).isoformat()[:16]}
        for f in sorted(_NOTES_DIR.glob("*.md"))
    ]
    return {"notes": notes, "count": len(notes)}

async def note_read(title: str, **_) -> dict:
    _NOTES_DIR.mkdir(parents=True, exist_ok=True)
    p = _NOTES_DIR / f"{title}.md"
    if not p.exists():
        matches = list(_NOTES_DIR.glob(f"*{title}*.md"))
        if matches:
            p = matches[0]
        else:
            return {"error": f"Note not found: {title}"}
    return {"title": p.stem, "content": p.read_text(encoding="utf-8")}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 15 — todo_add / todo_list / todo_done  (JSON file todos)
# ═══════════════════════════════════════════════════════════════════════════════
_TODO_FILE = DATA_DIR / "todos.json"

def _load_todos() -> list:
    try:
        return json.loads(_TODO_FILE.read_text()) if _TODO_FILE.exists() else []
    except Exception:
        return []

def _save_todos(t: list):
    _TODO_FILE.parent.mkdir(parents=True, exist_ok=True)
    _TODO_FILE.write_text(json.dumps(t, indent=2))

async def todo_add(task: str, priority: str = "normal", **_) -> dict:
    todos = _load_todos()
    item = {
        "id":       int(time.time() * 1000),
        "task":     task,
        "priority": priority,
        "done":     False,
        "created":  datetime.now().isoformat()[:16],
    }
    todos.append(item)
    _save_todos(todos)
    return {"id": item["id"], "task": task, "added": True}

async def todo_list(show_done: bool = False, **_) -> dict:
    todos = _load_todos()
    if not show_done:
        todos = [t for t in todos if not t.get("done")]
    return {"todos": todos, "count": len(todos)}

async def todo_done(task: str = None, id: int = None, **_) -> dict:
    todos = _load_todos()
    matched = 0
    for t in todos:
        if (id and t["id"] == id) or (task and task.lower() in t["task"].lower()):
            t["done"] = True
            t["completed"] = datetime.now().isoformat()[:16]
            matched += 1
    _save_todos(todos)
    return {"matched": matched, "success": matched > 0}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 16 — screenshot_take: Take a screenshot, save to file
# ═══════════════════════════════════════════════════════════════════════════════
async def screenshot_take(path: str = None, **_) -> dict:
    if not path:
        path = str(Path.home() / f"screenshot_{int(time.time())}.png")
    if sys.platform == "darwin":
        r = _run_sync(["screencapture", "-x", path])
    elif sys.platform == "linux":
        if _has("scrot"):
            r = _run_sync(["scrot", path])
        elif _has("gnome-screenshot"):
            r = _run_sync(["gnome-screenshot", "-f", path])
        elif _has("import"):   # ImageMagick
            r = _run_sync(["import", "-window", "root", path])
        else:
            return {"error": "No screenshot tool found. Install scrot: sudo apt install scrot"}
    elif sys.platform == "win32":
        code = (
            f"Add-Type -AssemblyName System.Windows.Forms;"
            f"[System.Windows.Forms.Screen]::PrimaryScreen | Out-Null;"
            f"$b=New-Object System.Drawing.Bitmap([System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Width,"
            f"[System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Height);"
            f"$g=[System.Drawing.Graphics]::FromImage($b);"
            f"$g.CopyFromScreen(0,0,0,0,$b.Size);"
            f"$b.Save('{path}')"
        )
        r = _run_sync(["powershell", "-Command", code])
    else:
        return {"error": f"Unsupported platform: {sys.platform}"}

    if isinstance(r, dict) and r.get("returncode") == 0:
        return {"path": path, "success": True}
    return r if isinstance(r, dict) else {"path": path, "success": True}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 17 — notify_send: Desktop notification
# ═══════════════════════════════════════════════════════════════════════════════
async def notify_send(title: str, message: str, **_) -> dict:
    if sys.platform == "darwin":
        script = f'display notification "{message}" with title "{title}"'
        return _run_sync(["osascript", "-e", script])
    elif sys.platform == "linux":
        if _has("notify-send"):
            return _run_sync(["notify-send", title, message])
        return {"error": "Install libnotify: sudo apt install libnotify-bin"}
    elif sys.platform == "win32":
        # PowerShell balloon notification
        code = (
            f"Add-Type -AssemblyName System.Windows.Forms;"
            f"$n=New-Object System.Windows.Forms.NotifyIcon;"
            f"$n.Icon=[System.Drawing.SystemIcons]::Information;"
            f"$n.Visible=$true;"
            f"$n.ShowBalloonTip(5000,'{title}','{message}',"
            f"[System.Windows.Forms.ToolTipIcon]::Info)"
        )
        return _run_sync(["powershell", "-Command", code])
    return {"error": f"Unsupported platform: {sys.platform}"}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 18 — git_status: Git status + recent commits for a repo
# ═══════════════════════════════════════════════════════════════════════════════
async def git_status(path: str = ".", **_) -> dict:
    if not _has("git"):
        return {"error": "git not installed"}
    base = ["git", "-C", str(Path(path).expanduser())]
    r1 = _run_sync(base + ["status", "--short"])
    if not r1.get("success"):
        return {"error": r1.get("stderr") or "Not a git repository"}
    r2 = _run_sync(base + ["log", "--oneline", "-8"])
    r3 = _run_sync(base + ["branch", "--show-current"])
    return {
        "branch":  r3.get("stdout", "?").strip(),
        "status":  r1["stdout"] or "Working tree clean",
        "commits": r2.get("stdout", ""),
        "path":    str(Path(path).expanduser().resolve()),
    }


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 19 — sys_kill: Kill a process by PID
# ═══════════════════════════════════════════════════════════════════════════════
SAFE_TO_KILL_BLOCKLIST = {"systemd", "init", "launchd", "kernel", "svchost", "wininit"}

async def sys_kill(pid: int, **_) -> dict:
    try:
        proc = psutil.Process(int(pid))
        name = proc.name().lower()
        if any(c in name for c in SAFE_TO_KILL_BLOCKLIST):
            return {"error": f"Refusing to kill critical process: {name}"}
        proc.terminate()
        try:
            proc.wait(timeout=5)
        except psutil.TimeoutExpired:
            proc.kill()
        return {"pid": pid, "name": name, "killed": True}
    except psutil.NoSuchProcess:
        return {"error": f"No process with PID {pid}"}
    except psutil.AccessDenied:
        return {"error": f"Access denied to PID {pid} — try with sudo"}
    except Exception as e:
        return {"error": str(e)}


# ═══════════════════════════════════════════════════════════════════════════════
# TOOL 20 — reminder_set: Set a reminder (fires desktop notification + Telegram)
# ═══════════════════════════════════════════════════════════════════════════════
_reminder_callbacks: list = []   # bot registers its send_message here

def register_reminder_callback(cb):
    _reminder_callbacks.append(cb)

async def reminder_set(message: str, minutes: float = 5, **_) -> dict:
    async def _fire():
        await asyncio.sleep(minutes * 60)
        await notify_send("Reminder ⏰", message)
        for cb in _reminder_callbacks:
            try:
                await cb(f"⏰ *Reminder:* {message}")
            except Exception:
                pass
    asyncio.create_task(_fire())
    return {"message": message, "minutes": minutes, "scheduled": True}


# ═══════════════════════════════════════════════════════════════════════════════
# REGISTRY — maps names to functions, plus metadata
# ═══════════════════════════════════════════════════════════════════════════════

TOOLS: dict[str, dict] = {
    # name → {fn, description, dangerous}
    "fs_list":        {"fn": fs_list,        "desc": "List directory contents",                        "danger": False},
    "fs_read":        {"fn": fs_read,        "desc": "Read a text file",                               "danger": False},
    "fs_write":       {"fn": fs_write,       "desc": "Write / create a file",                          "danger": False},
    "fs_delete":      {"fn": fs_delete,      "desc": "Delete a file or folder",                        "danger": True},
    "fs_search":      {"fn": fs_search,      "desc": "Find files by name or content",                  "danger": False},
    "sys_info":       {"fn": sys_info,       "desc": "Full system info (OS, RAM, disk, CPU)",          "danger": False},
    "sys_monitor":    {"fn": sys_monitor,    "desc": "Live CPU / RAM / disk usage",                    "danger": False},
    "sys_processes":  {"fn": sys_processes,  "desc": "Top processes by memory",                        "danger": False},
    "sys_kill":       {"fn": sys_kill,       "desc": "Kill a process by PID",                          "danger": True},
    "shell_exec":     {"fn": shell_exec,     "desc": "Run any shell command",                           "danger": True},
    "web_search":     {"fn": web_search,     "desc": "Search the web (DuckDuckGo)",                    "danger": False},
    "weather":        {"fn": weather,        "desc": "Current weather for any city",                    "danger": False},
    "calc":           {"fn": calc,           "desc": "Evaluate a math expression",                     "danger": False},
    "memory_store":   {"fn": memory_store,   "desc": "Remember a named fact",                          "danger": False},
    "memory_recall":  {"fn": memory_recall,  "desc": "Recall stored facts",                            "danger": False},
    "memory_delete":  {"fn": memory_delete,  "desc": "Forget a stored fact",                           "danger": False},
    "note_create":    {"fn": note_create,    "desc": "Create / save a markdown note",                  "danger": False},
    "note_list":      {"fn": note_list,      "desc": "List all notes",                                 "danger": False},
    "note_read":      {"fn": note_read,      "desc": "Read a note by title",                           "danger": False},
    "todo_add":       {"fn": todo_add,       "desc": "Add a to-do item",                               "danger": False},
    "todo_list":      {"fn": todo_list,      "desc": "Show the to-do list",                            "danger": False},
    "todo_done":      {"fn": todo_done,      "desc": "Mark a to-do item done",                         "danger": False},
    "screenshot_take":{"fn": screenshot_take,"desc": "Take a screenshot",                              "danger": False},
    "notify_send":    {"fn": notify_send,    "desc": "Send a desktop notification",                    "danger": False},
    "git_status":     {"fn": git_status,     "desc": "Git status + recent commits",                    "danger": False},
    "reminder_set":   {"fn": reminder_set,   "desc": "Set a reminder (minutes from now)",              "danger": False},
}


def tool_list_for_prompt() -> str:
    """One-line-per-tool description for the AI system prompt."""
    lines = []
    for name, meta in TOOLS.items():
        danger = " ⚠️ DANGEROUS — always set confirm:true" if meta["danger"] else ""
        lines.append(f"- {name}: {meta['desc']}{danger}")
    return "\n".join(lines)


async def run_tool(name: str, args: dict) -> dict:
    """Execute a tool by name. Returns a dict — never raises."""
    t = TOOLS.get(name)
    if not t:
        return {"error": f"Unknown tool: {name!r}. Available: {', '.join(TOOLS)}"}
    try:
        result = await t["fn"](**args)
        if not isinstance(result, dict):
            result = {"result": result}
        return result
    except TypeError as e:
        return {"error": f"Wrong arguments for {name}: {e}"}
    except Exception as e:
        logger.exception("Tool %s crashed", name)
        return {"error": f"Tool error: {e}"}
