"""
Doit Agent - Telegram Interface v2.1
UX goals:
  • Non-technical users feel welcome — plain language everywhere
  • Progressive disclosure — /help gives examples, /tools gives detail
  • Inline buttons for dangerous confirmations
  • Rich result formatting — short, readable, no JSON dumps
  • Voice / photo / document handling with friendly guidance
  • Smart safe-mode, inline /run, /memory, /clear
"""
from __future__ import annotations

import asyncio, json, logging, os, time
from typing import Any, Optional

from telegram import Bot, Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (
    Application, CommandHandler, MessageHandler,
    CallbackQueryHandler, ContextTypes, filters,
)
from telegram.error import NetworkError, TimedOut

from core.config import TELEGRAM_RETRY_MAX
from security.security import AuthorizationManager, InjectionGuard
from task_engine.engine import get_engine, Task
from persistence.database import get_db

logger = logging.getLogger("doit.telegram")

MAX_MSG = 3800  # Safe Telegram limit (real limit 4096)

# ── Formatting helpers ────────────────────────────────────────────────────────

def _truncate(text: str, limit: int = MAX_MSG) -> str:
    if len(text) <= limit:
        return text
    return text[:limit] + "\n\n…_(message truncated — use /export for full output)_"


def _fmt_size(bytes_: int) -> str:
    for unit in ("B", "KB", "MB", "GB"):
        if bytes_ < 1024:
            return f"{bytes_:.1f} {unit}"
        bytes_ /= 1024
    return f"{bytes_:.1f} TB"


def _fmt_result(result: Any, description: str = "") -> str:
    """Convert a tool result dict into a readable, friendly Telegram message."""
    if result is None:
        return f"✅ Done{': ' + description if description else '.'}"

    if isinstance(result, dict):
        # Error
        if "error" in result:
            err = result["error"]
            return (
                f"😕 Something went wrong:\n`{err}`\n\n"
                "Try rephrasing your request, or check that the file/path exists."
            )

        lines = []
        if description:
            lines.append(f"✅ {description}")

        # Special-case common result shapes for cleaner display
        # Directory listing
        if "entries" in result:
            entries = result["entries"]
            count = result.get("count", len(entries))
            lines.append(f"📁 {result.get('path', '')} — {count} items\n")
            for e in entries[:30]:
                icon = "📁" if e.get("type") == "dir" else "📄"
                size = f" ({_fmt_size(e['size'])})" if e.get("size") else ""
                lines.append(f"  {icon} {e['name']}{size}")
            if count > 30:
                lines.append(f"  _…and {count - 30} more_")
            return "\n".join(lines)

        # File content
        if "content" in result and "path" in result:
            content = result["content"]
            lines.append(f"📄 {result['path']}")
            lines.append("```")
            lines.append(content[:2000])
            if len(content) > 2000:
                lines.append("…(truncated)")
            lines.append("```")
            return "\n".join(lines)

        # Process list
        if "processes" in result:
            procs = result["processes"][:15]
            lines.append(f"⚙️ Top processes ({result.get('total', len(procs))} total):\n")
            for p in procs:
                mem = f"{p.get('mem_mb', 0):.0f}MB" if p.get("mem_mb") else "?"
                lines.append(f"  `{p.get('pid','?'):>6}` {p.get('name','?'):<25} {mem}")
            return "\n".join(lines)

        # Search results
        if "results" in result and "pattern" in result:
            results = result["results"]
            lines.append(f"🔍 Found {len(results)} match(es) for `{result['pattern']}`\n")
            for r in results[:20]:
                icon = "📁" if r.get("type") == "dir" else "📄"
                lines.append(f"  {icon} `{r['path']}`")
                if r.get("matching_lines"):
                    for ml in r["matching_lines"][:2]:
                        lines.append(f"    > {ml.strip()[:80]}")
            return "\n".join(lines)

        # System monitor
        if "cpu_percent" in result and "ram_percent" in result:
            cpu = result["cpu_percent"]
            ram_pct = result["ram_percent"]
            disk_pct = result.get("disk_percent", 0)
            cpu_bar = _bar(cpu); ram_bar = _bar(ram_pct); disk_bar = _bar(disk_pct)
            return (
                f"💻 *System Health*\n\n"
                f"CPU:  {cpu_bar} {cpu}%\n"
                f"RAM:  {ram_bar} {ram_pct}%  "
                f"({result.get('ram_used_gb','?')}/{result.get('ram_total_gb','?')} GB)\n"
                f"Disk: {disk_bar} {disk_pct}%  "
                f"({result.get('disk_used_gb','?')}/{result.get('disk_total_gb','?')} GB)\n"
                f"Net:  ↑{result.get('net_sent_mb','?')} MB  ↓{result.get('net_recv_mb','?')} MB"
            )

        # Shell output
        if "stdout" in result or "returncode" in result:
            stdout = result.get("stdout", "").strip()
            stderr = result.get("stderr", "").strip()
            rc = result.get("returncode", 0)
            icon = "✅" if rc == 0 else "❌"
            out = f"{icon} Exit code: {rc}\n"
            if stdout:
                out += f"```\n{stdout[:2000]}\n```"
            if stderr:
                out += f"\n⚠️ Errors:\n```\n{stderr[:500]}\n```"
            return out

        # Weather
        if "temp_c" in result:
            return (
                f"🌤 *{result.get('city', 'Weather')}*\n\n"
                f"🌡 {result.get('temp_c')}°C / {result.get('temp_f')}°F\n"
                f"💧 Humidity: {result.get('humidity')}%\n"
                f"💨 Wind: {result.get('wind_kmph')} km/h\n"
                f"☁️ {result.get('description', '')}"
            )

        # Web search
        if "results" in result and "query" in result:
            results = result["results"]
            lines.append(f"🔍 Results for: *{result['query']}*\n")
            for r in results[:5]:
                title = r.get("title", "")[:80]
                snippet = r.get("snippet", "")[:120]
                url = r.get("url", "")
                lines.append(f"• *{title}*\n  {snippet}")
                if url: lines.append(f"  {url}")
            return "\n".join(lines)

        # Memory recall
        if "memories" in result:
            mems = result["memories"]
            if not mems:
                return "🧠 No memories stored yet. Tell me something to remember!"
            lines.append("🧠 *What I remember:*\n")
            for k, v in mems.items():
                lines.append(f"• *{k}*: {v}")
            return "\n".join(lines)

        # Todo list
        if "todos" in result:
            todos = result["todos"]
            if not todos:
                return "✅ Your to-do list is empty!"
            lines.append("📋 *Your To-Do List:*\n")
            priority_icon = {"high": "🔴", "normal": "🟡", "low": "🟢"}
            for t in todos:
                icon = priority_icon.get(t.get("priority", "normal"), "•")
                done = "~~" if t.get("done") else ""
                lines.append(f"{icon} {done}{t['task']}{done}")
            return "\n".join(lines)

        # Notes list
        if "notes" in result:
            notes = result["notes"]
            if not notes:
                return "📝 No notes yet. Say 'make a note about...' to start!"
            lines.append(f"📝 *Your Notes ({len(notes)}):*\n")
            for n in notes:
                lines.append(f"• {n['title']}")
            return "\n".join(lines)

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

        # Translation
        if "translated" in result:
            return f"🌐 Translation:\n\n*{result['translated']}*"

        # Git status
        if "status" in result and "recent_commits" in result:
            status = result["status"] or "Working tree clean"
            commits = result.get("recent_commits", "")
            out = f"🌿 *Git Status*\n```\n{status}\n```"
            if commits:
                out += f"\n\n📝 *Recent commits:*\n```\n{commits}\n```"
            return out

        # Generic fallback — render key: value pairs
        for k, v in result.items():
            if k == "success": continue
            if isinstance(v, (dict, list)):
                val = json.dumps(v, indent=2)[:300]
            else:
                val = str(v)
            lines.append(f"• *{k}*: {val}")
        return "\n".join(lines) if lines else "✅ Done"

    return f"✅ {description}\n{str(result)[:500]}" if description else str(result)[:500]


def _bar(pct: float, width: int = 10) -> str:
    """ASCII progress bar."""
    filled = int(pct / 100 * width)
    return "█" * filled + "░" * (width - filled)


# ── Bot class ─────────────────────────────────────────────────────────────────

class TelegramBot:
    def __init__(self, token, auth, ai_engine, safe_mode_callback=None):
        self._token = token
        self._auth: AuthorizationManager = auth
        self._ai = ai_engine
        self._safe_mode_cb = safe_mode_callback
        self._app: Optional[Application] = None
        self._bot: Optional[Bot] = None
        self._safe_mode = False
        # Held task store: task_id → Task (awaiting inline button confirmation)
        self._held: dict[str, Task] = {}

    async def start(self):
        """Start with automatic reconnect on network errors."""
        backoff = 5
        while True:
            try:
                await self._run()
            except (NetworkError, TimedOut) as e:
                logger.warning("Network error: %s — retrying in %ds", e, backoff)
                await asyncio.sleep(backoff)
                backoff = min(backoff * 2, TELEGRAM_RETRY_MAX)
            except Exception as e:
                logger.error("Bot crashed: %s — restarting in 10s", e)
                await asyncio.sleep(10)
                backoff = 5

    async def _run(self):
        self._app = Application.builder().token(self._token).build()
        self._bot = self._app.bot

        # Register commands
        cmds = [
            ("start",   self._cmd_start),
            ("help",    self._cmd_help),
            ("status",  self._cmd_status),
            ("tasks",   self._cmd_tasks),
            ("logs",    self._cmd_logs),
            ("health",  self._cmd_health),
            ("export",  self._cmd_export),
            ("tools",   self._cmd_tools),
            ("memory",  self._cmd_memory),
            ("clear",   self._cmd_clear),
            ("run",     self._cmd_run),
            ("safe",    self._cmd_safe),
            ("resume",  self._cmd_resume),
            ("cancel",  self._cmd_cancel),
        ]
        for name, handler in cmds:
            self._app.add_handler(CommandHandler(name, handler))

        # Message types
        self._app.add_handler(
            MessageHandler(filters.TEXT & ~filters.COMMAND, self._handle_message))
        self._app.add_handler(MessageHandler(filters.VOICE, self._handle_voice))
        self._app.add_handler(MessageHandler(filters.Document.ALL, self._handle_document))
        self._app.add_handler(MessageHandler(filters.PHOTO, self._handle_photo))
        self._app.add_handler(CallbackQueryHandler(self._handle_callback))

        # Register task result callback
        get_engine().on_result(self._deliver_result)

        logger.info("Telegram bot starting")
        async with self._app:
            await self._app.start()
            await self._app.updater.start_polling(
                drop_pending_updates=True,
                timeout=30,
                allowed_updates=["message", "callback_query"],
            )
            while True:
                await asyncio.sleep(1)

    # ── Authorization ─────────────────────────────────────────────────────────

    def _authed(self, update: Update) -> bool:
        return bool(update.effective_user and
                    self._auth.is_authorized(update.effective_user.id))

    async def _deny(self, update: Update):
        try:
            await update.message.reply_text(
                "🔒 Sorry, I only take orders from my owner. "
                "If you set this bot up, check that your Telegram ID is configured correctly."
            )
        except Exception:
            pass

    # ── Result delivery ───────────────────────────────────────────────────────

    async def _deliver_result(self, task: Task):
        """Called by the task engine when a task completes."""
        try:
            uid = self._auth.authorized_user_id()
            if not uid:
                return
            description = task.payload.get("description", task.type)
            msg = _fmt_result(task.result, description)
            await self._bot.send_message(
                uid, _truncate(msg), parse_mode="Markdown")
        except Exception as e:
            logger.warning("Failed to deliver result: %s", e)

    # ── Commands ──────────────────────────────────────────────────────────────

    async def _cmd_start(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        await update.message.reply_text(
            "👋 *Hey! I'm Doit — your personal computer assistant.*\n\n"
            "Just talk to me naturally and I'll take care of it. No need to learn "
            "any commands or technical language.\n\n"
            "*Try saying things like:*\n"
            "  📁  _show me what's in my Downloads folder_\n"
            "  💾  _backup my Documents folder_\n"
            "  🔍  _find all my PDF files_\n"
            "  📊  _how much space is left on my computer?_\n"
            "  ☁️  _what's the weather in London?_\n"
            "  ⏰  _remind me to take a break in 30 minutes_\n"
            "  📝  _make a note: call dentist on Friday_\n"
            "  🌐  _search for Python tutorials_\n\n"
            "Type /help to see everything I can do!",
            parse_mode="Markdown",
        )

    async def _cmd_help(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        await update.message.reply_text(
            "🏋️ *What I can do for you:*\n\n"
            "📁 *Files & Folders*\n"
            "  List, search, copy, move, delete, zip, backup, organize\n"
            "  _'find my invoice file'  •  'organize Downloads'_\n\n"
            "💻 *Your Computer*\n"
            "  Check health, see processes, monitor resources, shutdown\n"
            "  _'how's my computer doing?'  •  'what's using up RAM?'_\n\n"
            "🌐 *Internet & Network*\n"
            "  Download files, search the web, check websites, make requests\n"
            "  _'download this file'  •  'is my website up?'_\n\n"
            "🖥 *Desktop Automation*\n"
            "  Open apps, take screenshots, control volume, send notifications\n"
            "  _'take a screenshot'  •  'turn volume to 50'_\n\n"
            "📅 *Scheduling*\n"
            "  Recurring backups, reminders, automatic tasks\n"
            "  _'remind me in 20 minutes'  •  'backup daily at 9am'_\n\n"
            "🧠 *Memory & Notes*\n"
            "  I can remember things, keep notes, manage your to-do list\n"
            "  _'remember my server IP is 10.0.0.1'  •  'add todo: buy milk'_\n\n"
            "🛠 *Developer Tools*\n"
            "  Git, Docker, run tests, build projects, SSH to servers\n\n"
            "🎁 *Extras*\n"
            "  Weather, translation, calculations, QR codes, text-to-speech\n\n"
            "━━━━━━━━━━━━━━━━━━━\n"
            "*Slash commands:*\n"
            "/health — system resources\n"
            "/tasks — recent task history\n"
            "/memory — things I've remembered\n"
            "/tools — full list of capabilities\n"
            "/run cmd — run a shell command\n"
            "/clear — reset conversation context\n"
            "/safe • /resume — pause/resume execution\n"
            "/status — agent status\n"
            "/export — download audit log\n",
            parse_mode="Markdown",
        )

    async def _cmd_status(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        engine = get_engine()
        s = engine.status()
        mode = "🔴 PAUSED (safe mode)" if s["safe_mode"] else "🟢 Active"
        ai_status = "✅ Online" if self._ai._available else "⚠️ Offline (using fallback or retrying)"
        held = len(self._held)
        msg = (
            f"🤖 *Doit Status*\n\n"
            f"Agent: {mode}\n"
            f"AI: {ai_status}\n"
            f"Queue: {s['queue_size']} task(s) pending\n"
            f"Running: {s['running']} task(s) now\n"
            f"Workers: {s['workers']}"
        )
        if held:
            msg += f"\n\n⏸ {held} task(s) waiting for your confirmation"
        if s.get("running_tasks"):
            items = "\n".join(f"  • {t['type']}" for t in s["running_tasks"])
            msg += f"\n\n*Currently running:*\n{items}"
        await update.message.reply_text(msg, parse_mode="Markdown")

    async def _cmd_tasks(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        db = await get_db()
        tasks = await db.tasks_list(limit=12)
        if not tasks:
            await update.message.reply_text("No tasks yet. Just tell me what to do!"); return
        icons = {"completed": "✅", "failed": "❌", "running": "⚙️", "pending": "⏳", "held": "⏸"}
        lines = ["📋 *Recent Tasks:*\n"]
        for t in tasks:
            icon = icons.get(t.get("status", ""), "❓")
            desc = t.get("payload", {}).get("description") or t.get("type", "?")
            ts = t.get("created_at", "")[:16].replace("T", " ")
            lines.append(f"{icon} {desc}  _({ts})_")
        await update.message.reply_text("\n".join(lines), parse_mode="Markdown")

    async def _cmd_health(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        await update.message.chat.send_action("typing")
        from tools.registry import get_registry
        reg = get_registry()
        monitor = await reg.execute("sys_monitor", {})
        batt = await reg.execute("sys_battery", {})
        info = await reg.execute("sys_info", {})
        msg = _fmt_result(monitor)
        uptime = info.get("uptime_hours", "?")
        msg += f"\n⏱ Uptime: {uptime}h"
        if "percent" in batt:
            plug_icon = "🔌" if batt.get("plugged") else "🔋"
            msg += f"\n{plug_icon} Battery: {batt['percent']}%"
            if batt.get("time_left_min"):
                msg += f" ({batt['time_left_min']} min left)"
        await update.message.reply_text(msg, parse_mode="Markdown")

    async def _cmd_logs(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        db = await get_db()
        logs = await db.logs_list(limit=15)
        if not logs:
            await update.message.reply_text("No logs yet."); return
        icons = {"ERROR": "❌", "WARNING": "⚠️", "INFO": "ℹ️", "DEBUG": "🔍"}
        lines = ["📄 *Recent Logs:*\n"]
        for l in logs:
            icon = icons.get(l.get("level", "INFO"), "•")
            ts = l.get("timestamp", "")[:19].replace("T", " ")
            lines.append(f"{icon} `{ts}` {l.get('message','')[:80]}")
        await update.message.reply_text("\n".join(lines), parse_mode="Markdown")

    async def _cmd_export(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        fmt = (ctx.args[0].lower() if ctx.args else "json")
        if fmt not in ("json", "csv"):
            await update.message.reply_text("Use `/export json` or `/export csv`", parse_mode="Markdown"); return
        db = await get_db()
        audit = await db.audit_export(format=fmt)
        import tempfile
        with tempfile.NamedTemporaryFile(mode="w", suffix=f".{fmt}", delete=False) as f:
            f.write(audit); fname = f.name
        try:
            with open(fname, "rb") as f:
                await update.message.reply_document(f, filename=f"doit_audit.{fmt}",
                                                     caption=f"Audit log exported as {fmt.upper()}")
        finally:
            os.unlink(fname)

    async def _cmd_tools(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        from tools.registry import get_registry
        tools = get_registry().list_with_descriptions()
        categories = [
            ("📁 Files", lambda t: t["name"].startswith("fs_")),
            ("💻 System", lambda t: t["name"].startswith("sys_")),
            ("🐚 Shell & Code", lambda t: t["name"] in ("shell_exec","repl_python","repl_js")),
            ("🌿 Git", lambda t: t["name"].startswith("git_")),
            ("🐳 Docker", lambda t: t["name"].startswith("docker_")),
            ("📦 Packages", lambda t: t["name"].startswith("pkg_")),
            ("🌐 Network & Web", lambda t: t["name"].startswith(("net_","web_")) or t["name"]=="webhook_send"),
            ("🔒 SSH / SFTP", lambda t: t["name"].startswith(("ssh_","sftp_"))),
            ("🖥 Desktop", lambda t: t["name"] in ("app_open","notify_send","screenshot_take",
                                                      "clipboard_read","clipboard_write","type_text",
                                                      "hotkey","vol_get","vol_set","speech_say")),
            ("🛠 Dev Tools", lambda t: t["name"] in ("test_run","lint_run","format_run","build_run")),
            ("📄 Files+", lambda t: t["name"].startswith(("pdf_","img_","csv_","json_","crypto_","qr_"))),
            ("🧠 Memory & Notes", lambda t: t["name"].startswith(("memory_","note_","todo_","secret_"))),
            ("🎯 Utilities", lambda t: t["name"] in ("calc","translate","weather","reminder_set")),
        ]
        pages = []
        for cat_name, matcher in categories:
            cat_tools = [t for t in tools if matcher(t)]
            if not cat_tools: continue
            lines = [f"*{cat_name}*"]
            for t in cat_tools:
                warn = " ⚠️" if t["dangerous"] else ""
                lines.append(f"  `{t['name']}`{warn} — {t['description']}")
            pages.append("\n".join(lines))
        
        # Send in chunks under 3800 chars
        chunk = f"🗂 *All {len(tools)} tools:*\n\n"
        for page in pages:
            if len(chunk) + len(page) + 2 > 3600:
                await update.message.reply_text(chunk, parse_mode="Markdown")
                chunk = page
            else:
                chunk = (chunk + "\n\n" + page).strip()
        if chunk:
            await update.message.reply_text(chunk, parse_mode="Markdown")
        await update.message.reply_text(
            "💡 _Just describe what you want in plain language — "
            "no need to use tool names directly!_",
            parse_mode="Markdown")

    async def _cmd_memory(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        from tools.registry import get_registry
        result = await get_registry().execute("memory_recall", {})
        await update.message.reply_text(
            _truncate(_fmt_result(result, "Stored memories")), parse_mode="Markdown")

    async def _cmd_clear(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        self._ai.clear_memory()
        await update.message.reply_text(
            "🗑 Conversation context cleared!\n\n"
            "I've forgotten what we were talking about, but your saved memories "
            "and notes are still there. Fresh start! 😊")

    async def _cmd_run(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        if not ctx.args:
            await update.message.reply_text(
                "Usage: `/run <shell command>`\n\nExample: `/run ls -la ~/Downloads`",
                parse_mode="Markdown"); return
        if self._safe_mode:
            await update.message.reply_text(
                "🔴 Execution is paused. Send `/resume` to continue.",
                parse_mode="Markdown"); return
        cmd = " ".join(ctx.args)
        await update.message.chat.send_action("typing")
        from tools.registry import get_registry
        result = await get_registry().execute("shell_exec", {"command": cmd})
        msg = _fmt_result(result, f"Run: `{cmd}`")
        await update.message.reply_text(_truncate(msg), parse_mode="Markdown")

    async def _cmd_safe(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        self._safe_mode = True
        if self._safe_mode_cb:
            await self._safe_mode_cb(True)
        await update.message.reply_text(
            "🔴 *Safe mode ON*\n\nAll task execution is paused. "
            "I'll still understand your messages but won't take any action. "
            "Send `/resume` when you're ready.", parse_mode="Markdown")

    async def _cmd_resume(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        self._safe_mode = False
        if self._safe_mode_cb:
            await self._safe_mode_cb(False)
        await update.message.reply_text(
            "🟢 *Ready!* Execution resumed — what would you like me to do?",
            parse_mode="Markdown")

    async def _cmd_cancel(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        held = list(self._held.keys())
        if not held:
            await update.message.reply_text("Nothing pending to cancel."); return
        for tid in held:
            del self._held[tid]
        await update.message.reply_text(f"❌ Cancelled {len(held)} pending task(s).")

    # ── Message handlers ──────────────────────────────────────────────────────

    async def _handle_message(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        text = update.message.text.strip()
        if not text: return

        # Injection guard
        safe, reason = InjectionGuard.check_message(text)
        if not safe:
            await update.message.reply_text(
                f"⛔ I can't process that message. "
                f"If you were trying something legitimate, please rephrase it."); return

        # Safe mode check
        if self._safe_mode:
            lower = text.lower()
            if lower in ("resume", "/resume", "safe mode off", "unpause"):
                self._safe_mode = False
                await update.message.reply_text("🟢 Resumed! What would you like me to do?"); return
            await update.message.reply_text(
                "🔴 I'm currently paused.\n"
                "Say *resume* or send `/resume` to let me take action again.",
                parse_mode="Markdown"); return

        # Show typing
        await update.message.chat.send_action("typing")

        # Understand intent
        action = await self._ai.parse_intent(text)
        await self._execute_action(update, action)

    async def _handle_voice(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        await update.message.reply_text(
            "🎤 *Voice message received!*\n\n"
            "Voice transcription is available when you configure a Whisper/OpenAI key.\n\n"
            "For now, please type your request — I'm listening! 😊",
            parse_mode="Markdown")

    async def _handle_document(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        doc = update.message.document
        caption = update.message.caption or ""
        size_str = _fmt_size(doc.file_size) if doc.file_size else "unknown size"
        await update.message.reply_text(
            f"📎 Got your file: *{doc.file_name}* ({size_str})\n\n"
            f"{'Caption: ' + caption + chr(10) + chr(10) if caption else ''}"
            "What would you like me to do with it? For example:\n"
            "  • _save it to my Desktop_\n"
            "  • _extract it_\n"
            "  • _read the contents_",
            parse_mode="Markdown")

    async def _handle_photo(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if not self._authed(update): await self._deny(update); return
        caption = update.message.caption or ""
        await update.message.reply_text(
            f"🖼 Got your photo{': ' + caption if caption else ''}!\n\n"
            "What should I do with it? For example:\n"
            "  • _save it to my Pictures folder_\n"
            "  • _resize it to 800px wide_",
            parse_mode="Markdown")

    async def _handle_callback(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        """Handle inline confirm/cancel buttons."""
        query = update.callback_query
        await query.answer()
        if not self._auth.is_authorized(query.from_user.id): return

        data = query.data or ""

        if data.startswith("confirm:"):
            task_id = data.split(":", 1)[1]
            held = self._held.pop(task_id, None)
            if held:
                engine = get_engine()
                held.payload.pop("held", None)  # clear hold flag
                await engine.submit(held)
                await query.edit_message_text(
                    f"✅ *Confirmed!* I'll take care of it now…",
                    parse_mode="Markdown")
            else:
                await query.edit_message_text("⚠️ Task not found — it may have expired.")

        elif data.startswith("cancel:"):
            task_id = data.split(":", 1)[1]
            self._held.pop(task_id, None)
            await query.edit_message_text("❌ Cancelled. No changes were made.")

    # ── Action dispatch ───────────────────────────────────────────────────────

    async def _execute_action(self, update: Update, action: dict):
        """Dispatch parsed action — single tool or multi-step plan."""
        if not isinstance(action, dict):
            await update.message.reply_text("😕 I got a confusing response. Please try again."); return

        tool = action.get("tool")
        args = action.get("args", {}) or {}
        description = action.get("description", "")
        metadata = action.get("metadata", {}) or {}
        steps = action.get("steps")

        # Clarify
        if tool == "clarify":
            msg = args.get("message", "Could you give me a bit more detail?")
            await update.message.reply_text(f"🤔 {msg}"); return

        # AI error
        if tool == "error":
            msg = args.get("message", "Something went wrong.")
            await update.message.reply_text(f"⚠️ {msg}"); return

        # ── Multi-step plan ───────────────────────────────────────────────────
        if steps:
            needs_confirm = (
                metadata.get("confirm") or
                any(s.get("metadata", {}).get("confirm") for s in steps)
            )
            if needs_confirm:
                task_id = f"plan_{int(time.time() * 1000)}"
                plan_lines = "\n".join(
                    f"  {i+1}. {s.get('description', s.get('tool','?'))}"
                    for i, s in enumerate(steps))
                keyboard = [[
                    InlineKeyboardButton("✅ Yes, do it", callback_data=f"confirm:{task_id}"),
                    InlineKeyboardButton("❌ Cancel",     callback_data=f"cancel:{task_id}"),
                ]]
                # Store as held task
                held_task = Task(
                    task_type="multi_step",
                    task_id=task_id,
                    payload={"steps": steps, "description": description},
                )
                self._held[task_id] = held_task
                await update.message.reply_text(
                    f"📋 *Plan: {description}*\n\n{plan_lines}\n\n"
                    "⚠️ This will make changes to your computer. Confirm?",
                    reply_markup=InlineKeyboardMarkup(keyboard),
                    parse_mode="Markdown")
            else:
                task = Task(
                    task_type="multi_step",
                    payload={"steps": steps, "description": description},
                )
                await get_engine().submit(task)
                await update.message.reply_text(
                    f"⚙️ *{description}*\n\nOn it! I'll let you know when done.",
                    parse_mode="Markdown")
            return

        # ── Single action with confirmation ───────────────────────────────────
        if metadata.get("confirm"):
            task_id = f"task_{int(time.time() * 1000)}"
            # Build friendly description
            args_preview = json.dumps(args)[:200] if args else ""
            msg = (
                f"⚠️ *Before I do this, please confirm:*\n\n"
                f"*{description}*\n"
            )
            if args_preview:
                msg += f"\n`{args_preview}`\n"
            msg += "\nThis action cannot be easily undone."
            keyboard = [[
                InlineKeyboardButton("✅ Yes, do it", callback_data=f"confirm:{task_id}"),
                InlineKeyboardButton("❌ Cancel",     callback_data=f"cancel:{task_id}"),
            ]]
            held_task = Task(
                task_type=tool,
                task_id=task_id,
                payload={**args, "description": description},
            )
            self._held[task_id] = held_task
            await update.message.reply_text(msg,
                                             reply_markup=InlineKeyboardMarkup(keyboard),
                                             parse_mode="Markdown")
            return

        # ── Execute immediately ───────────────────────────────────────────────
        task = Task(
            task_type=tool,
            payload={**args, "description": description},
        )
        await get_engine().submit(task)
        # Friendly acknowledgement
        friendly_acks = {
            "fs_list":   "Looking at your files…",
            "fs_search": "Searching…",
            "sys_monitor": "Checking your system…",
            "web_search": "Searching the web…",
            "weather":   "Getting the weather…",
            "calc":      "Calculating…",
        }
        ack = friendly_acks.get(tool, f"Working on it…")
        if description:
            ack = description + " ⚙️"
        await update.message.reply_text(f"⏳ {ack}")
