"""
bot.py — Telegram long-polling bot.

Classes:
  TelegramAPI   — Raw HTTP wrapper for Telegram Bot API (urllib, async via executor)
  TelegramBot   — Full bot logic: routing, confirmations, command dispatch

Formatting:
  fmt(result)   — Converts any tool result dict to a readable Telegram message
"""
from __future__ import annotations

import asyncio
import json
import secrets
import time
import urllib.error
import urllib.parse
import urllib.request
from typing import Any

from .ai import AIEngine
from .tools import run_tool


# ---------------------------------------------------------------------------
# TelegramAPI — raw HTTP wrapper
# ---------------------------------------------------------------------------

class TelegramAPI:
    BASE = "https://api.telegram.org/bot{token}/{method}"

    def __init__(self, token: str) -> None:
        self._token = token
        self._loop: asyncio.AbstractEventLoop | None = None

    def _url(self, method: str) -> str:
        return self.BASE.format(token=self._token, method=method)

    def _post_sync(self, method: str, payload: dict) -> dict:
        """Synchronous POST to Telegram. Run in executor."""
        url = self._url(method)
        body = json.dumps(payload).encode()
        headers = {"Content-Type": "application/json"}
        req = urllib.request.Request(url, data=body, headers=headers, method="POST")
        try:
            with urllib.request.urlopen(req, timeout=35) as resp:
                return json.loads(resp.read())
        except urllib.error.HTTPError as exc:
            body_bytes = exc.read()
            try:
                return json.loads(body_bytes)
            except Exception:
                return {"ok": False, "description": body_bytes.decode(errors="replace")[:300]}
        except Exception as exc:
            return {"ok": False, "description": str(exc)}

    async def _post(self, method: str, payload: dict) -> dict:
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, self._post_sync, method, payload)

    # ------------------------------------------------------------------
    # Bot API methods
    # ------------------------------------------------------------------

    async def get_updates(self, offset: int = 0, timeout: int = 30) -> list[dict]:
        data = await self._post("getUpdates", {"offset": offset, "timeout": timeout,
                                                "allowed_updates": ["message", "callback_query"]})
        if data.get("ok"):
            return data.get("result", [])
        return []

    async def send_message(
        self,
        chat_id: int,
        text: str,
        parse_mode: str = "Markdown",
        reply_markup: dict | None = None,
    ) -> dict:
        payload: dict[str, Any] = {"chat_id": chat_id, "text": text, "parse_mode": parse_mode}
        if reply_markup:
            payload["reply_markup"] = reply_markup
        result = await self._post("sendMessage", payload)
        # Auto-retry without Markdown if formatting caused an error
        if not result.get("ok") and parse_mode:
            plain = _strip_markdown(text)
            payload2: dict[str, Any] = {"chat_id": chat_id, "text": plain}
            if reply_markup:
                payload2["reply_markup"] = reply_markup
            result = await self._post("sendMessage", payload2)
        return result

    async def send_photo(self, chat_id: int, photo_path: str, caption: str = "") -> dict:
        """Send a photo file via multipart upload."""
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(
            None, self._send_photo_sync, chat_id, photo_path, caption
        )

    def _send_photo_sync(self, chat_id: int, photo_path: str, caption: str) -> dict:
        import mimetypes
        url = self._url("sendPhoto")
        boundary = "----doit_fm_boundary"
        with open(photo_path, "rb") as f:
            photo_bytes = f.read()
        mime = mimetypes.guess_type(photo_path)[0] or "image/png"
        parts = []
        parts.append(
            f'--{boundary}\r\nContent-Disposition: form-data; name="chat_id"\r\n\r\n{chat_id}'.encode()
        )
        parts.append(
            f'--{boundary}\r\nContent-Disposition: form-data; name="photo"; '
            f'filename="screenshot.png"\r\nContent-Type: {mime}\r\n\r\n'.encode()
            + photo_bytes
        )
        if caption:
            parts.append(
                f'--{boundary}\r\nContent-Disposition: form-data; name="caption"\r\n\r\n{caption}'.encode()
            )
        parts.append(f'--{boundary}--'.encode())
        body = b"\r\n".join(parts)
        headers = {"Content-Type": f"multipart/form-data; boundary={boundary}"}
        req = urllib.request.Request(url, data=body, headers=headers, method="POST")
        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                return json.loads(resp.read())
        except urllib.error.HTTPError as exc:
            return {"ok": False, "description": exc.read().decode(errors="replace")[:300]}
        except Exception as exc:
            return {"ok": False, "description": str(exc)}

    async def send_document(self, chat_id: int, file_path: str, caption: str = "") -> dict:
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(
            None, self._send_document_sync, chat_id, file_path, caption
        )

    def _send_document_sync(self, chat_id: int, file_path: str, caption: str) -> dict:
        import mimetypes
        import os
        url = self._url("sendDocument")
        boundary = "----doit_fm_doc_boundary"
        with open(file_path, "rb") as f:
            doc_bytes = f.read()
        filename = os.path.basename(file_path)
        mime = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
        parts = []
        parts.append(
            f'--{boundary}\r\nContent-Disposition: form-data; name="chat_id"\r\n\r\n{chat_id}'.encode()
        )
        parts.append(
            f'--{boundary}\r\nContent-Disposition: form-data; name="document"; '
            f'filename="{filename}"\r\nContent-Type: {mime}\r\n\r\n'.encode()
            + doc_bytes
        )
        if caption:
            parts.append(
                f'--{boundary}\r\nContent-Disposition: form-data; name="caption"\r\n\r\n{caption}'.encode()
            )
        parts.append(f'--{boundary}--'.encode())
        body = b"\r\n".join(parts)
        headers = {"Content-Type": f"multipart/form-data; boundary={boundary}"}
        req = urllib.request.Request(url, data=body, headers=headers, method="POST")
        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                return json.loads(resp.read())
        except urllib.error.HTTPError as exc:
            return {"ok": False, "description": exc.read().decode(errors="replace")[:300]}
        except Exception as exc:
            return {"ok": False, "description": str(exc)}

    async def edit_message_text(
        self, chat_id: int, message_id: int, text: str, parse_mode: str = "Markdown"
    ) -> dict:
        payload: dict[str, Any] = {
            "chat_id": chat_id,
            "message_id": message_id,
            "text": text,
            "parse_mode": parse_mode,
        }
        result = await self._post("editMessageText", payload)
        if not result.get("ok") and parse_mode:
            payload["text"] = _strip_markdown(text)
            del payload["parse_mode"]
            result = await self._post("editMessageText", payload)
        return result

    async def send_chat_action(self, chat_id: int, action: str = "typing") -> dict:
        return await self._post("sendChatAction", {"chat_id": chat_id, "action": action})

    async def answer_callback_query(self, callback_query_id: str, text: str = "") -> dict:
        return await self._post("answerCallbackQuery", {
            "callback_query_id": callback_query_id, "text": text
        })

    async def get_me(self) -> dict:
        return await self._post("getMe", {})


# ---------------------------------------------------------------------------
# Formatting
# ---------------------------------------------------------------------------

def _strip_markdown(text: str) -> str:
    """Remove Telegram Markdown special characters."""
    for ch in ["*", "_", "`", "[", "]", "(", ")"]:
        text = text.replace(ch, "")
    return text


def _bar(pct: float, width: int = 10) -> str:
    """Build a visual progress bar from block characters."""
    filled = round(pct / 100 * width)
    return "█" * filled + "░" * (width - filled)


def fmt(result: dict) -> str:
    """Convert any tool result dict into a readable Telegram message string."""

    # Error
    if "error" in result:
        return f"❌ *Error:* {result['error']}"

    # Clarify
    if "clarify" in result:
        return f"🤔 {result['clarify']}"

    # System health
    if "cpu_percent" in result:
        cpu = result["cpu_percent"]
        ram = result["ram_percent"]
        disk = result["disk_percent"]
        top = result.get("top_processes", [])
        lines = [
            "🖥️ *System Health*",
            f"CPU:  {_bar(cpu)} {cpu}%",
            f"RAM:  {_bar(ram)} {ram}%  ({result.get('ram_used_gb')}GB / {result.get('ram_total_gb')}GB)",
            f"Disk: {_bar(disk)} {disk}%  ({result.get('disk_used_gb')}GB / {result.get('disk_total_gb')}GB)",
            f"Swap: {result.get('swap_used_gb', 0)}GB used",
        ]
        if top:
            lines.append("\n📊 *Top Processes:*")
            for p in top:
                lines.append(f"  `{p['name'][:20]:<20}` CPU {p['cpu_pct']}%  MEM {p['mem_pct']}%")
        return "\n".join(lines)

    # Uptime
    if "uptime" in result and "boot_time" in result:
        return f"⏱️ *Uptime:* {result['uptime']}\n🕐 *Boot time:* {result['boot_time']}"

    # Battery
    if "percent" in result and "plugged_in" in result:
        plug = "🔌 Charging" if result["plugged_in"] else "🔋 On battery"
        return f"🔋 *Battery:* {result['percent']}%\n{plug}\n⏳ Time left: {result['time_left']}"

    # Shell output
    if "exit_code" in result:
        code = result["exit_code"]
        icon = "✅" if code == 0 else "⚠️"
        out = result.get("stdout", "")
        err = result.get("stderr", "")
        parts = [f"{icon} *Exit code:* {code}"]
        if out:
            # Truncate long output
            if len(out) > 3000:
                out = out[:3000] + "\n[...truncated]"
            parts.append(f"```\n{out}\n```")
        if err:
            parts.append(f"⚠️ *stderr:*\n```\n{err[:1000]}\n```")
        return "\n".join(parts)

    # Volume
    if "volume" in result:
        v = result["volume"]
        icon = "🔇" if v == 0 else ("🔈" if v < 40 else ("🔉" if v < 70 else "🔊"))
        bar = _bar(v)
        return f"{icon} *Volume:* {bar} {v}%"

    if "volume_set" in result:
        v = result["volume_set"]
        bar = _bar(v)
        return f"🔊 *Volume set to:* {bar} {v}%"

    if "muted" in result:
        return "🔇 *Audio muted*"

    # Screenshot
    if "path" in result and "size_kb" in result:
        return f"📸 Screenshot saved\n📁 `{result['path']}`\n💾 {result['size_kb']} KB"

    # Directory listing
    if "items" in result and "path" in result:
        items = result["items"]
        path = result["path"]
        lines = [f"📁 *{path}* ({result.get('count', len(items))} items)\n"]
        for item in items[:30]:
            icon = "📂" if item["type"] == "dir" else "📄"
            size = f"  {item['size_bytes']:,}B" if item.get("size_bytes") is not None else ""
            modified = f"  {item['modified']}" if item.get("modified") else ""
            lines.append(f"{icon} `{item['name']}`{size}{modified}")
        if len(items) > 30:
            lines.append(f"... and {len(items) - 30} more")
        return "\n".join(lines)

    # File read
    if "content" in result and "lines" in result:
        content = result["content"]
        trunc = " (truncated)" if result.get("truncated") else ""
        return (
            f"📄 *{result['path']}*{trunc} — {result['lines']} lines\n"
            f"```\n{content[:3000]}\n```"
        )

    # File write/create
    if "bytes_written" in result:
        return f"✅ *File written:* `{result['path']}`\n💾 {result['bytes_written']} bytes ({result.get('mode', 'write')})"

    # Deleted
    if "deleted" in result:
        icon = "🗂️" if result.get("type") == "directory" else "🗑️"
        return f"{icon} *Deleted:* `{result['deleted']}`"

    # Copied / moved
    if "copied" in result:
        return f"📋 *Copied:*\n  From: `{result['copied']}`\n  To: `{result['to']}`"
    if "moved" in result:
        return f"🚚 *Moved:*\n  From: `{result['moved']}`\n  To: `{result['to']}`"

    # Search results
    if "matches" in result:
        matches = result["matches"]
        lines = [f"🔍 *Found {result['count']} matches in* `{result['root']}`\n"]
        for m in matches[:20]:
            icon = "📂" if m["type"] == "dir" else "📄"
            size = f"  {m['size_bytes']:,}B" if m.get("size_bytes") is not None else ""
            lines.append(f"{icon} `{m['path']}`{size}")
        if result["count"] > 20:
            lines.append(f"... and {result['count'] - 20} more")
        return "\n".join(lines)

    # Weather
    if "temperature_c" in result:
        temp = result["temperature_c"]
        cond = result.get("conditions", "")
        city = result.get("city", "")
        wind = result.get("windspeed_kmh", "?")
        hum = result.get("humidity_percent")
        day = "☀️" if result.get("is_day") else "🌙"
        lines = [
            f"{day} *Weather in {city}*",
            f"🌡️ Temperature: *{temp}°C*",
            f"🌤️ Conditions: {cond}",
            f"💨 Wind: {wind} km/h",
        ]
        if hum is not None:
            lines.append(f"💧 Humidity: {hum}%")
        return "\n".join(lines)

    # Ping
    if "host" in result and "packets" in result:
        avg = result.get("avg_ms")
        avg_str = f"{avg} ms" if avg else "N/A"
        return (
            f"🏓 *Ping {result['host']}*\n"
            f"📊 Packets: {result['packets']}\n"
            f"⏱️ Avg latency: {avg_str}"
        )

    # Internet connectivity
    if "connectivity" in result:
        lines = ["🌐 *Internet Connectivity*"]
        for name, info in result["connectivity"].items():
            if info.get("reachable"):
                lines.append(f"  ✅ {name}: {info.get('latency_ms')} ms")
            else:
                lines.append(f"  ❌ {name}: unreachable")
        return "\n".join(lines)

    # Network stats
    if "bytes_sent_mb" in result:
        lines = [
            "📡 *Network Stats*",
            f"⬆️ Sent: {result['bytes_sent_mb']} MB",
            f"⬇️ Received: {result['bytes_recv_mb']} MB",
            f"🔗 Active connections: {result.get('active_connections', '?')}",
        ]
        ifaces = result.get("interfaces", {})
        if ifaces:
            lines.append("🌐 *Interfaces:*")
            for name, addr in ifaces.items():
                lines.append(f"  {name}: `{addr}`")
        return "\n".join(lines)

    # Datetime
    if "local" in result and "date" in result:
        return (
            f"🕐 *Date & Time*\n"
            f"📅 {result['date']}\n"
            f"⏰ {result['time']}\n"
            f"🌍 UTC: {result['utc']}\n"
            f"🗓️ Week {result.get('week_number')} · Day {result.get('day_of_year')} of year"
        )

    # Reminder set
    if "reminder_set" in result:
        return (
            f"⏰ *Reminder set!*\n"
            f"💬 \"{result['reminder_set']}\"\n"
            f"⏱️ Fires at: {result['fires_at']}"
        )

    # Clipboard read
    if "clipboard" in result:
        text = result["clipboard"]
        if len(text) > 1000:
            text = text[:1000] + "..."
        return f"📋 *Clipboard:*\n```\n{text}\n```"

    if "clipboard_set" in result:
        return f"📋 *Copied to clipboard:* `{result['clipboard_set']}`"

    # URL opened / app launched
    if "opened" in result:
        return f"🌐 *Opened:* {result['opened']}"
    if "launched" in result:
        return f"🚀 *Launched:* {result['launched']}"

    # Kill process
    if "killed" in result:
        killed = "\n".join(f"  • {k}" for k in result["killed"])
        return f"💀 *Killed processes:*\n{killed}"

    # Process list
    if "processes" in result:
        procs = result["processes"]
        total = result.get("total", len(procs))
        lines = [f"⚙️ *Processes ({total} total, showing top {len(procs)}):*"]
        for p in procs[:20]:
            lines.append(
                f"  `{p['pid']:>6}` `{p['name'][:20]:<20}` CPU {p['cpu_pct']:>5}% MEM {p['mem_pct']:>5}%"
            )
        return "\n".join(lines)

    # Disk usage
    if "total_gb" in result and "free_gb" in result:
        used_pct = result.get("used_percent", 0)
        return (
            f"💾 *Disk Usage* `{result.get('path', '/')}`\n"
            f"{_bar(used_pct)} {used_pct}%\n"
            f"  Used: {result['used_gb']} GB\n"
            f"  Free: {result['free_gb']} GB\n"
            f"  Total: {result['total_gb']} GB"
        )

    # Python info
    if "python_version" in result:
        return (
            f"🐍 *Python Info*\n"
            f"Version: `{result['python_version'].split()[0]}`\n"
            f"Executable: `{result['executable']}`\n"
            f"Platform: {result['platform']}\n"
            f"Packages installed: {result['packages_installed']}\n"
            f"Sample: {', '.join(result.get('packages_sample', []))}"
        )

    # Pip list
    if "packages" in result:
        pkgs = result["packages"]
        total = result.get("count", len(pkgs))
        lines = [f"📦 *{total} packages installed:*"]
        for p in pkgs[:30]:
            lines.append(f"  `{p['name']}` {p.get('version', '')}")
        if total > 30:
            lines.append(f"  ... and {total - 30} more")
        return "\n".join(lines)

    # Calculate
    if "expression" in result and "result" in result:
        return f"🧮 `{result['expression']}` = *{result['result']}*"

    # Word count
    if "words" in result and "characters" in result:
        return (
            f"📝 *Word Count*\n"
            f"  Words: {result['words']}\n"
            f"  Characters: {result['characters']}\n"
            f"  Lines: {result['lines']}\n"
            f"  Paragraphs: {result.get('paragraphs', '?')}"
        )

    # Git status
    if "status" in result and "path" in result:
        return f"🔀 *Git status* `{result['path']}`\n```\n{result['status']}\n```"

    # Git log
    if "commits" in result:
        lines = [f"📜 *Git log* `{result.get('path', '.')}`"]
        for c in result["commits"]:
            lines.append(f"  `{c['hash']}` {c['message']}")
        return "\n".join(lines)

    # Created directory
    if "created" in result:
        return f"📁 *Directory created:* `{result['created']}`"

    # Env vars
    if "env" in result:
        env = result["env"]
        count = result.get("count", len(env))
        lines = [f"🌿 *Environment Variables ({count}):*"]
        for k, v in list(env.items())[:25]:
            lines.append(f"  `{k}` = `{v[:80]}`")
        if count > 25:
            lines.append(f"  ... and {count - 25} more")
        return "\n".join(lines)

    # Fallback — render all key/value pairs
    lines = ["📊 *Result:*"]
    for k, v in result.items():
        val_str = str(v)
        if len(val_str) > 200:
            val_str = val_str[:200] + "..."
        lines.append(f"  *{k}:* `{val_str}`")
    return "\n".join(lines)


# ---------------------------------------------------------------------------
# TelegramBot
# ---------------------------------------------------------------------------

class TelegramBot:
    """
    Full Telegram bot. Receives updates via long-polling, routes them,
    handles slash commands and free-text through the AI engine,
    and manages confirmation flows.
    """

    def __init__(self, token: str, user_id: int, engine: AIEngine) -> None:
        self._api = TelegramAPI(token)
        self._user_id = user_id
        self._chat_id = user_id  # private bot: chat_id == user_id
        self._engine = engine
        self._offset = 0
        self._running = False

        # Pending confirmations: key → {tool, args, description, msg_id}
        self._pending: dict[str, dict] = {}

        # Slash command dispatch
        self._commands: dict[str, Any] = {
            "start":    self._cmd_start,
            "help":     self._cmd_help,
            "health":   self._cmd_health,
            "status":   self._cmd_status,
            "time":     self._cmd_time,
            "screenshot": self._cmd_screenshot,
            "weather":  self._cmd_weather,
            "ls":       self._cmd_ls,
            "run":      self._cmd_run,
            "ps":       self._cmd_ps,
            "battery":  self._cmd_battery,
            "ping":     self._cmd_ping,
            "memory":   self._cmd_memory,
            "clear":    self._cmd_clear_memory,
        }

    # ------------------------------------------------------------------
    # Public entry points
    # ------------------------------------------------------------------

    async def start(self) -> None:
        """Start the polling loop. Runs indefinitely until stopped."""
        self._running = True
        me = await self._api.get_me()
        bot_name = me.get("result", {}).get("username", "unknown")
        print(f"[doit-fm] Bot @{bot_name} started, polling…")
        while self._running:
            try:
                updates = await self._api.get_updates(offset=self._offset, timeout=30)
                for update in updates:
                    self._offset = update["update_id"] + 1
                    await self._route(update)
            except Exception as exc:
                print(f"[doit-fm] Polling error: {exc}")
                await asyncio.sleep(3)

    def stop(self) -> None:
        self._running = False

    async def send_message(self, text: str) -> None:
        """Public method used by tools (e.g. reminders) to push messages."""
        await self._api.send_message(self._chat_id, text)

    # ------------------------------------------------------------------
    # Routing
    # ------------------------------------------------------------------

    async def _route(self, update: dict) -> None:
        # Callback query (button tap)
        if "callback_query" in update:
            await self._handle_callback(update["callback_query"])
            return

        message = update.get("message", {})
        if not message:
            return

        # Security: only respond to the configured user
        sender = message.get("from", {}).get("id")
        if sender != self._user_id:
            return

        text = message.get("text", "").strip()
        if not text:
            return

        self._chat_id = message["chat"]["id"]

        if text.startswith("/"):
            await self._handle_command(text, message)
        else:
            await self._handle_freetext(text, message)

    # ------------------------------------------------------------------
    # Callback handler (confirmation buttons)
    # ------------------------------------------------------------------

    async def _handle_callback(self, cq: dict) -> None:
        await self._api.answer_callback_query(cq["id"])
        data = cq.get("data", "")
        chat_id = cq["message"]["chat"]["id"]
        msg_id = cq["message"]["message_id"]

        if data.startswith("confirm:"):
            key = data[len("confirm:"):]
            pending = self._pending.pop(key, None)
            if not pending:
                await self._api.edit_message_text(chat_id, msg_id, "⚠️ This confirmation has expired.")
                return
            await self._api.edit_message_text(chat_id, msg_id, "⏳ Running…")
            result = await run_tool(pending["tool"], pending["args"])
            response = fmt(result)
            # If screenshot, send as photo
            if pending["tool"] == "screenshot" and "path" in result:
                await self._api.send_photo(chat_id, result["path"],
                                            caption=f"📸 Screenshot — {result.get('size_kb', '?')} KB")
                await self._api.edit_message_text(chat_id, msg_id, "✅ Done")
            else:
                await self._api.edit_message_text(chat_id, msg_id, response)

        elif data.startswith("cancel:"):
            key = data[len("cancel:"):]
            self._pending.pop(key, None)
            await self._api.edit_message_text(chat_id, msg_id, "❌ Cancelled.")

    # ------------------------------------------------------------------
    # Command handler
    # ------------------------------------------------------------------

    async def _handle_command(self, text: str, message: dict) -> None:
        parts = text.lstrip("/").split(None, 1)
        cmd = parts[0].lower().split("@")[0]   # strip @botname
        args_str = parts[1] if len(parts) > 1 else ""

        handler = self._commands.get(cmd)
        if handler:
            await handler(args_str, message)
        else:
            await self._api.send_message(
                self._chat_id,
                f"❓ Unknown command `/{cmd}`. Send /help for the list.",
            )

    # ------------------------------------------------------------------
    # Free text → AI → tool
    # ------------------------------------------------------------------

    async def _handle_freetext(self, text: str, message: dict) -> None:
        await self._api.send_chat_action(self._chat_id, "typing")
        action = await self._engine.parse_intent(text)

        tool_name = action.get("tool", "clarify")
        args = action.get("args", {})
        description = action.get("description", "")
        confirm = action.get("confirm", False)

        if confirm:
            await self._send_confirmation(tool_name, args, description)
            return

        # Execute immediately
        await self._api.send_chat_action(self._chat_id, "typing")
        result = await run_tool(tool_name, args)

        # Special handling: screenshot → send as photo
        if tool_name == "screenshot" and "path" in result:
            await self._api.send_photo(
                self._chat_id, result["path"],
                caption=f"📸 {description or 'Screenshot'} — {result.get('size_kb', '?')} KB"
            )
            return

        await self._api.send_message(self._chat_id, fmt(result))

    async def _send_confirmation(self, tool: str, args: dict, description: str) -> None:
        key = secrets.token_hex(6)
        self._pending[key] = {"tool": tool, "args": args, "description": description}
        args_preview = ", ".join(f"{k}={repr(v)[:50]}" for k, v in args.items())
        text = (
            f"⚠️ *Confirmation required*\n\n"
            f"🔧 *Tool:* `{tool}`\n"
            f"📝 *Action:* {description}\n"
            f"🔩 *Args:* `{args_preview or '(none)'}`\n\n"
            f"Are you sure?"
        )
        markup = {
            "inline_keyboard": [[
                {"text": "✅ Yes, do it", "callback_data": f"confirm:{key}"},
                {"text": "❌ Cancel",     "callback_data": f"cancel:{key}"},
            ]]
        }
        await self._api.send_message(self._chat_id, text, reply_markup=markup)

    # ------------------------------------------------------------------
    # Slash command handlers
    # ------------------------------------------------------------------

    async def _cmd_start(self, args: str, message: dict) -> None:
        await self._api.send_message(
            self._chat_id,
            "👋 *doit-fm agent is running!*\n\n"
            "Send me a natural language command and I'll execute it on your computer.\n\n"
            "Examples:\n"
            "  • `how's my cpu?`\n"
            "  • `take a screenshot`\n"
            "  • `what's the weather in Tokyo?`\n"
            "  • `remind me in 10 minutes to drink water`\n"
            "  • `list files in ~/Documents`\n\n"
            "Type /help to see all slash commands.",
        )

    async def _cmd_help(self, args: str, message: dict) -> None:
        commands = "\n".join(
            f"  /{cmd}" for cmd in sorted(self._commands.keys())
        )
        await self._api.send_message(
            self._chat_id,
            f"*doit-fm Commands:*\n\n{commands}\n\n"
            "Or just type anything in plain English!",
        )

    async def _cmd_health(self, args: str, message: dict) -> None:
        await self._api.send_chat_action(self._chat_id, "typing")
        result = await run_tool("system_health", {})
        await self._api.send_message(self._chat_id, fmt(result))

    async def _cmd_status(self, args: str, message: dict) -> None:
        await self._cmd_health(args, message)

    async def _cmd_time(self, args: str, message: dict) -> None:
        result = await run_tool("datetime", {})
        await self._api.send_message(self._chat_id, fmt(result))

    async def _cmd_screenshot(self, args: str, message: dict) -> None:
        await self._api.send_chat_action(self._chat_id, "upload_photo")
        result = await run_tool("screenshot", {})
        if "error" in result:
            await self._api.send_message(self._chat_id, fmt(result))
        else:
            await self._api.send_photo(
                self._chat_id, result["path"],
                caption=f"📸 Screenshot — {result.get('size_kb', '?')} KB"
            )

    async def _cmd_weather(self, args: str, message: dict) -> None:
        if not args:
            await self._api.send_message(self._chat_id, "❓ Usage: /weather <city>")
            return
        await self._api.send_chat_action(self._chat_id, "typing")
        result = await run_tool("weather", {"city": args.strip()})
        await self._api.send_message(self._chat_id, fmt(result))

    async def _cmd_ls(self, args: str, message: dict) -> None:
        path = args.strip() or "~"
        result = await run_tool("list_directory", {"path": path})
        await self._api.send_message(self._chat_id, fmt(result))

    async def _cmd_run(self, args: str, message: dict) -> None:
        if not args:
            await self._api.send_message(self._chat_id, "❓ Usage: /run <command>")
            return
        key = secrets.token_hex(6)
        self._pending[key] = {"tool": "run_shell", "args": {"command": args}, "description": f"Run: {args}"}
        markup = {
            "inline_keyboard": [[
                {"text": "✅ Run it", "callback_data": f"confirm:{key}"},
                {"text": "❌ Cancel", "callback_data": f"cancel:{key}"},
            ]]
        }
        await self._api.send_message(
            self._chat_id,
            f"⚠️ *Run shell command?*\n```\n{args}\n```",
            reply_markup=markup,
        )

    async def _cmd_ps(self, args: str, message: dict) -> None:
        await self._api.send_chat_action(self._chat_id, "typing")
        result = await run_tool("list_processes", {})
        await self._api.send_message(self._chat_id, fmt(result))

    async def _cmd_battery(self, args: str, message: dict) -> None:
        result = await run_tool("battery", {})
        await self._api.send_message(self._chat_id, fmt(result))

    async def _cmd_ping(self, args: str, message: dict) -> None:
        host = args.strip() or "8.8.8.8"
        await self._api.send_chat_action(self._chat_id, "typing")
        result = await run_tool("ping", {"host": host})
        await self._api.send_message(self._chat_id, fmt(result))

    async def _cmd_memory(self, args: str, message: dict) -> None:
        result = await run_tool("system_health", {})
        if "error" in result:
            await self._api.send_message(self._chat_id, fmt(result))
            return
        ram = result["ram_percent"]
        msg = (
            f"🧠 *Memory Usage*\n"
            f"{_bar(ram)} {ram}%\n"
            f"  Used: {result['ram_used_gb']} GB\n"
            f"  Total: {result['ram_total_gb']} GB"
        )
        await self._api.send_message(self._chat_id, msg)

    async def _cmd_clear_memory(self, args: str, message: dict) -> None:
        self._engine.memory.clear()
        await self._api.send_message(self._chat_id, "🧹 *Conversation memory cleared.*")
