"""
bot.py — Telegram long-polling bot for BySalim v2.

New in v2:
  • send_photo / send_audio / send_video / send_document routing
  • register_send_file_callback wired up
  • All formatter cases updated for new tools
  • Bug fixes: callback_query auth, multipart boundary, message splitting
"""
from __future__ import annotations

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

from .ai import AIEngine
from .config import (
    MAX_RETRIES,
    RETRY_BASE_DELAY,
    RETRY_MAX_DELAY,
    TELEGRAM_CAPTION_MAX_LENGTH,
    TELEGRAM_MAX_MESSAGE_LENGTH,
)
from .logger import get_logger
from .tools import TOOLS, run_tool

log = get_logger("bot")


# ---------------------------------------------------------------------------
# TelegramAPI
# ---------------------------------------------------------------------------

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

    def __init__(self, token: str) -> None:
        if not token or not token.strip():
            raise ValueError("Telegram bot token cannot be empty")
        self._token = token.strip()

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

    def _post_sync(self, method: str, payload: dict, timeout: float = 35.0) -> dict:
        url = self._url(method)
        body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
        req = urllib.request.Request(url, data=body,
                                     headers={"Content-Type": "application/json; charset=utf-8"}, method="POST")
        try:
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                return json.loads(resp.read())
        except urllib.error.HTTPError as exc:
            raw = exc.read()
            try:
                return json.loads(raw)
            except Exception:
                return {"ok": False, "description": raw.decode(errors="replace")[:400]}
        except Exception as exc:
            return {"ok": False, "description": str(exc)[:400]}

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

    async def _post_with_retry(self, method: str, payload: dict) -> dict:
        last: dict = {"ok": False, "description": "Not attempted"}
        for attempt in range(MAX_RETRIES):
            result = await self._post(method, payload)
            if result.get("ok"):
                return result
            code = result.get("error_code", 0)
            desc = result.get("description", "")
            if code in (400, 401, 403, 404):
                log.warning("Telegram %s permanent error %d: %s", method, code, desc)
                return result
            retry_after = result.get("parameters", {}).get("retry_after", 0)
            delay = retry_after if retry_after > 0 else min(RETRY_BASE_DELAY * (2 ** attempt), RETRY_MAX_DELAY)
            log.warning("Telegram %s error (attempt %d/%d): %s — retry %.1fs", method, attempt + 1, MAX_RETRIES, desc, delay)
            await asyncio.sleep(delay)
            last = result
        return last

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

    async def send_message(self, chat_id: int, text: str, parse_mode: str = "Markdown",
                            reply_markup: dict | None = None, disable_web_page_preview: bool = True) -> dict:
        chunks = _split_message(text, TELEGRAM_MAX_MESSAGE_LENGTH)
        last: dict = {}
        for i, chunk in enumerate(chunks):
            payload: dict[str, Any] = {"chat_id": chat_id, "text": chunk,
                                        "parse_mode": parse_mode, "disable_web_page_preview": disable_web_page_preview}
            if reply_markup and i == 0:
                payload["reply_markup"] = reply_markup
            result = await self._post_with_retry("sendMessage", payload)
            if not result.get("ok"):
                plain: dict[str, Any] = {"chat_id": chat_id, "text": _strip_markdown(chunk)}
                if reply_markup and i == 0:
                    plain["reply_markup"] = reply_markup
                result = await self._post_with_retry("sendMessage", plain)
            last = result
        return last

    def _multipart_upload(self, chat_id: int, file_path: str, caption: str,
                           field_name: str, method: str) -> dict:
        """RFC 2046 multipart/form-data upload."""
        import mimetypes
        url = self._url(method)
        boundary = f"bysalim_{secrets.token_hex(12)}"
        CRLF = b"\r\n"
        try:
            with open(file_path, "rb") as f:
                file_bytes = f.read()
        except OSError as exc:
            return {"ok": False, "description": f"Cannot read file: {exc}"}
        filename = os.path.basename(file_path)
        mime = mimetypes.guess_type(file_path)[0] or "application/octet-stream"

        def text_field(name: str, value: str) -> bytes:
            return (f"--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"\r\n\r\n{value}").encode("utf-8")

        def file_field(name: str, fname: str, content: bytes, content_type: str) -> bytes:
            header = (f"--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"; filename=\"{fname}\"\r\nContent-Type: {content_type}\r\n\r\n").encode("utf-8")
            return header + content

        parts: list[bytes] = [text_field("chat_id", str(chat_id)), file_field(field_name, filename, file_bytes, mime)]
        if caption:
            parts.append(text_field("caption", caption[:TELEGRAM_CAPTION_MAX_LENGTH]))
        body = CRLF.join(parts) + CRLF + f"--{boundary}--".encode("utf-8") + CRLF
        headers = {"Content-Type": f"multipart/form-data; boundary={boundary}"}
        req = urllib.request.Request(url, data=body, headers=headers, method="POST")
        try:
            with urllib.request.urlopen(req, timeout=120) as resp:
                return json.loads(resp.read())
        except urllib.error.HTTPError as exc:
            raw = exc.read()
            try:
                return json.loads(raw)
            except Exception:
                return {"ok": False, "description": raw.decode(errors="replace")[:400]}
        except Exception as exc:
            return {"ok": False, "description": str(exc)[:400]}

    async def send_photo(self, chat_id: int, photo_path: str, caption: str = "") -> dict:
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, lambda: self._multipart_upload(chat_id, photo_path, caption, "photo", "sendPhoto"))

    async def send_audio(self, chat_id: int, audio_path: str, caption: str = "") -> dict:
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, lambda: self._multipart_upload(chat_id, audio_path, caption, "audio", "sendAudio"))

    async def send_video(self, chat_id: int, video_path: str, caption: str = "") -> dict:
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, lambda: self._multipart_upload(chat_id, video_path, caption, "video", "sendVideo"))

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

    async def send_file_auto(self, chat_id: int, path: str, caption: str, file_type: str) -> dict:
        """Route file to the correct Telegram send method based on file_type."""
        if file_type == "photo" or file_type == "image":
            return await self.send_photo(chat_id, path, caption)
        elif file_type == "audio":
            return await self.send_audio(chat_id, path, caption)
        elif file_type == "video":
            return await self.send_video(chat_id, path, caption)
        else:
            return await self.send_document(chat_id, path, caption)

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

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

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

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

    async def delete_webhook(self) -> dict:
        return await self._post_with_retry("deleteWebhook", {"drop_pending_updates": False})


# ---------------------------------------------------------------------------
# Text helpers
# ---------------------------------------------------------------------------

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


def _split_message(text: str, max_len: int = TELEGRAM_MAX_MESSAGE_LENGTH) -> list[str]:
    if len(text) <= max_len:
        return [text]
    chunks: list[str] = []
    while text:
        if len(text) <= max_len:
            chunks.append(text)
            break
        split_at = text.rfind("\n", 0, max_len)
        if split_at <= 0:
            split_at = max_len
        chunks.append(text[:split_at])
        text = text[split_at:].lstrip("\n")
    return [c for c in chunks if c.strip()]


def _bar(pct: float, width: int = 10) -> str:
    pct = max(0.0, min(100.0, float(pct)))
    filled = round(pct / 100 * width)
    return "█" * filled + "░" * (width - filled)


# ---------------------------------------------------------------------------
# Formatter
# ---------------------------------------------------------------------------

def fmt(result: dict) -> str:
    if not isinstance(result, dict):
        return f"⚠️ Unexpected result: {type(result).__name__}"
    if "error" in result:
        return f"❌ *Error:* {str(result['error'])[:1500]}"
    if "clarify" in result:
        return f"🤔 {result['clarify']}"

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

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

    if "percent" in result and "plugged_in" in result:
        pct = result["percent"]
        plug = "🔌 Charging" if result["plugged_in"] else "🔋 On battery"
        return f"{'🪫' if pct < 20 else '🔋'} *Battery:* {_bar(pct)} {pct}%\n{plug}\n⏳ {result['time_left']}"

    if "exit_code" in result:
        code = result["exit_code"]
        icon = "✅" if code == 0 else "⚠️"
        out = str(result.get("stdout", ""))
        err = str(result.get("stderr", ""))
        parts = [f"{icon} *Exit code:* `{code}`"]
        if out:
            out = out[:3000] + ("\n[…truncated]" if len(out) > 3000 else "")
            parts.append(f"```\n{out}\n```")
        if err:
            parts.append(f"⚠️ *stderr:*\n```\n{err[:800]}\n```")
        if not out and not err:
            parts.append("_(no output)_")
        return "\n".join(parts)

    if set(result.keys()) <= {"volume"}:
        v = result["volume"]
        return f"{'🔇' if v == 0 else '🔊'} *Volume:* {_bar(v)} {v}%"
    if "volume_set" in result:
        v = result["volume_set"]
        return f"🔊 *Volume set to:* {_bar(v)} {v}%"
    if "muted" in result:
        return "🔇 *Audio muted*"

    # Screenshot
    if "path" in result and "size_kb" in result and "size_bytes" in result:
        return f"📸 Screenshot — {result['size_kb']} KB"

    # File sent
    if "sent" in result:
        icons = {"image": "🖼️", "audio": "🎵", "video": "🎬", "document": "📄"}
        icon = icons.get(result.get("media_type", "document"), "📎")
        return f"{icon} *Sent:* `{result['sent']}`\n💾 {result.get('size_human', '')}"

    # Image info
    if "megapixels" in result:
        lines = [f"🖼️ *Image Info*", f"📐 {result['width']}×{result['height']}  ({result['megapixels']} MP)",
                 f"🗂️ Format: {result.get('format', '?')} | Mode: {result.get('mode', '?')}",
                 f"💾 {result.get('size_human', '')}"]
        if "exif" in result:
            exif = result["exif"]
            for key in ("Make", "Model", "DateTimeOriginal", "GPSLatitude"):
                if key in exif:
                    lines.append(f"📷 {key}: {exif[key]}")
        return "\n".join(lines)

    # Image operations
    if "resized" in result:
        return (f"↔️ *Resized*: `{result['resized']}`\n"
                f"  {result.get('orig_width')}×{result.get('orig_height')} → {result.get('new_width')}×{result.get('new_height')}")
    if "rotated" in result:
        return f"🔄 *Rotated {result.get('angle')}°*: `{result['rotated']}`\n  {result.get('width')}×{result.get('height')}"
    if "cropped" in result:
        return f"✂️ *Cropped*: `{result['cropped']}`\n  {result.get('width')}×{result.get('height')}"
    if "converted" in result and "format" in result:
        return f"🔀 *Converted*: `{result['converted']}`\n  Format: {result['format']} | {result.get('size_human', '')}"

    # Audio info
    if "duration_seconds" in result and "bitrate_kbps" in result:
        lines = [f"🎵 *Audio Info*", f"⏱️ Duration: {result.get('duration', '?')}",
                 f"🎚️ Bitrate: {result.get('bitrate_kbps', '?')} kbps",
                 f"📻 {result.get('sample_rate_hz', '?')} Hz | {result.get('channels', '?')} ch",
                 f"💾 {result.get('size_human', '')}"]
        for tag in ("title", "artist", "album", "date"):
            if tag in result:
                lines.append(f"🏷️ {tag.capitalize()}: {result[tag]}")
        return "\n".join(lines)

    if "trimmed" in result:
        return f"✂️ *Trimmed*: `{result['trimmed']}`\n  From {result.get('start_sec')}s, {result.get('duration_sec')}s long"

    # Video info
    if "resolution" in result and "video_codec" in result:
        lines = [f"🎬 *Video Info*",
                 f"📐 {result.get('resolution', '?')} @ {result.get('fps', '?')} fps",
                 f"🎥 Video: {result.get('video_codec', '?')} | Audio: {result.get('audio_codec', '?')}",
                 f"⏱️ Duration: {result.get('duration', '?')}",
                 f"🔈 Bitrate: {result.get('bit_rate_kbps', '?')} kbps",
                 f"💾 {result.get('size_human', '')}"]
        return "\n".join(lines)

    if "thumbnail" in result:
        return f"🖼️ *Thumbnail extracted*: `{result['thumbnail']}`\n  At {result.get('at_sec')}s | {result.get('size_human', '')}"

    # Directory listing
    if "items" in result and "path" in result:
        items = result["items"]
        count = result.get("count", len(items))
        lines = [f"📁 *{result['path']}* ({count} items)\n"]
        media_icons = {"image": "🖼️", "audio": "🎵", "video": "🎬", "document": "📄"}
        for item in items[:40]:
            if item.get("type") == "dir":
                icon = "📂"
            else:
                icon = media_icons.get(item.get("media_type"), "📄")
            size = f"  {item['size_human']}" if item.get("size_human") else ""
            mod = f"  {item['modified']}" if item.get("modified") else ""
            lines.append(f"{icon} `{item['name']}`{size}{mod}")
        if count > 40:
            lines.append(f"…and {count - 40} more")
        return "\n".join(lines)

    if "content" in result and "lines" in result:
        content = str(result["content"])
        trunc = " *(truncated)*" if result.get("truncated") else ""
        body = content[:3500] + ("\n[…]" if len(content) > 3500 else "")
        return f"📄 *{result.get('path', 'file')}*{trunc} — {result['lines']} lines\n```\n{body}\n```"

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

    if "deleted" in result:
        method = result.get("method", "")
        icon = "🗑️" if result.get("type") == "file" else "🗂️"
        trash_note = " (in Trash — recoverable)" if method == "trash" else " (permanently deleted)"
        return f"{icon} *Deleted*: `{result['deleted']}`{trash_note}"

    if "renamed" in result:
        return f"✏️ *Renamed*: `{result['renamed']}`\n  → `{result['to']}`"
    if "copied" in result:
        return f"📋 *Copied*\n  From: `{result['copied']}`\n  To: `{result['to']}`"
    if "moved" in result:
        return f"🚚 *Moved*\n  From: `{result['moved']}`\n  To: `{result['to']}`"
    if "created" in result:
        return f"📁 *Directory created*: `{result['created']}`"

    if "matches" in result and "root" in result:
        matches = result["matches"]
        count = result["count"]
        media_icons = {"image": "🖼️", "audio": "🎵", "video": "🎬", "document": "📄"}
        lines = [f"🔍 *Found {count} matches* in `{result['root']}`\n"]
        for m in matches[:25]:
            icon = "📂" if m.get("type") == "dir" else media_icons.get(m.get("media_type"), "📄")
            size = f"  {_human_size(m['size_bytes'])}" if m.get("size_bytes") else ""
            lines.append(f"{icon} `{m['path']}`{size}")
        if count > 25:
            lines.append(f"…and {count - 25} more")
        return "\n".join(lines)

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

    if "host" in result and "packets" in result:
        avg = result.get("avg_ms")
        reach = "✅ Reachable" if result.get("reachable") else "❌ Unreachable"
        return (f"🏓 *Ping {result['host']}*\n"
                f"Packets: {result['packets']}  |  Loss: {result.get('packet_loss', '?')}\n"
                f"Avg: {f'{avg} ms' if avg else 'N/A'}\n{reach}")

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

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

    if "local" in result and "date" in result:
        tz = result.get("timezone", "?")
        off = result.get("utc_offset_hours", 0)
        return (f"🕐 *Date & Time*\n📅 {result['date']}\n⏰ {result['time']}  ({tz}  UTC{'+' if off >= 0 else ''}{off})\n"
                f"🌍 UTC: {result['utc']}\n🗓️ Week {result.get('week_number')}  ·  Day {result.get('day_of_year')}")

    if "reminder_set" in result:
        return (f"⏰ *Reminder set!*\n💬 \"{result['reminder_set']}\"\n"
                f"⏱️ Fires at: {result['fires_at']}  ({result.get('delay_human', '?')} from now)")

    if "clipboard" in result:
        text = str(result["clipboard"])
        display = text[:1000] + ("…" if len(text) > 1000 else "")
        return f"📋 *Clipboard* ({result.get('length', len(text))} chars):\n```\n{display}\n```"
    if "clipboard_set" in result:
        return f"📋 *Copied to clipboard:*\n`{result['clipboard_set']}`"

    if "opened" in result:
        return f"🌐 *Opened:* {result['opened']}"
    if "launched" in result:
        return f"🚀 *Launched:* {result['launched']}" + (f"\n  via {result['via']}" if result.get("via") else "")
    if "opened_file" in result:
        return f"📂 *Opened with default app:* `{result['opened_file']}`"

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

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

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

    if "python_version" in result:
        ver = result["python_version"].split()[0]
        return (f"🐍 *Python {ver}*\n📍 `{result.get('executable', '?')}`\n"
                f"📦 {result.get('packages_installed', '?')} packages")

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

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

    if "words" in result and "characters" in result:
        return (f"📝 *Word Count*\n  Words: {result['words']}\n  Chars: {result['characters']}\n"
                f"  Lines: {result['lines']}  ·  Sentences: {result.get('sentences', '?')}")

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

    if "env" in result:
        env = result["env"]
        count = result.get("count", len(env))
        lines = [f"🌿 *Environment Variables ({count}):*"]
        for k, v in list(env.items())[:25]:
            lines.append(f"  `{k}` = `{str(v)[:100]}`")
        return "\n".join(lines)

    # Generic fallback
    lines = ["📊 *Result:*"]
    for k, v in result.items():
        lines.append(f"  *{k}:* `{str(v)[:300]}`")
    return "\n".join(lines)


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


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

class TelegramBot:
    def __init__(self, token: str, user_id: int, engine: AIEngine) -> None:
        self._api = TelegramAPI(token)
        self._user_id = int(user_id)
        self._chat_id: int = int(user_id)
        self._engine = engine
        self._offset: int = 0
        self._running: bool = False
        self._pending: dict[str, dict] = {}

        self._commands: dict[str, Any] = {
            "start":       self._cmd_start,
            "help":        self._cmd_help,
            "health":      self._cmd_health,
            "status":      self._cmd_status,
            "time":        self._cmd_time,
            "screenshot":  self._cmd_screenshot,
            "weather":     self._cmd_weather,
            "ls":          self._cmd_ls,
            "run":         self._cmd_run,
            "ps":          self._cmd_ps,
            "battery":     self._cmd_battery,
            "ping":        self._cmd_ping,
            "memory":      self._cmd_memory,
            "disk":        self._cmd_disk,
            "clear":       self._cmd_clear_memory,
            "ip":          self._cmd_ip,
            "uptime":      self._cmd_uptime,
            "volume":      self._cmd_volume,
            "internet":    self._cmd_internet,
        }

    async def start(self) -> None:
        self._running = True
        await self._api.delete_webhook()
        me = await self._api.get_me()
        bot_name = me.get("result", {}).get("username", "unknown")
        log.info("Bot @%s started, polling…", bot_name)
        print(f"[bysalim] Bot @{bot_name} started. Waiting for messages…")
        consecutive_errors = 0
        while self._running:
            try:
                updates = await self._api.get_updates(offset=self._offset, timeout=30)
                consecutive_errors = 0
                for update in updates:
                    self._offset = update["update_id"] + 1
                    try:
                        await self._route(update)
                    except Exception:
                        log.exception("Error routing update %s", update.get("update_id"))
            except Exception as exc:
                consecutive_errors += 1
                delay = min(3 * consecutive_errors, 60)
                log.warning("Polling error #%d: %s — wait %.0fs", consecutive_errors, exc, delay)
                await asyncio.sleep(delay)

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

    async def send_message(self, text: str) -> None:
        await self._api.send_message(self._chat_id, text)

    async def send_file(self, path: str, caption: str, file_type: str) -> None:
        """Called by register_send_file_callback."""
        await self._api.send_file_auto(self._chat_id, path, caption, file_type)

    async def _route(self, update: dict) -> None:
        if "callback_query" in update:
            await self._handle_callback(update["callback_query"])
            return
        message = update.get("message", {})
        if not message:
            return
        sender_id = message.get("from", {}).get("id")
        if sender_id != self._user_id:
            log.warning("Ignoring message from unauthorized user %s", sender_id)
            return
        text = message.get("text", "").strip()
        if not text:
            return
        self._chat_id = message["chat"]["id"]
        if text.startswith("/"):
            await self._handle_command(text, message)
        else:
            await self._handle_freetext(text, message)

    async def _handle_callback(self, cq: dict) -> None:
        # Verify the callback is from our user
        user_id = cq.get("from", {}).get("id")
        if user_id != self._user_id:
            return
        await self._api.answer_callback_query(cq["id"])
        data = cq.get("data", "")
        chat_id = cq["message"]["chat"]["id"]
        msg_id = cq["message"]["message_id"]

        if data.startswith("confirm:"):
            key = data[len("confirm:"):]
            pending = self._pending.pop(key, None)
            if not pending:
                await self._api.edit_message_text(chat_id, msg_id, "⚠️ This confirmation expired.")
                return
            tool_name = pending["tool"]
            args = pending["args"]
            if tool_name == "screenshot":
                await self._api.send_chat_action(chat_id, "upload_photo")
                await self._api.edit_message_text(chat_id, msg_id, "📸 Taking screenshot…")
            else:
                await self._api.send_chat_action(chat_id, "typing")
                await self._api.edit_message_text(chat_id, msg_id, "⏳ Running…")
            result = await run_tool(tool_name, args)
            await self._deliver_result(chat_id, tool_name, result, pending.get("description", ""), msg_id_to_edit=msg_id)

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

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

    async def _handle_freetext(self, text: str, message: dict) -> None:
        await self._api.send_chat_action(self._chat_id, "typing")
        action = await self._engine.parse_intent(text)
        tool_name = action.get("tool", "clarify")
        args = action.get("args", {})
        description = action.get("description", "")
        needs_confirm = action.get("confirm", False)
        if TOOLS.get(tool_name, {}).get("danger", False):
            needs_confirm = True
        if needs_confirm:
            await self._send_confirmation(tool_name, args, description)
            return
        if tool_name == "screenshot":
            await self._api.send_chat_action(self._chat_id, "upload_photo")
        else:
            await self._api.send_chat_action(self._chat_id, "typing")
        result = await run_tool(tool_name, args)
        await self._deliver_result(self._chat_id, tool_name, result, description)

    async def _deliver_result(self, chat_id: int, tool_name: str, result: dict,
                               description: str = "", msg_id_to_edit: int | None = None) -> None:
        """Send result to user, handling file-sending tools specially."""
        # Screenshot → photo
        if tool_name == "screenshot" and "path" in result and "error" not in result:
            photo_result = await self._api.send_photo(
                chat_id, result["path"],
                caption=f"📸 {description or 'Screenshot'} — {result.get('size_kb', '?')} KB"
            )
            if msg_id_to_edit:
                edit_text = "✅ Screenshot sent" if photo_result.get("ok") else f"❌ Photo send failed: {photo_result.get('description', '?')}"
                await self._api.edit_message_text(chat_id, msg_id_to_edit, edit_text)
            elif not photo_result.get("ok"):
                await self._api.send_message(chat_id, f"❌ Screenshot taken but send failed: {photo_result.get('description', '?')}")
            return

        # send_file tool → auto-detect type
        if tool_name == "send_file" and "sent" in result and "error" not in result:
            from .tools import _guess_media_type
            path = result["sent"]
            media_type = result.get("media_type", _guess_media_type(path))
            cap = description or f"📎 {os.path.basename(path)}"
            file_result = await self._api.send_file_auto(chat_id, path, cap, media_type)
            response_text = "✅ File sent!" if file_result.get("ok") else f"❌ Send failed: {file_result.get('description', '?')}"
            if msg_id_to_edit:
                await self._api.edit_message_text(chat_id, msg_id_to_edit, response_text)
            else:
                await self._api.send_message(chat_id, response_text)
            return

        # Video thumbnail → send as photo
        if tool_name == "video_thumbnail" and "thumbnail" in result and "error" not in result:
            await self._api.send_photo(chat_id, result["thumbnail"],
                                        caption=f"🎬 Thumbnail at {result.get('at_sec')}s")
            return

        # Generic text response
        text = fmt(result)
        if msg_id_to_edit:
            await self._api.edit_message_text(chat_id, msg_id_to_edit, text)
        else:
            await self._api.send_message(chat_id, text)

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

    async def _run_and_send(self, tool: str, args: dict, description: str = "") -> None:
        if tool == "screenshot":
            await self._api.send_chat_action(self._chat_id, "upload_photo")
        else:
            await self._api.send_chat_action(self._chat_id, "typing")
        result = await run_tool(tool, args)
        await self._deliver_result(self._chat_id, tool, result, description)

    # Slash commands
    async def _cmd_start(self, args: str, message: dict) -> None:
        await self._api.send_message(self._chat_id,
            "👋 *BySalim v2 is running!*\n\n"
            "Send me anything in natural language.\n\n"
            "*Examples:*\n"
            "  `take a screenshot`\n"
            "  `show me info about photo.jpg`\n"
            "  `resize ~/Downloads/pic.png to 800px wide`\n"
            "  `get info on song.mp3`\n"
            "  `what's the video duration of movie.mp4`\n"
            "  `extract thumbnail from video.mp4 at 5 seconds`\n"
            "  `open VLC`\n"
            "  `open ~/Documents/report.pdf`\n"
            "  `send me the file ~/Downloads/photo.jpg`\n"
            "  `weather in Dubai`\n"
            "  `remind me in 30 minutes to check email`\n\n"
            "Type /help to see all commands.")

    async def _cmd_help(self, args: str, message: dict) -> None:
        cmd_lines = "\n".join(f"  /{c}" for c in sorted(self._commands))
        await self._api.send_message(self._chat_id,
            f"*BySalim v2 Commands:*\n\n{cmd_lines}\n\n"
            "📁 *File & Media:*\n"
            "  `show info for photo.jpg`\n"
            "  `resize ~/pic.jpg to 1920x1080`\n"
            "  `rotate ~/pic.jpg 90 degrees`\n"
            "  `convert ~/pic.jpg to webp`\n"
            "  `get audio info ~/music.mp3`\n"
            "  `convert ~/audio.wav to mp3`\n"
            "  `get video info ~/movie.mp4`\n"
            "  `extract thumbnail from ~/video.mp4 at 10s`\n"
            "  `send ~/Downloads/file.pdf`\n"
            "  `open VLC / open Spotify / open Terminal`\n"
            "  `open ~/Documents/report.pdf`")

    async def _cmd_health(self, args: str, message: dict) -> None:
        await self._run_and_send("system_health", {})
    async def _cmd_status(self, args: str, message: dict) -> None:
        await self._run_and_send("system_health", {})
    async def _cmd_time(self, args: str, message: dict) -> None:
        await self._run_and_send("datetime", {})
    async def _cmd_screenshot(self, args: str, message: dict) -> None:
        await self._run_and_send("screenshot", {})
    async def _cmd_weather(self, args: str, message: dict) -> None:
        if not args.strip():
            await self._api.send_message(self._chat_id, "❓ Usage: /weather <city>")
            return
        await self._run_and_send("weather", {"city": args.strip()})
    async def _cmd_ls(self, args: str, message: dict) -> None:
        await self._run_and_send("list_directory", {"path": args.strip() or "~"})
    async def _cmd_run(self, args: str, message: dict) -> None:
        if not args.strip():
            await self._api.send_message(self._chat_id, "❓ Usage: /run <shell command>")
            return
        key = secrets.token_hex(8)
        self._pending[key] = {"tool": "run_shell", "args": {"command": args.strip()}, "description": f"Run: {args.strip()[:80]}"}
        markup = {"inline_keyboard": [[{"text": "✅ Run it", "callback_data": f"confirm:{key}"},
                                        {"text": "❌ Cancel", "callback_data": f"cancel:{key}"}]]}
        await self._api.send_message(self._chat_id, f"⚠️ *Run shell command?*\n```\n{args.strip()[:500]}\n```", reply_markup=markup)
    async def _cmd_ps(self, args: str, message: dict) -> None:
        await self._run_and_send("list_processes", {})
    async def _cmd_battery(self, args: str, message: dict) -> None:
        await self._run_and_send("battery", {})
    async def _cmd_ping(self, args: str, message: dict) -> None:
        await self._run_and_send("ping", {"host": args.strip() or "8.8.8.8"})
    async def _cmd_memory(self, args: str, message: dict) -> None:
        await self._run_and_send("system_health", {})
    async def _cmd_disk(self, args: str, message: dict) -> None:
        await self._run_and_send("disk_usage", {"path": args.strip() or "/"})
    async def _cmd_clear_memory(self, args: str, message: dict) -> None:
        await self._engine.memory.clear()
        await self._api.send_message(self._chat_id, "🧹 *Conversation memory cleared.*")
    async def _cmd_ip(self, args: str, message: dict) -> None:
        await self._run_and_send("network_stats", {})
    async def _cmd_uptime(self, args: str, message: dict) -> None:
        await self._run_and_send("uptime", {})
    async def _cmd_volume(self, args: str, message: dict) -> None:
        if args.strip():
            try:
                await self._run_and_send("set_volume", {"level": int(args.strip())})
            except ValueError:
                await self._api.send_message(self._chat_id, "❓ Usage: /volume [0-100]")
        else:
            await self._run_and_send("get_volume", {})
    async def _cmd_internet(self, args: str, message: dict) -> None:
        await self._run_and_send("internet_speed", {})
