"""
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 datetime import datetime
from pathlib import Path
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 doit_fm.core.config import TELEGRAM_RETRY_MAX
from doit_fm.security.security import AuthorizationManager, InjectionGuard
from doit_fm.task_engine.engine import get_engine
from doit_fm.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):
        from doit_fm.task_engine.engine import Task
        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

    async def send_message(self, text: str) -> None:
        """Send a message to the authorized owner — used by health monitor and startup notify."""
        try:
            uid = self._auth.authorized_user_id()
            if not uid or not self._bot:
                return
            await self._bot.send_message(uid, _truncate(text), parse_mode="Markdown")
        except Exception as e:
            logger.warning("send_message failed: %s", e)

    # ── 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
        try:
            db = await get_db()
            tasks = await db.tasks_list(limit=12)
        except Exception:
            # Fallback: show engine queue
            engine = get_engine()
            s = engine.status()
            await update.message.reply_text(
                f"📋 *Task Queue*\n\n"
                f"Pending: {s.get('queue_size', 0)}\n"
                f"Running: {s.get('running', 0)}\n\n"
                "_Note: Database unavailable — task history not accessible._",
                parse_mode="Markdown")
            return
        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 doit_fm.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
        try:
            db = await get_db()
            logs = await db.logs_list(limit=15)
        except Exception:
            # Fallback: read from log file
            from doit_fm.core.config import LOG_DIR
            log_file = LOG_DIR / "doit-fm.log"
            if log_file.exists():
                try:
                    lines = log_file.read_text().splitlines()[-15:]
                    await update.message.reply_text(
                        "📄 *Recent Logs* (from file):\n\n```\n" + "\n".join(lines[-15:]) + "\n```",
                        parse_mode="Markdown")
                    return
                except Exception:
                    pass
            await update.message.reply_text("📄 No logs available yet."); return
        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("created_at", 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
        try:
            db = await get_db()
            audit = await db.audit_export(format=fmt)
        except Exception as e:
            # Fallback: export memory, notes, todos as JSON
            from doit_fm.tools.registry import get_registry
            reg = get_registry()
            mem = await reg.execute("memory_recall", {})
            notes = await reg.execute("note_list", {})
            todos = await reg.execute("todo_list", {"show_done": True})
            schedules = await reg.execute("schedule_list", {})
            export_data = {
                "exported_at": datetime.now().isoformat(),
                "memories": mem.get("memories", {}),
                "notes": notes.get("notes", []),
                "todos": todos.get("todos", []),
                "schedules": schedules.get("schedules", []),
                "note": f"Full audit log unavailable: {e}",
            }
            import json as _json
            audit = _json.dumps(export_data, indent=2)
            fmt = "json"
        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_export.{fmt}",
                                                     caption=f"Export ({fmt.upper()}) — {datetime.now().strftime('%Y-%m-%d %H:%M')}")
        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 doit_fm.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","vol_mute","vol_unmute","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_"))),
            ("📅 Schedules", lambda t: t["name"].startswith("schedule_")),
            ("🎯 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 ""
                # Escape special Markdown chars in description
                desc = t["description"].replace("_", "\\_").replace("*", "\\*")
                lines.append(f"  `{t['name']}`{warn} — {desc}")
            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:
                try:
                    await update.message.reply_text(chunk, parse_mode="Markdown")
                except Exception:
                    await update.message.reply_text(chunk)  # fallback no parse
                chunk = page
            else:
                chunk = (chunk + "\n\n" + page).strip()
        if chunk:
            try:
                await update.message.reply_text(chunk, parse_mode="Markdown")
            except Exception:
                await update.message.reply_text(chunk)
        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 doit_fm.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 doit_fm.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:
            import asyncio as _aio
            if _aio.iscoroutinefunction(self._safe_mode_cb):
                await self._safe_mode_cb(True)
            else:
                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:
            import asyncio as _aio
            if _aio.iscoroutinefunction(self._safe_mode_cb):
                await self._safe_mode_cb(False)
            else:
                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
        voice = update.message.voice or update.message.audio
        # Try transcription via whisper if available
        try:
            import openai  # noqa
            file = await ctx.bot.get_file(voice.file_id)
            import tempfile
            with tempfile.NamedTemporaryFile(suffix=".ogg", delete=False) as tmp:
                await file.download_to_drive(tmp.name)
                tmp_path = tmp.name
            try:
                client = openai.OpenAI(api_key=self._ai.api_key)
                with open(tmp_path, "rb") as audio_file:
                    transcript = client.audio.transcriptions.create(model="whisper-1", file=audio_file)
                text = transcript.text.strip()
                if text:
                    await update.message.reply_text(f"🎤 _Transcribed:_ {text}", parse_mode="Markdown")
                    await update.message.chat.send_action("typing")
                    action = await self._ai.parse_intent(text)
                    await self._execute_action(update, action)
                    return
            finally:
                import os as _os
                try: _os.unlink(tmp_path)
                except: pass
        except Exception:
            pass
        await update.message.reply_text(
            "🎤 *Voice message received!*\n\n"
            "Voice transcription requires an OpenAI Whisper 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"

        # Download file to temp location
        try:
            file = await ctx.bot.get_file(doc.file_id)
            download_dir = Path.home() / "Downloads"
            download_dir.mkdir(parents=True, exist_ok=True)
            local_path = download_dir / doc.file_name
            await file.download_to_drive(str(local_path))
            saved_msg = f"✅ Saved to: `{local_path}`"
        except Exception as e:
            saved_msg = f"⚠️ Could not auto-save: {e}"
            local_path = None

        # If there's a caption, parse intent with file context
        if caption:
            await update.message.chat.send_action("typing")
            enhanced_msg = f"{caption}\n[File: {doc.file_name} ({size_str}) saved to {local_path}]"
            action = await self._ai.parse_intent(enhanced_msg)
            await update.message.reply_text(saved_msg)
            await self._execute_action(update, action)
        else:
            await update.message.reply_text(
                f"📎 Got your file: *{doc.file_name}* ({size_str})\n\n"
                f"{saved_msg}\n\n"
                "What would you like me to do with it? For example:\n"
                "  • _read the contents_\n"
                "  • _extract it_\n"
                "  • _convert it_",
                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 ""
        # Get highest resolution photo
        photo = update.message.photo[-1]
        try:
            file = await ctx.bot.get_file(photo.file_id)
            download_dir = Path.home() / "Pictures"
            download_dir.mkdir(parents=True, exist_ok=True)
            filename = f"photo_{int(time.time())}.jpg"
            local_path = download_dir / filename
            await file.download_to_drive(str(local_path))
            saved_msg = f"✅ Saved to: `{local_path}`"
        except Exception as e:
            saved_msg = f"⚠️ Could not save: {e}"
            local_path = None

        if caption:
            await update.message.chat.send_action("typing")
            enhanced_msg = f"{caption}\n[Photo saved to {local_path}]"
            action = await self._ai.parse_intent(enhanced_msg)
            await update.message.reply_text(saved_msg)
            await self._execute_action(update, action)
        else:
            await update.message.reply_text(
                f"🖼 Got your photo!\n\n{saved_msg}\n\n"
                "What should I do with it? For example:\n"
                "  • _resize it to 800px wide_\n"
                "  • _convert it to PNG_",
                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}")


# ── Helper functions called by CLI init wizard ────────────────────────────────

async def validate_bot_token(token: str) -> tuple[bool, str]:
    """Validate a Telegram bot token by calling getMe API. Returns (ok, info)."""
    try:
        import aiohttp
        async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as sess:
            async with sess.get(f"https://api.telegram.org/bot{token}/getMe") as resp:
                if resp.status != 200:
                    return False, f"HTTP {resp.status}"
                data = await resp.json()
                if not data.get("ok"):
                    return False, data.get("description", "Invalid token")
                bot_info = data["result"]
                username = bot_info.get("username", "?")
                name = bot_info.get("first_name", "?")
                return True, f"@{username} ({name})"
    except Exception as e:
        return False, str(e)


async def send_test_message(token: str, user_id: int) -> tuple[bool, str]:
    """Send a test message to verify bot→user communication. Returns (ok, info)."""
    try:
        import aiohttp
        text = (
            "👋 Hello from doit-fm!\n\n"
            "Setup is almost complete. You'll receive messages here whenever "
            "I complete a task.\n\n"
            "Type /help after setup to see everything I can do!"
        )
        async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as sess:
            async with sess.post(
                f"https://api.telegram.org/bot{token}/sendMessage",
                json={"chat_id": user_id, "text": text, "parse_mode": "Markdown"},
            ) as resp:
                if resp.status == 200:
                    return True, "Message delivered"
                data = await resp.json()
                return False, data.get("description", f"HTTP {resp.status}")
    except Exception as e:
        return False, str(e)
