"""
doit-fm Telegram bot — 15 real commands, zero mocks.
Uses stdlib urllib only — no python-telegram-bot dependency.
Architecture: long-poll getUpdates → AI.parse_intent() → run_tool() → sendMessage
"""
from __future__ import annotations
import asyncio, json, logging, re, urllib.error, urllib.parse, urllib.request
from typing import Any

logger = logging.getLogger("doit.bot")
MAX_MSG = 3900


# ── Telegram HTTP ──────────────────────────────────────────────────────────────

class TelegramError(Exception):
    def __init__(self, code: int, desc: str):
        self.code = code
        super().__init__(f"Telegram {code}: {desc}")


class TelegramAPI:
    def __init__(self, token: str):
        self._base = f"https://api.telegram.org/bot{token}"

    def _post(self, method: str, payload: dict = None, timeout: int = 35) -> dict:
        url  = f"{self._base}/{method}"
        data = json.dumps(payload or {}).encode()
        req  = urllib.request.Request(url, data=data,
                   headers={"Content-Type": "application/json"})
        try:
            with urllib.request.urlopen(req, timeout=timeout) as r:
                return json.loads(r.read())
        except urllib.error.HTTPError as e:
            body = {}
            try: body = json.loads(e.read())
            except Exception: pass
            raise TelegramError(e.code, body.get("description", str(e)))
        except Exception as e:
            raise TelegramError(0, str(e))

    async def _async(self, fn, *a, **kw):
        return await asyncio.get_event_loop().run_in_executor(None, lambda: fn(*a, **kw))

    async def get_updates(self, offset: int = 0, timeout: int = 30) -> list:
        r = await asyncio.get_event_loop().run_in_executor(
            None, lambda: self._post("getUpdates",
                {"offset": offset, "timeout": timeout}, timeout=timeout+5))
        return r.get("result", [])

    async def send(self, chat_id: int, text: str, parse_mode: str = "Markdown",
                   reply_markup: dict = None) -> dict:
        p = {"chat_id": chat_id, "text": text[:MAX_MSG]}
        if parse_mode:      p["parse_mode"]   = parse_mode
        if reply_markup:    p["reply_markup"]  = json.dumps(reply_markup)
        try:
            return await self._async(self._post, "sendMessage", p)
        except TelegramError as e:
            if e.code == 400 and parse_mode:
                p.pop("parse_mode", None)
                p["text"] = re.sub(r"[*_`\[\]]", "", text)[:MAX_MSG]
                return await self._async(self._post, "sendMessage", p)
            raise

    async def send_document(self, chat_id: int, file_path: str, caption: str = "") -> dict:
        """Send a file as a Telegram document attachment."""
        import os
        url = f"{self._base}/sendDocument"
        with open(file_path, "rb") as f:
            file_data = f.read()
        filename = os.path.basename(file_path)
        boundary = "----doit_boundary_xyz"
        body_parts = [
            f"--{boundary}\r\nContent-Disposition: form-data; name=\"chat_id\"\r\n\r\n{chat_id}",
        ]
        if caption:
            body_parts.append(f"--{boundary}\r\nContent-Disposition: form-data; name=\"caption\"\r\n\r\n{caption[:1024]}")
        body_parts.append(
            f"--{boundary}\r\nContent-Disposition: form-data; name=\"document\"; filename=\"{filename}\"\r\nContent-Type: text/plain\r\n\r\n"
        )
        body = "\r\n".join(body_parts).encode()
        body += file_data
        body += f"\r\n--{boundary}--\r\n".encode()
        req = urllib.request.Request(url, data=body,
              headers={"Content-Type": f"multipart/form-data; boundary={boundary}"})
        try:
            loop = asyncio.get_event_loop()
            return await loop.run_in_executor(
                None, lambda: json.loads(urllib.request.urlopen(req, timeout=30).read()))
        except Exception as e:
            logger.warning("send_document failed: %s", e)
            return {"error": str(e)}

    async def typing(self, chat_id: int):
        try:
            await self._async(self._post, "sendChatAction",
                              {"chat_id": chat_id, "action": "typing"})
        except Exception: pass

    async def answer_cb(self, cb_id: str):
        try:
            await self._async(self._post, "answerCallbackQuery",
                              {"callback_query_id": cb_id})
        except Exception: pass

    async def edit(self, chat_id: int, msg_id: int, text: str,
                   parse_mode: str = "Markdown") -> dict:
        p = {"chat_id": chat_id, "message_id": msg_id, "text": text[:MAX_MSG]}
        if parse_mode: p["parse_mode"] = parse_mode
        try:
            return await self._async(self._post, "editMessageText", p)
        except TelegramError as e:
            if e.code == 400 and parse_mode:
                p.pop("parse_mode", None)
                p["text"] = re.sub(r"[*_`\[\]]", "", text)[:MAX_MSG]
                return await self._async(self._post, "editMessageText", p)
            raise


# ── Formatting ─────────────────────────────────────────────────────────────────

def _sz(b: int) -> str:
    b = int(b or 0)
    for u in ("B","KB","MB","GB"):
        if b < 1024: return f"{b:.0f} {u}"
        b //= 1024
    return f"{b:.0f} TB"

def _bar(pct: float, w: int = 10) -> str:
    f = max(0, min(w, round(float(pct)/100*w)))
    return "█"*f + "░"*(w-f)

def _trunc(t: str) -> str:
    return t if len(t) <= MAX_MSG else t[:MAX_MSG-40] + "\n\n…_(truncated)_"


def fmt(result: Any, desc: str = "") -> str:
    """Format a tool result dict into a nice Telegram message."""
    if result is None:
        return f"✅ {desc or 'Done'}"
    if isinstance(result, dict) and "error" in result:
        return f"😕 *Error:*\n`{result['error']}`"
    if not isinstance(result, dict):
        return (f"✅ {desc}\n" if desc else "") + str(result)[:500]

    L = []
    if desc: L.append(f"✅ *{desc}*")

    # sys_health
    if "cpu_percent" in result and "ram_percent" in result:
        cpu  = result["cpu_percent"]
        ram  = result["ram_percent"]
        disk = result.get("disk_percent", 0)
        bat  = result.get("battery_pct")
        bat_str = f"\n🔋 Battery: {_bar(bat)} {bat}%{'  🔌' if result.get('plugged_in') else ''}" if bat is not None else ""
        top  = result.get("top_processes", [])
        top_str = ""
        if top:
            top_str = "\n\n⚙️ Top processes:\n" + "\n".join(
                f"  `{p['pid']:>6}` {p['name']:<20} {p.get('mem_mb',0):.0f} MB" for p in top
            )
        return (
            f"💻 *System Health*\n\n"
            f"CPU:  {_bar(cpu)} {cpu}%\n"
            f"RAM:  {_bar(ram)} {ram}%  ({result.get('ram_used_gb','?')}/{result.get('ram_total_gb','?')} GB)\n"
            f"Disk: {_bar(disk)} {disk}%  ({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"
            f"{bat_str}{top_str}"
        )

    # shell output
    if "stdout" in result or "returncode" in result:
        rc  = result.get("returncode", 0)
        out = result.get("stdout", "").strip()
        err = result.get("stderr", "").strip()
        icon = "✅" if rc == 0 else "❌"
        s = f"{icon} Exit {rc}\n"
        if out: s += f"```\n{out[:2500]}\n```"
        if err: s += f"\n⚠️ stderr:\n```\n{err[:400]}\n```"
        return s

    # vol_get / vol_set
    if "volume" in result:
        v = result["volume"]
        muted = result.get("muted", False)
        icon = "🔇" if muted else ("🔉" if v < 50 else "🔊")
        return f"{icon} Volume: *{v}%*{'  (muted)' if muted else ''}"

    # screenshot
    if "path" in result and result.get("success") and "size_kb" in result:
        return f"📸 Screenshot saved\n`{result['path']}`\n_{result['size_kb']} KB_"

    # app_open
    if "app" in result and result.get("launched"):
        return f"🚀 Launched *{result['app']}*"

    # delete
    if "deleted" in result:
        d = result["deleted"]
        if isinstance(d, list):
            return f"🗑 Deleted {len(d)} item(s):\n" + "\n".join(f"`{x}`" for x in d[:10])
        return f"🗑 Deleted `{d}`"

    # export
    if "entries" in result and "path" in result:
        return f"📋 *Task log exported*\n`{result['path']}`\n_{result['entries']} entries_"

    # fs_list
    if "entries" in result:
        ent = result["entries"]; c = result.get("count", len(ent))
        L.append(f"📁 `{result.get('path','.')}` — {c} item(s)\n")
        for e in ent[:30]:
            icon = "📁" if e.get("type") == "dir" else "📄"
            sz = f" ({_sz(e['size'])})" if e.get("size") else ""
            L.append(f"  {icon} {e['name']}{sz}")
        if c > 30: L.append(f"  _…and {c-30} more_")
        return "\n".join(L)

    # fs_read
    if "content" in result and "path" in result:
        content = result["content"]
        L += [f"📄 `{result['path']}`\n```", content[:2000]]
        if len(content) > 2000: L.append("_(truncated)_")
        L.append("```")
        return "\n".join(L)

    # memory
    if "memories" in result:
        mems = result["memories"]
        if not mems: return "🧠 Nothing stored yet."
        L.append("🧠 *Memories:*\n")
        for k, v in mems.items(): L.append(f"• *{k}*: {v}")
        return "\n".join(L)

    # 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  "
                f"(feels like {result.get('feels_c')}°C)\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:
        L.append(f"🔍 *Results for:* {result['query']}\n")
        for r in result["results"][:5]:
            L.append(f"• *{str(r.get('title',''))[:80]}*\n  {str(r.get('snippet',''))[:120]}")
            if r.get("url"): L.append(f"  {r['url']}")
        return "\n".join(L)

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

    # reminder
    if "scheduled" in result:
        return f"⏰ Reminder set for {result.get('minutes')} min: _{result.get('message')}_"

    # fs_write success
    if "bytes" in result and "path" in result:
        return f"📝 Written `{result['path']}` ({result['bytes']} bytes)"

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


# ── Bot ────────────────────────────────────────────────────────────────────────

class TelegramBot:
    def __init__(self, token: str, user_id: int, ai_engine):
        self._api       = TelegramAPI(token)
        self._user_id   = int(user_id)
        self._ai        = ai_engine
        self._safe_mode = False
        self._pending   = {}  # confirm_id → {tool, args, description}

    async def send_message(self, text: str):
        try: await self._api.send(self._user_id, _trunc(text))
        except Exception as e: logger.warning("send failed: %s", e)

    async def start(self):
        logger.info("Bot polling — user_id=%s", self._user_id)
        offset = 0
        while True:
            try:
                updates = await self._api.get_updates(offset=offset, timeout=30)
            except asyncio.CancelledError:
                raise
            except Exception as e:
                logger.warning("getUpdates error: %s — retry in 5s", e)
                await asyncio.sleep(5)
                continue
            for u in updates:
                offset = u["update_id"] + 1
                try: await self._route(u)
                except Exception as e: logger.exception("Update error: %s", e)

    async def _route(self, u: dict):
        if "callback_query" in u:
            await self._on_callback(u["callback_query"]); return
        msg = u.get("message") or u.get("edited_message")
        if not msg: return
        chat_id = msg["chat"]["id"]
        uid     = msg.get("from", {}).get("id")
        if uid != self._user_id:
            await self._api.send(chat_id, "⛔ Unauthorised."); return
        text = msg.get("text","").strip()
        if not text: return
        if text.startswith("/"):
            parts = text.split()
            cmd   = parts[0].lstrip("/").split("@")[0].lower()
            args  = parts[1:]
            await self._on_cmd(chat_id, cmd, args)
        else:
            await self._on_msg(chat_id, text)

    # ── Command dispatch ───────────────────────────────────────────────────────

    async def _on_cmd(self, chat_id: int, cmd: str, args: list):
        dispatch = {
            "start":      self._c_start,
            "help":       self._c_help,
            "health":     self._c_health,
            "run":        self._c_run,
            "export":     self._c_export,
            "tools":      self._c_tools,
            "vol":        self._c_vol,
            "screenshot": self._c_screenshot,
            "app":        self._c_app,
            "delete":     self._c_delete,
            "status":     self._c_status,
            "safe":       self._c_safe,
            "resume":     self._c_resume,
            "clear":      self._c_clear,
            "memory":     self._c_memory,
        }
        fn = dispatch.get(cmd)
        if fn:
            await fn(chat_id, args)
        else:
            await self._api.send(chat_id,
                f"❓ Unknown command `/{cmd}`\n\nType /help to see all commands.")

    # ── /start ─────────────────────────────────────────────────────────────────
    async def _c_start(self, c: int, a: list):
        await self._api.send(c,
            "👋 *Hey! I'm Doit — your AI computer assistant.*\n\n"
            "Just talk to me naturally, or use commands:\n\n"
            "  /health — system stats\n"
            "  /run ls -la — run any command\n"
            "  /screenshot — capture screen\n"
            "  /vol up — volume control\n"
            "  /app Chrome — open any app\n"
            "  /delete ~/old.txt — delete anything\n"
            "  /export — download task history\n"
            "  /tools — see all tools\n\n"
            "💡 _Or just say what you want in plain English!_")

    # ── /help ──────────────────────────────────────────────────────────────────
    async def _c_help(self, c: int, a: list):
        await self._api.send(c,
            "📖 *Doit Commands*\n\n"
            "`/health` — CPU, RAM, disk, battery\n"
            "`/run <cmd>` — run any shell command\n"
            "`/screenshot` — capture your screen\n"
            "`/vol up|down|<0-100>` — volume control\n"
            "`/app <name>` — open any application\n"
            "`/delete <path|pkg>` — delete file/folder/package\n"
            "`/export` — download full task history log\n"
            "`/tools` — list all AI-accessible tools\n"
            "`/memory` — show stored facts\n"
            "`/status` — agent status\n"
            "`/safe` / `/resume` — pause/resume execution\n"
            "`/clear` — reset conversation memory\n\n"
            "💬 _No command needed — just type what you want!_\n"
            "📝 _Examples: 'weather in Tokyo', 'remind me in 20 min', 'what's in Downloads'_")

    # ── /health ────────────────────────────────────────────────────────────────
    async def _c_health(self, c: int, a: list):
        await self._api.typing(c)
        from doit_fm.tools import run_tool
        result = await run_tool("sys_health", {})
        await self._api.send(c, _trunc(fmt(result)))

    # ── /run ───────────────────────────────────────────────────────────────────
    async def _c_run(self, c: int, a: list):
        if not a:
            await self._api.send(c,
                "Usage: `/run <command>`\n\nExamples:\n"
                "  `/run ls -la ~/Downloads`\n"
                "  `/run df -h`\n"
                "  `/run python3 --version`"); return
        if self._safe_mode:
            await self._api.send(c, "🔴 Safe mode is ON. Send /resume first."); return
        cmd = " ".join(a)
        await self._api.typing(c)
        from doit_fm.tools import run_tool
        result = await run_tool("shell_run", {"command": cmd})
        await self._api.send(c, _trunc(fmt(result, f"Run: `{cmd}`")))

    # ── /export ────────────────────────────────────────────────────────────────
    async def _c_export(self, c: int, a: list):
        await self._api.typing(c)
        from doit_fm.tools import run_tool
        result = await run_tool("export_log", {})
        if "error" in result:
            await self._api.send(c, fmt(result)); return
        # Try to send the file as a Telegram document
        path = result.get("path")
        if path:
            try:
                caption = (f"📋 Task log — {result.get('entries',0)} entries\n"
                           f"_Generated by doit-fm_")
                await self._api.send_document(c, path, caption)
                return
            except Exception as e:
                logger.warning("send_document failed: %s", e)
        # Fallback: send preview as text
        preview = result.get("preview", [])
        lines = [f"📋 *Task Log* — last {len(preview)} entries\n"]
        for e in preview:
            icon = "✓" if e.get("ok") else "✗"
            lines.append(f"[{e['ts']}] {icon} {e['tool']}")
        lines.append(f"\n_Full log: `{result.get('path','')}`_")
        await self._api.send(c, "\n".join(lines))

    # ── /tools ─────────────────────────────────────────────────────────────────
    async def _c_tools(self, c: int, a: list):
        from doit_fm.tools import TOOLS
        cats = {
            "🖥 System":  ["sys_health", "shell_run"],
            "🔊 Audio":   ["vol_get", "vol_set"],
            "📸 Desktop": ["screenshot", "app_open"],
            "🗑 Files":   ["delete_item", "fs_list", "fs_read", "fs_write"],
            "🧠 Memory":  ["memory_store", "memory_recall"],
            "🌐 Web":     ["web_search", "weather"],
            "📋 Utility": ["calc", "reminder_set", "export_log"],
        }
        lines = [f"🔧 *All {len(TOOLS)} tools:*\n"]
        for cat, names in cats.items():
            lines.append(f"\n{cat}")
            for name in names:
                meta = TOOLS.get(name)
                if meta:
                    danger = " ⚠️" if meta["danger"] else ""
                    lines.append(f"  `{name}` — {meta['desc']}{danger}")
        lines.append(f"\n💬 _Just talk naturally — I'll pick the right tool!_")
        text = "\n".join(lines)
        for i in range(0, len(text), 3800):
            await self._api.send(c, text[i:i+3800])

    # ── /vol ───────────────────────────────────────────────────────────────────
    async def _c_vol(self, c: int, a: list):
        from doit_fm.tools import run_tool
        if not a:
            result = await run_tool("vol_get", {})
            await self._api.send(c, fmt(result)); return
        arg = a[0].lower()
        if arg in ("up", "down"):
            step = int(a[1]) if len(a) > 1 and a[1].isdigit() else 10
            result = await run_tool("vol_set", {"direction": arg, "step": step})
        elif arg.isdigit():
            result = await run_tool("vol_set", {"level": int(arg)})
        else:
            await self._api.send(c,
                "Usage:\n  `/vol` — show current\n  `/vol up` — louder\n"
                "  `/vol down` — quieter\n  `/vol 75` — set to 75%"); return
        await self._api.send(c, fmt(result))

    # ── /screenshot ────────────────────────────────────────────────────────────
    async def _c_screenshot(self, c: int, a: list):
        if self._safe_mode:
            await self._api.send(c, "🔴 Safe mode is ON. Send /resume first."); return
        await self._api.typing(c)
        from doit_fm.tools import run_tool
        result = await run_tool("screenshot", {})
        if "error" in result:
            await self._api.send(c, fmt(result)); return
        path = result.get("path")
        # Send the actual image file if it's a PNG
        if path:
            try:
                import os
                url = f"{self._api._base}/sendPhoto"
                with open(path, "rb") as f:
                    img_data = f.read()
                boundary = "----doit_photo_boundary"
                body = (
                    f"--{boundary}\r\nContent-Disposition: form-data; name=\"chat_id\"\r\n\r\n{c}\r\n"
                    f"--{boundary}\r\nContent-Disposition: form-data; name=\"photo\"; filename=\"{os.path.basename(path)}\"\r\nContent-Type: image/png\r\n\r\n"
                ).encode() + img_data + f"\r\n--{boundary}--\r\n".encode()
                req = urllib.request.Request(url, data=body,
                      headers={"Content-Type": f"multipart/form-data; boundary={boundary}"})
                loop = asyncio.get_event_loop()
                await loop.run_in_executor(None, lambda: urllib.request.urlopen(req, timeout=30).read())
                return
            except Exception as e:
                logger.warning("sendPhoto failed: %s", e)
        await self._api.send(c, fmt(result))

    # ── /app ───────────────────────────────────────────────────────────────────
    async def _c_app(self, c: int, a: list):
        if not a:
            await self._api.send(c,
                "Usage: `/app <name>`\n\nExamples:\n"
                "  `/app Chrome`\n  `/app Spotify`\n  `/app VSCode`"); return
        if self._safe_mode:
            await self._api.send(c, "🔴 Safe mode is ON. Send /resume first."); return
        name = " ".join(a)
        await self._api.typing(c)
        from doit_fm.tools import run_tool
        result = await run_tool("app_open", {"name": name})
        await self._api.send(c, fmt(result))

    # ── /delete ────────────────────────────────────────────────────────────────
    async def _c_delete(self, c: int, a: list):
        if not a:
            await self._api.send(c,
                "Usage: `/delete <path or package>`\n\nExamples:\n"
                "  `/delete ~/Downloads/old.zip`\n"
                "  `/delete requests` _(pip package)_\n"
                "  `/delete ~/Documents/trash`"); return
        if self._safe_mode:
            await self._api.send(c, "🔴 Safe mode is ON. Send /resume first."); return
        target = " ".join(a)
        # Infer kind
        kind = "file"
        tl = target.lower()
        if tl.startswith("~/") or tl.startswith("/") or tl.startswith("."):
            kind = "file"
        elif any(c in target for c in ["/","\\",".txt",".pdf",".mp4",".jpg",".png",".zip",".tar"]):
            kind = "file"
        # Confirmation for destructive action
        import uuid
        cid = str(uuid.uuid4())[:8]
        self._pending[cid] = {"tool": "delete_item", "args": {"target": target, "kind": kind},
                               "description": f"Delete `{target}`"}
        kb = {"inline_keyboard": [[
            {"text": "🗑 Yes, delete it", "callback_data": f"confirm:{cid}"},
            {"text": "❌ Cancel",          "callback_data": f"cancel:{cid}"},
        ]]}
        await self._api.send(c,
            f"⚠️ *Confirm deletion:*\n\n`{target}`\n\nThis cannot be undone.",
            reply_markup=kb)

    # ── /status ────────────────────────────────────────────────────────────────
    async def _c_status(self, c: int, a: list):
        from doit_fm.tools import TOOLS
        mode   = "🔴 PAUSED" if self._safe_mode else "🟢 Active"
        ai_ok  = "✅ Online" if self._ai._available else "⚠️ Last call failed"
        turns  = len(self._ai._memory._turns) // 2
        await self._api.send(c,
            f"🤖 *Doit Status*\n\n"
            f"Agent: {mode}\n"
            f"AI: {ai_ok}  ({self._ai.provider} / {self._ai.model})\n"
            f"Tools: {len(TOOLS)} loaded\n"
            f"Context: {turns} turns")

    # ── /safe  /resume ─────────────────────────────────────────────────────────
    async def _c_safe(self, c: int, a: list):
        self._safe_mode = True
        await self._api.send(c,
            "🔴 *Safe mode ON* — I'll understand but won't execute anything.\nSend /resume when ready.")

    async def _c_resume(self, c: int, a: list):
        self._safe_mode = False
        await self._api.send(c, "🟢 *Ready!* What would you like me to do?")

    # ── /clear ─────────────────────────────────────────────────────────────────
    async def _c_clear(self, c: int, a: list):
        self._ai.clear_memory()
        await self._api.send(c, "🗑 Conversation cleared. Fresh start! 😊")

    # ── /memory ────────────────────────────────────────────────────────────────
    async def _c_memory(self, c: int, a: list):
        from doit_fm.tools import run_tool
        result = await run_tool("memory_recall", {})
        await self._api.send(c, _trunc(fmt(result, "Stored memories")))

    # ── Free text ──────────────────────────────────────────────────────────────
    async def _on_msg(self, chat_id: int, text: str):
        if self._safe_mode:
            await self._api.send(chat_id,
                "🔴 Safe mode is ON — I understand you but won't act.\nSend /resume to continue.")
            return
        await self._api.typing(chat_id)
        try:
            action = await self._ai.parse_intent(text)
        except Exception as e:
            logger.error("parse_intent error: %s", e)
            await self._api.send(chat_id,
                "😕 Couldn't reach the AI right now. Check your internet connection."); return
        await self._dispatch(chat_id, action)

    async def _dispatch(self, chat_id: int, action: dict):
        tool    = action.get("tool", "")
        args    = action.get("args") or {}
        desc    = action.get("description", "")
        confirm = action.get("confirm", False)

        if tool == "clarify":
            await self._api.send(chat_id,
                f"🤔 {(args or {}).get('message','Could you give me a bit more detail?')}"); return
        if tool == "error":
            await self._api.send(chat_id,
                f"⚠️ {(args or {}).get('message','Something went wrong.')}"); return

        if confirm:
            import uuid
            cid = str(uuid.uuid4())[:8]
            self._pending[cid] = {"tool": tool, "args": args, "description": desc}
            kb = {"inline_keyboard": [[
                {"text": "✅ Yes, do it", "callback_data": f"confirm:{cid}"},
                {"text": "❌ Cancel",      "callback_data": f"cancel:{cid}"},
            ]]}
            await self._api.send(chat_id,
                f"⚠️ *Confirm:* {desc}\n\nThis cannot be undone.", reply_markup=kb)
            return

        await self._api.typing(chat_id)
        await self._exec_reply(chat_id, tool, args, desc)

    async def _exec_reply(self, chat_id: int, tool: str, args: dict, desc: str):
        from doit_fm.tools import run_tool
        try:
            result = await run_tool(tool, args)
        except Exception as e:
            result = {"error": str(e)}
        await self._api.send(chat_id, _trunc(fmt(result, desc)))

    # ── Inline button callbacks ────────────────────────────────────────────────
    async def _on_callback(self, q: dict):
        await self._api.answer_cb(q["id"])
        uid     = q.get("from", {}).get("id")
        chat_id = q["message"]["chat"]["id"]
        msg_id  = q["message"]["message_id"]
        data    = q.get("data","")
        if uid != self._user_id: return

        if data.startswith("confirm:"):
            cid = data[8:]
            p   = self._pending.pop(cid, None)
            if not p:
                await self._api.edit(chat_id, msg_id, "⚠️ Expired — please try again."); return
            from doit_fm.tools import run_tool
            result = await run_tool(p["tool"], p["args"])
            await self._api.edit(chat_id, msg_id, _trunc(fmt(result, p["description"])))
        elif data.startswith("cancel:"):
            self._pending.pop(data[7:], None)
            await self._api.edit(chat_id, msg_id, "❌ Cancelled. Nothing was changed.")


# ── CLI helpers ────────────────────────────────────────────────────────────────

def _http_post(url: str, payload: dict, timeout: int = 15) -> dict:
    data = json.dumps(payload).encode()
    req  = urllib.request.Request(url, data=data,
               headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return json.loads(r.read())

def _http_get(url: str, timeout: int = 10) -> dict:
    req = urllib.request.Request(url, headers={"User-Agent": "doit-fm/3.1"})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return json.loads(r.read())

async def validate_bot_token(token: str) -> tuple[bool, str]:
    loop = asyncio.get_event_loop()
    try:
        data = await loop.run_in_executor(
            None, lambda: _http_get(f"https://api.telegram.org/bot{token}/getMe"))
        if data.get("ok"):
            bot = data["result"]
            return True, f"@{bot['username']} ({bot['first_name']})"
        return False, data.get("description","Unknown")
    except Exception as e:
        return False, str(e)

async def send_test_message(token: str, user_id: int) -> tuple[bool, str]:
    loop = asyncio.get_event_loop()
    try:
        data = await loop.run_in_executor(None, lambda: _http_post(
            f"https://api.telegram.org/bot{token}/sendMessage",
            {"chat_id": user_id,
             "text": "👋 doit-fm is connecting — setup almost done!"},
        ))
        if data.get("ok"): return True, "sent"
        return False, data.get("description","Unknown")
    except Exception as e:
        return False, str(e)
