"""
doit-fm Telegram bot.
Uses ONLY stdlib urllib — zero external Telegram dependencies.
Architecture: getUpdates long-poll -> 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 helper ──────────────────────────────────────────────────────

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


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

    def _post(self, method, payload=None, timeout=35):
        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))

    def _get(self, url, timeout=10):
        req = urllib.request.Request(url, headers={"User-Agent": "doit-fm/3.0"})
        with urllib.request.urlopen(req, timeout=timeout) as r:
            return json.loads(r.read())

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

    async def get_updates(self, offset=0, timeout=30):
        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, text, parse_mode="Markdown", reply_markup=None):
        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 typing(self, chat_id):
        try: await self._async(self._post, "sendChatAction", {"chat_id": chat_id, "action": "typing"})
        except Exception: pass

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

    async def edit(self, chat_id, msg_id, text, parse_mode="Markdown"):
        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):
    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, w=10):
    f = max(0, min(w, round(float(pct)/100*w)))
    return "█"*f + "░"*(w-f)

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


def fmt(result: Any, desc: str = "") -> str:
    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}*")

    # Directory listing
    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)

    # File content
    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)

    # System monitor
    if "cpu_percent" in result and "ram_percent" in result:
        cpu=result["cpu_percent"]; ram=result["ram_percent"]; disk=result.get("disk_percent",0)
        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")

    # System info
    if "os" in result and "cpu_cores" in result:
        return (f"🖥 *System Info*\n\n"
                f"OS: {result.get('os')}\n"
                f"CPU: {result.get('cpu_cores')} cores / {result.get('cpu_threads')} threads\n"
                f"RAM: {result.get('ram_used_gb')}/{result.get('ram_total_gb')} GB ({result.get('ram_percent')}%)\n"
                f"Disk: {result.get('disk_used_gb')}/{result.get('disk_total_gb')} GB ({result.get('disk_percent')}%)\n"
                f"Online since: {result.get('uptime_since','?')}\nUser: {result.get('user','?')}")

    # Processes
    if "processes" in result:
        L.append(f"⚙️ Top processes ({result.get('total_visible','?')} total)\n")
        for p in result["processes"][:15]:
            L.append(f"  `{str(p.get('pid','?')):>6}` {str(p.get('name','?')):<22} {p.get('mem_mb',0):.0f} MB")
        return "\n".join(L)

    # 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[:500]}\n```"
        return s

    # 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 (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)

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

    # Memory
    if "memories" in result:
        mems=result["memories"]
        if not mems: return "🧠 Nothing stored yet. Say 'remember that my name is Alice' to start!"
        L.append("🧠 *What I remember:*\n")
        for k,v in mems.items(): L.append(f"• *{k}*: {v}")
        return "\n".join(L)

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

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

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

    # Git
    if "status" in result and "branch" in result:
        s = f"🌿 *Git* — branch `{result['branch']}`\n```\n{result['status']}\n```"
        if result.get("commits"): s += f"\n📝 Recent:\n```\n{result['commits']}\n```"
        return s

    # Screenshot / success with path
    if "path" in result and result.get("success"):
        return f"📸 Saved to `{result['path']}`"

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

    # Generic
    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, user_id, 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):
        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):
        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)

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

    async def _on_cmd(self, chat_id, cmd, args):
        dispatch = {
            "start":  self._c_start,  "help":   self._c_help,
            "status": self._c_status, "health": self._c_health,
            "memory": self._c_memory, "todos":  self._c_todos,
            "notes":  self._c_notes,  "tools":  self._c_tools,
            "run":    self._c_run,    "clear":  self._c_clear,
            "safe":   self._c_safe,   "resume": self._c_resume,
            "search": self._c_search,
        }
        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 for all commands.")

    async def _c_start(self, c, a):
        await self._api.send(c,
            "👋 *Hey! I'm Doit — your personal computer assistant.*\n\n"
            "Just talk to me naturally:\n"
            "  📁  _show me what's in Downloads_\n"
            "  💻  _how's my computer doing?_\n"
            "  🌤  _weather in London_\n"
            "  📝  _make a note: call dentist Friday_\n"
            "  ⏰  _remind me in 30 minutes_\n"
            "  🔍  _find all my PDF files_\n\n"
            "Type /help for all commands.")

    async def _c_help(self, c, a):
        await self._api.send(c,
            "🏋️ *What I can do:*\n\n"
            "📁 *Files* — list, read, write, delete, search\n"
            "💻 *System* — CPU/RAM/disk, processes\n"
            "🐚 *Shell* — `/run <command>`\n"
            "🌐 *Web* — `/search`, weather\n"
            "🧮 *Calc* — math expressions\n"
            "🧠 *Memory* — remember facts, notes, todos\n"
            "📸 *Desktop* — screenshots, notifications\n"
            "🌿 *Git* — status & commits\n"
            "⏰ *Reminders*\n\n"
            "*Commands:*\n"
            "`/health` — CPU/RAM/disk\n"
            "`/memory` — stored facts\n"
            "`/todos` — to-do list\n"
            "`/notes` — your notes\n"
            "`/tools` — all tools\n"
            "`/run cmd` — run shell command\n"
            "`/search q` — web search\n"
            "`/clear` — reset conversation\n"
            "`/safe` / `/resume` — pause/resume\n"
            "`/status` — agent status\n\n"
            "💡 _Just talk naturally — no commands needed!_")

    async def _c_status(self, c, a):
        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"
        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: {len(self._ai._memory._turns)//2} turns")

    async def _c_health(self, c, a):
        await self._api.typing(c)
        from doit_fm.tools import run_tool
        result = await run_tool("sys_monitor", {})
        await self._api.send(c, _trunc(fmt(result, "")))

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

    async def _c_todos(self, c, a):
        from doit_fm.tools import run_tool
        result = await run_tool("todo_list", {})
        await self._api.send(c, _trunc(fmt(result, "To-do list")))

    async def _c_notes(self, c, a):
        from doit_fm.tools import run_tool
        result = await run_tool("note_list", {})
        await self._api.send(c, _trunc(fmt(result, "Your notes")))

    async def _c_tools(self, c, a):
        from doit_fm.tools import TOOLS
        lines = ["🔧 *Available tools:*\n"]
        for name, meta in TOOLS.items():
            danger = " ⚠️" if meta["danger"] else ""
            lines.append(f"  `{name}` — {meta['desc']}{danger}")
        lines.append(f"\n_{len(TOOLS)} tools. Just talk naturally!_")
        text = "\n".join(lines)
        for i in range(0, len(text), 3800):
            await self._api.send(c, text[i:i+3800])

    async def _c_run(self, c, a):
        if not a:
            await self._api.send(c, "Usage: `/run <command>`\n\nExample: `/run ls -la ~/Downloads`"); return
        if self._safe_mode:
            await self._api.send(c, "🔴 Paused. Send /resume first."); return
        cmd = " ".join(a)
        await self._api.typing(c)
        from doit_fm.tools import run_tool
        result = await run_tool("shell_exec", {"command": cmd})
        await self._api.send(c, _trunc(fmt(result, f"Run: `{cmd}`")))

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

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

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

    async def _c_search(self, c, a):
        if not a:
            await self._api.send(c, "Usage: `/search <query>`"); return
        q = " ".join(a)
        await self._api.typing(c)
        from doit_fm.tools import run_tool
        result = await run_tool("web_search", {"query": q})
        await self._api.send(c, _trunc(fmt(result, f"Search: {q}")))

    # ── Free-text messages ────────────────────────────────────────────────────

    async def _on_msg(self, chat_id, text):
        if self._safe_mode:
            await self._api.send(chat_id, "🔴 Paused. Send /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, "😕 Had trouble understanding that — please try again."); return
        await self._dispatch(chat_id, action)

    async def _dispatch(self, chat_id, action):
        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','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, tool, args, desc):
        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 callbacks ──────────────────────────────────────────────────────

    async def _on_callback(self, q):
        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 — 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. No changes made.")


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

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

def _http_post(url, payload, timeout=10):
    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())


async def validate_bot_token(token):
    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, user_id):
    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)
