"""
doit-fm tools — 15 real, fully-working implementations.
No mocks. No placeholders. No simulations. Every function does exactly what it says.

15 Telegram commands backed here:
  /health      → sys_health
  /run <cmd>   → shell_run
  /export      → export_log
  /tools       → (listed in bot directly from TOOLS dict)
  /vol up|down|N → vol_get / vol_set
  /screenshot  → screenshot
  /app <name>  → app_open
  /delete <path|desc> → delete_item
  + free-text via AI: fs_list, fs_read, fs_write, memory_store,
    memory_recall, web_search, weather, calc, notify, reminder_set
"""
from __future__ import annotations
import asyncio, json, os, platform, re, shutil
import subprocess, sys, time
from datetime import datetime
from pathlib import Path
from typing import Any

import psutil

from doit_fm.config import BLOCKED_PATHS, DATA_DIR, TASK_LOG

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


# ─── helpers ──────────────────────────────────────────────────────────────────

def _safe_path(p: str) -> tuple[bool, str]:
    resolved = str(Path(p).expanduser().resolve())
    for b in BLOCKED_PATHS:
        if resolved.startswith(b):
            return False, f"Blocked path: {b}"
    return True, ""

async def _shell(cmd: str, timeout: int = 60, cwd: str = None) -> dict:
    """Run a shell command asynchronously. Returns stdout/stderr/rc dict."""
    cwd_path = str(Path(cwd).expanduser()) if cwd else str(Path.home())
    try:
        proc = await asyncio.create_subprocess_shell(
            cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=cwd_path,
        )
        try:
            out, err = await asyncio.wait_for(proc.communicate(), timeout=timeout)
        except asyncio.TimeoutError:
            proc.kill()
            return {"error": f"Command timed out after {timeout}s", "returncode": -1}
        return {
            "stdout":     out.decode(errors="replace").strip(),
            "stderr":     err.decode(errors="replace").strip(),
            "returncode": proc.returncode,
            "success":    proc.returncode == 0,
        }
    except Exception as e:
        return {"error": str(e), "returncode": -1, "success": False}

def _log_task(tool: str, args: dict, result: dict):
    """Append one line to tasks.jsonl for /export."""
    try:
        DATA_DIR.mkdir(parents=True, exist_ok=True)
        entry = {
            "ts":     datetime.now().isoformat(timespec="seconds"),
            "tool":   tool,
            "args":   {k: str(v)[:200] for k, v in (args or {}).items()},
            "ok":     "error" not in result,
            "result": str(result)[:300],
        }
        with open(TASK_LOG, "a", encoding="utf-8") as f:
            f.write(json.dumps(entry) + "\n")
    except Exception:
        pass


# ═══════════════════════════════════════════════════════════════════════════════
# 1 — sys_health  (/health)
#     Real psutil metrics — CPU, RAM, disk, battery, top processes
# ═══════════════════════════════════════════════════════════════════════════════
async def sys_health(**_) -> dict:
    try:
        cpu   = psutil.cpu_percent(interval=0.5)
        vm    = psutil.virtual_memory()
        disk  = psutil.disk_usage("/")
        net   = psutil.net_io_counters()
        bat   = psutil.sensors_battery()
        procs = sorted(
            psutil.process_iter(["pid","name","cpu_percent","memory_info"]),
            key=lambda p: p.info.get("memory_info").rss if p.info.get("memory_info") else 0,
            reverse=True,
        )[:5]
        top = []
        for p in procs:
            try:
                mi = p.info["memory_info"]
                top.append({"pid": p.info["pid"], "name": p.info["name"],
                             "mem_mb": round(mi.rss/1e6,1) if mi else 0})
            except Exception:
                pass
        return {
            "cpu_percent":   cpu,
            "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),
            "battery_pct":   round(bat.percent) if bat else None,
            "plugged_in":    bat.power_plugged if bat else None,
            "top_processes": top,
        }
    except Exception as e:
        return {"error": str(e)}


# ═══════════════════════════════════════════════════════════════════════════════
# 2 — shell_run  (/run <command>)
#     Executes any shell command on the user's machine. No fakery.
# ═══════════════════════════════════════════════════════════════════════════════
_BLOCKED_CMDS = ["rm -rf /", "mkfs", ":(){:|:&};:", "dd if=/dev/zero of=/dev/"]

async def shell_run(command: str, cwd: str = None, timeout: int = 120, **_) -> dict:
    for bad in _BLOCKED_CMDS:
        if bad in command:
            return {"error": f"Blocked dangerous pattern: {bad!r}"}
    r = await _shell(command, timeout=timeout, cwd=cwd)
    _log_task("shell_run", {"command": command}, r)
    return r


# ═══════════════════════════════════════════════════════════════════════════════
# 3 — export_log  (/export)
#     Reads real tasks.jsonl, formats it as readable audit log, saves to file
# ═══════════════════════════════════════════════════════════════════════════════
async def export_log(last_n: int = 100, **_) -> dict:
    try:
        if not TASK_LOG.exists():
            return {"error": "No task history yet. Run some commands first!"}
        lines = TASK_LOG.read_text(encoding="utf-8").strip().splitlines()
        if not lines:
            return {"error": "Task log is empty."}
        entries = []
        for line in lines[-last_n:]:
            try:
                entries.append(json.loads(line))
            except Exception:
                pass
        # Write a human-readable export file
        out_path = DATA_DIR / f"export_{int(time.time())}.txt"
        with open(out_path, "w", encoding="utf-8") as f:
            f.write(f"doit-fm Task Log Export — {datetime.now().strftime('%Y-%m-%d %H:%M')}\n")
            f.write("=" * 60 + "\n\n")
            for e in entries:
                icon = "✓" if e.get("ok") else "✗"
                f.write(f"[{e['ts']}] {icon} {e['tool']}\n")
                if e.get("args"):
                    for k, v in e["args"].items():
                        f.write(f"   {k}: {v}\n")
                f.write(f"   → {e.get('result','')[:120]}\n\n")
        return {
            "path":    str(out_path),
            "entries": len(entries),
            "success": True,
            "preview": entries[-5:],   # last 5 for Telegram display
        }
    except Exception as e:
        return {"error": str(e)}


# ═══════════════════════════════════════════════════════════════════════════════
# 4 — vol_get  — reads real system volume
# ═══════════════════════════════════════════════════════════════════════════════
async def vol_get(**_) -> dict:
    try:
        if sys.platform == "darwin":
            r = await _shell("osascript -e 'output volume of (get volume settings)'")
            if r.get("success"):
                return {"volume": int(r["stdout"].strip()), "muted": False}
        elif sys.platform == "linux":
            # Try pactl (PulseAudio / PipeWire)
            r = await _shell("pactl get-sink-volume @DEFAULT_SINK@")
            if r.get("success") and "%" in r["stdout"]:
                m = re.search(r"(\d+)%", r["stdout"])
                if m:
                    muted = await _shell("pactl get-sink-mute @DEFAULT_SINK@")
                    return {"volume": int(m.group(1)), "muted": "yes" in muted.get("stdout","").lower()}
            # Fallback: amixer
            r = await _shell("amixer sget Master")
            if r.get("success"):
                m = re.search(r"\[(\d+)%\]", r["stdout"])
                if m:
                    return {"volume": int(m.group(1)), "muted": "[off]" in r["stdout"]}
        elif sys.platform == "win32":
            ps = ("(New-Object -COMObject WScript.Shell).SendKeys('');" +
                  "Add-Type -TypeDefinition 'using System.Runtime.InteropServices;" +
                  "public class Audio{[DllImport(\"winmm.dll\")]public static extern int waveOutGetVolume(IntPtr h,out uint v);}'; " +
                  "$v=0; [Audio]::waveOutGetVolume([IntPtr]::Zero,[ref]$v); [math]::Round(($v -band 0xFFFF)/65535*100)")
            r = await _shell(f"powershell -Command \"{ps}\"")
            if r.get("success") and r["stdout"].strip().isdigit():
                return {"volume": int(r["stdout"].strip()), "muted": False}
        return {"error": "Could not read volume on this platform"}
    except Exception as e:
        return {"error": str(e)}


# ═══════════════════════════════════════════════════════════════════════════════
# 5 — vol_set  (/vol up|down|N)
#     Sets real system volume using OS-native APIs
# ═══════════════════════════════════════════════════════════════════════════════
async def vol_set(level: int = None, direction: str = None, step: int = 10, **_) -> dict:
    """
    level=0-100  → set absolute
    direction="up"|"down"  → change by step
    """
    try:
        # Resolve target level
        if direction in ("up", "down"):
            cur = await vol_get()
            if "error" in cur:
                return cur
            cur_v = cur.get("volume", 50)
            target = min(100, cur_v + step) if direction == "up" else max(0, cur_v - step)
        elif level is not None:
            target = max(0, min(100, int(level)))
        else:
            return {"error": "Provide level=0-100 or direction=up|down"}

        if sys.platform == "darwin":
            r = await _shell(f"osascript -e 'set volume output volume {target}'")
        elif sys.platform == "linux":
            r = await _shell(f"pactl set-sink-volume @DEFAULT_SINK@ {target}%")
            if not r.get("success"):
                r = await _shell(f"amixer sset Master {target}%")
        elif sys.platform == "win32":
            # Use nircmd if present, else PowerShell SoundVolumeView / wscript
            if shutil.which("nircmd"):
                nv = int(target / 100 * 65535)
                r = await _shell(f"nircmd setsysvolume {nv}")
            else:
                ps = (f"$wshShell = New-Object -ComObject WScript.Shell; "
                      f"$vol=[Math]::Round({target}/2); "
                      f"for($i=0;$i -lt 50;$i++){{$wshShell.SendKeys([char]174)}}; "
                      f"for($i=0;$i -lt $vol;$i++){{$wshShell.SendKeys([char]175)}}")
                r = await _shell(f"powershell -Command \"{ps}\"")
        else:
            return {"error": f"Unsupported platform: {sys.platform}"}

        if isinstance(r, dict) and "error" not in r:
            return {"volume": target, "success": True}
        return r
    except Exception as e:
        return {"error": str(e)}


# ═══════════════════════════════════════════════════════════════════════════════
# 6 — screenshot  (/screenshot)
#     Takes a real screenshot using OS-native tools. Saves with timestamp.
# ═══════════════════════════════════════════════════════════════════════════════
async def screenshot(path: str = None, **_) -> dict:
    if not path:
        ts  = datetime.now().strftime("%Y%m%d_%H%M%S")
        path = str(Path.home() / "Desktop" / f"screenshot_{ts}.png")
        # Fallback if no Desktop
        if not Path(str(Path.home() / "Desktop")).exists():
            path = str(DATA_DIR / f"screenshot_{ts}.png")
            DATA_DIR.mkdir(parents=True, exist_ok=True)

    if sys.platform == "darwin":
        r = await _shell(f"screencapture -x {path!r}")
    elif sys.platform == "linux":
        if shutil.which("scrot"):
            r = await _shell(f"scrot {path!r}")
        elif shutil.which("gnome-screenshot"):
            r = await _shell(f"gnome-screenshot -f {path!r}")
        elif shutil.which("import"):
            r = await _shell(f"import -window root {path!r}")
        elif shutil.which("spectacle"):
            r = await _shell(f"spectacle -b -n -o {path!r}")
        else:
            return {"error": "No screenshot tool found. Install scrot: sudo apt install scrot"}
    elif sys.platform == "win32":
        ps = (
            "Add-Type -AssemblyName System.Windows.Forms;"
            "Add-Type -AssemblyName System.Drawing;"
            f"$bmp=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($bmp);"
            f"$g.CopyFromScreen(0,0,0,0,$bmp.Size);"
            f"$bmp.Save('{path}')"
        )
        r = await _shell(f'powershell -Command "{ps}"')
    else:
        return {"error": f"Unsupported platform: {sys.platform}"}

    if Path(path).exists():
        size = Path(path).stat().st_size
        _log_task("screenshot", {}, {"path": path})
        return {"path": path, "size_kb": round(size/1024,1), "success": True}
    return {"error": f"Screenshot failed. {r.get('stderr','')[:200]}", **r}


# ═══════════════════════════════════════════════════════════════════════════════
# 7 — app_open  (/app <name>)
#     Opens any app by name using OS-native open/xdg-open/Start-Process
# ═══════════════════════════════════════════════════════════════════════════════
async def app_open(name: str, **_) -> dict:
    if not name:
        return {"error": "Provide an app name, e.g. 'Chrome' or 'VSCode'"}
    try:
        if sys.platform == "darwin":
            # Try `open -a AppName` first, then spotlight mdfind
            r = await _shell(f"open -a {name!r}")
            if not r.get("success"):
                # Try mdfind to locate .app bundle
                find = await _shell(f"mdfind -name '{name}.app' | head -1")
                app_path = find.get("stdout","").strip()
                if app_path:
                    r = await _shell(f"open {app_path!r}")
                else:
                    # Try lowercase
                    r = await _shell(f"open -a {name.lower()!r}")
        elif sys.platform == "linux":
            # Try xdg-open, then gtk-launch, then direct exec
            cmd = name.lower().replace(" ", "-")
            for attempt in [
                f"xdg-open {name!r}",
                f"gtk-launch {cmd}",
                f"{cmd} &",
                f"{name} &",
            ]:
                r = await _shell(attempt, timeout=5)
                if r.get("success") or r.get("returncode", -1) == 0:
                    break
        elif sys.platform == "win32":
            r = await _shell(f'powershell -Command "Start-Process {name!r}"')
            if not r.get("success"):
                # Try with .exe
                r = await _shell(f'start "" "{name}.exe"')
        else:
            return {"error": f"Unsupported platform: {sys.platform}"}

        if r.get("success") or r.get("returncode", -1) == 0:
            _log_task("app_open", {"name": name}, r)
            return {"app": name, "launched": True, "success": True}
        return {"error": f"Could not open '{name}'. {r.get('stderr','')[:200]}"}
    except Exception as e:
        return {"error": str(e)}


# ═══════════════════════════════════════════════════════════════════════════════
# 8 — delete_item  (/delete <path or description>)
#     Deletes files, folders, packages (pip/apt/brew), whatever is asked.
#     AI resolves the description to an exact target before calling this.
# ═══════════════════════════════════════════════════════════════════════════════
async def delete_item(target: str, kind: str = "file", **_) -> dict:
    """
    kind = "file" | "directory" | "pip" | "apt" | "brew" | "app"
    target = exact path OR package name
    """
    try:
        if kind in ("pip", "python_package"):
            r = await _shell(f"{sys.executable} -m pip uninstall -y {target!r}")
            _log_task("delete_item", {"target": target, "kind": kind}, r)
            return r if "error" not in r else {"error": r.get("stderr", r.get("error"))}

        if kind == "apt":
            r = await _shell(f"sudo apt remove -y {target!r}")
            _log_task("delete_item", {"target": target, "kind": kind}, r)
            return r

        if kind == "brew":
            r = await _shell(f"brew uninstall {target!r}")
            _log_task("delete_item", {"target": target, "kind": kind}, r)
            return r

        if kind == "app" and sys.platform == "darwin":
            # Find and trash .app bundle
            r = await _shell(f"mdfind -name '{target}.app' | head -1")
            app_path = r.get("stdout","").strip()
            if not app_path:
                return {"error": f"App '{target}' not found on this Mac"}
            r2 = await _shell(f"osascript -e 'tell application \"Finder\" to delete POSIX file \"{app_path}\"'")
            _log_task("delete_item", {"target": target, "kind": kind}, r2)
            return {"deleted": app_path, "success": r2.get("success", False)}

        # File or directory
        p = Path(target).expanduser().resolve()
        safe, reason = _safe_path(str(p))
        if not safe:
            return {"error": reason}
        if not p.exists():
            # Try glob expansion if exact path not found
            parent = p.parent
            pattern = p.name
            matches = list(parent.glob(pattern)) if parent.exists() else []
            if not matches:
                return {"error": f"Not found: {p}"}
            if len(matches) > 10:
                return {"error": f"Pattern matches {len(matches)} files — be more specific"}
            deleted = []
            for m in matches:
                if m.is_dir():
                    shutil.rmtree(m)
                else:
                    m.unlink()
                deleted.append(str(m))
            _log_task("delete_item", {"target": target}, {"deleted": deleted})
            return {"deleted": deleted, "count": len(deleted), "success": True}

        if p.is_dir():
            shutil.rmtree(p)
        else:
            p.unlink()
        _log_task("delete_item", {"target": target}, {"deleted": str(p)})
        return {"deleted": str(p), "success": True}
    except PermissionError:
        return {"error": f"Permission denied: {target}. Try with sudo or check file ownership."}
    except Exception as e:
        return {"error": str(e)}


# ═══════════════════════════════════════════════════════════════════════════════
# 9 — fs_list  (free text: "show my downloads")
# ═══════════════════════════════════════════════════════════════════════════════
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}"}
    try:
        entries = []
        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"})
        return {"path": str(p), "entries": entries, "count": len(entries)}
    except PermissionError:
        return {"error": f"Permission denied: {p}"}
    except Exception as e:
        return {"error": str(e)}


# ═══════════════════════════════════════════════════════════════════════════════
# 10 — fs_read  (free text: "read my notes.txt")
# ═══════════════════════════════════════════════════════════════════════════════
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 /run cat to stream it."}
    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)}


# ═══════════════════════════════════════════════════════════════════════════════
# 11 — fs_write  (free text: "create a file called hello.txt with ...")
# ═══════════════════════════════════════════════════════════════════════════════
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)}


# ═══════════════════════════════════════════════════════════════════════════════
# 12 — memory_store / memory_recall  (free text: "remember that X")
# ═══════════════════════════════════════════════════════════════════════════════
_MEM_FILE = DATA_DIR / "memory.json"

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

def _save_mem(d: dict):
    _MEM_FILE.parent.mkdir(parents=True, exist_ok=True)
    _MEM_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)}


# ═══════════════════════════════════════════════════════════════════════════════
# 13 — web_search  (free text: "search for X")
# ═══════════════════════════════════════════════════════════════════════════════
async def web_search(query: str, limit: int = 5, **_) -> 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:
        req = urllib.request.Request(url, headers={"User-Agent": "doit-fm/3.1"})
        loop = asyncio.get_event_loop()
        raw = await loop.run_in_executor(None, lambda: urllib.request.urlopen(req, timeout=10).read())
        data = json.loads(raw)
        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["Text"][:100],
                                 "snippet": item["Text"],
                                 "url": item.get("FirstURL","")})
        return {"query": query, "results": results[:limit]}
    except Exception as e:
        return {"error": f"Search failed: {e}"}


# ═══════════════════════════════════════════════════════════════════════════════
# 14 — weather  (free text: "weather in Tokyo")
# ═══════════════════════════════════════════════════════════════════════════════
async def weather(city: str, **_) -> dict:
    import urllib.request, urllib.parse
    url = f"https://wttr.in/{urllib.parse.quote(city)}?format=j1"
    try:
        req = urllib.request.Request(url, headers={"User-Agent": "doit-fm/3.1"})
        loop = asyncio.get_event_loop()
        raw = await loop.run_in_executor(None, lambda: urllib.request.urlopen(req, timeout=10).read())
        data = json.loads(raw)
        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"],
        }
    except Exception as e:
        return {"error": f"Weather unavailable: {e}"}


# ═══════════════════════════════════════════════════════════════════════════════
# 15 — calc  (free text: "calculate 2^32" or "what's sin(45)*pi")
# ═══════════════════════════════════════════════════════════════════════════════
async def calc(expression: str, **_) -> dict:
    import math as _m
    ns = {k: getattr(_m, k) for k in dir(_m) 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}"}


# ═══════════════════════════════════════════════════════════════════════════════
# 16 — reminder_set  (free text: "remind me in 20 minutes")
# ═══════════════════════════════════════════════════════════════════════════════
_reminder_callbacks: list = []

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)
        # Desktop notification
        if sys.platform == "darwin":
            await _shell(f"osascript -e 'display notification \"{message}\" with title \"⏰ Reminder\"'")
        elif sys.platform == "linux" and shutil.which("notify-send"):
            await _shell(f"notify-send '⏰ Reminder' '{message}'")
        # Telegram callback
        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
# ═══════════════════════════════════════════════════════════════════════════════
TOOLS: dict[str, dict] = {
    # Core commands
    "sys_health":     {"fn": sys_health,     "desc": "CPU, RAM, disk, battery, top processes",                "danger": False},
    "shell_run":      {"fn": shell_run,       "desc": "Run any shell command on the computer",                "danger": True},
    "export_log":     {"fn": export_log,      "desc": "Export task history as a readable log file",           "danger": False},
    "vol_get":        {"fn": vol_get,         "desc": "Get current system volume level",                      "danger": False},
    "vol_set":        {"fn": vol_set,         "desc": "Set system volume (level=0-100 or direction=up/down)", "danger": False},
    "screenshot":     {"fn": screenshot,      "desc": "Take a screenshot, save to Desktop",                   "danger": False},
    "app_open":       {"fn": app_open,        "desc": "Open any application by name",                        "danger": False},
    "delete_item":    {"fn": delete_item,     "desc": "Delete a file, folder, pip package, or app",          "danger": True},
    # Free-text AI tools
    "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},
    "memory_store":   {"fn": memory_store,    "desc": "Remember a named fact",                               "danger": False},
    "memory_recall":  {"fn": memory_recall,   "desc": "Recall stored facts",                                 "danger": False},
    "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},
    "reminder_set":   {"fn": reminder_set,    "desc": "Set a reminder N minutes from now",                   "danger": False},
}


def tool_list_for_prompt() -> str:
    lines = []
    for name, meta in TOOLS.items():
        danger = " ⚠️ DANGEROUS — always 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 to caller."""
    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}"}
