"""Telegram poller — polls getUpdates and sends outbound messages.

Runs as a background thread inside the Bridge MCP server.
"""

from __future__ import annotations

import json
import os
import re
import sys
import threading
import time
from urllib.request import urlopen, Request

from .message_db import MessageDB


def parse_updates(raw: dict) -> list[dict]:
    """Parse Telegram getUpdates response into normalized messages."""
    if not raw.get("ok"):
        return []
    results = []
    for update in raw.get("result", []):
        msg = update.get("message", {})
        text = msg.get("text")
        if not text:
            continue
        chat = msg.get("chat", {})
        from_user = msg.get("from", {})
        results.append({
            "update_id": update["update_id"],
            "chat_id": str(chat.get("id", "")),
            "user_id": str(from_user.get("id", "")),
            "username": from_user.get("username", ""),
            "text": text,
            "message_id": str(msg.get("message_id", "")),
        })
    return results


def is_allowed_user(
    user_id: str,
    access_path: str = os.path.expanduser("~/.claude/channels/telegram/access.json"),
) -> bool:
    """Check if user_id is in the Telegram access allowlist."""
    if not os.path.isfile(access_path):
        return True  # no access file = permissive
    try:
        with open(access_path) as f:
            access = json.load(f)
        allowed = access.get("allowFrom", [])
        if not allowed:
            return True  # empty list = allow all
        return user_id in allowed
    except (json.JSONDecodeError, IOError):
        return True


def telegram_get_updates(
    token: str, offset: int = 0, timeout: int = 30,
) -> tuple[list[dict], dict]:
    """Call Telegram getUpdates. Returns (parsed_updates, raw_response)."""
    url = f"https://api.telegram.org/bot{token}/getUpdates"
    params = {"offset": offset, "timeout": timeout}
    payload = json.dumps(params).encode()
    req = Request(url, data=payload, headers={"Content-Type": "application/json"})
    try:
        with urlopen(req, timeout=timeout + 10) as resp:
            raw = json.loads(resp.read())
        return parse_updates(raw), raw
    except Exception as e:
        print(f"[poller] getUpdates error: {e}", file=sys.stderr)
        return [], {}


def markdown_to_telegram_html(text: str) -> str:
    """Convert Markdown to Telegram HTML format (mirrors channel/format.ts)."""
    # Step 1: Extract fenced code blocks
    code_blocks: list[str] = []

    def save_code_block(m: re.Match) -> str:
        code_blocks.append(m.group(1))
        return f"\x00CB{len(code_blocks) - 1}\x00"

    result = re.sub(r"```[\w-]*\n?([\s\S]*?)```", save_code_block, text)

    # Step 2: Extract inline code blocks
    inline_codes: list[str] = []

    def save_inline_code(m: re.Match) -> str:
        inline_codes.append(m.group(1))
        return f"\x00IC{len(inline_codes) - 1}\x00"

    result = re.sub(r"`([^`\n]+)`", save_inline_code, result)

    # Step 3: Escape HTML entities in non-code text
    result = result.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")

    # Step 4: Restore inline code with escaped content
    def restore_inline(m: re.Match) -> str:
        code = inline_codes[int(m.group(1))]
        escaped = code.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
        return f"<code>{escaped}</code>"

    result = re.sub(r"\x00IC(\d+)\x00", restore_inline, result)

    # Step 5: Convert headings (# Title → <b>Title</b>)
    result = re.sub(r"^#{1,6}\s+(.+)$", r"<b>\1</b>", result, flags=re.MULTILINE)

    # Step 6: Convert bold (**text** or __text__)
    result = re.sub(r"\*\*([^*\n]+)\*\*", r"<b>\1</b>", result)
    result = re.sub(r"(?<![_\w])__([^_\n]+)__(?![_\w])", r"<b>\1</b>", result)

    # Step 7: Convert italic (*text* or _text_) — after bold to avoid conflict
    result = re.sub(r"(?<!\*)\*([^*\n]+)\*(?!\*)", r"<i>\1</i>", result)
    result = re.sub(r"(?<![_\w])_([^_\n]+)_(?![_\w])", r"<i>\1</i>", result)

    # Step 8: Convert strikethrough
    result = re.sub(r"~~([^~\n]+)~~", r"<s>\1</s>", result)

    # Step 9: Convert links [label](url)
    result = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2">\1</a>', result)

    # Step 10: Restore code blocks with escaped content
    def restore_code_block(m: re.Match) -> str:
        code = code_blocks[int(m.group(1))]
        escaped = code.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
        return f"<pre><code>{escaped.rstrip(chr(10))}</code></pre>"

    result = re.sub(r"\x00CB(\d+)\x00", restore_code_block, result)

    return result


def _strip_html(html: str) -> str:
    """Strip HTML tags and unescape entities (plain text fallback)."""
    text = re.sub(r"<[^>]+>", "", html)
    return text.replace("&amp;", "&").replace("&lt;", "<").replace("&gt;", ">")


def telegram_send_message(token: str, chat_id: str, text: str) -> bool | int:
    """Send a message via Telegram Bot API with HTML formatting (markdown converted).

    Returns True on success, False on error, or an int (retry_after seconds) on rate limit.
    """
    url = f"https://api.telegram.org/bot{token}/sendMessage"
    html = markdown_to_telegram_html(text)

    def _post(body: dict) -> bool | int:
        payload = json.dumps(body).encode()
        req = Request(url, data=payload, headers={"Content-Type": "application/json"})
        try:
            with urlopen(req, timeout=10) as resp:
                result = json.loads(resp.read())
                return result.get("ok", False)
        except Exception as e:
            # Check for 429 rate limit
            if hasattr(e, "code") and e.code == 429:
                try:
                    error_body = json.loads(e.read())
                    retry_after = error_body.get("parameters", {}).get("retry_after", 30)
                    print(f"[poller] rate limited, retry after {retry_after}s", file=sys.stderr)
                    return retry_after
                except Exception:
                    return 30  # default backoff
            print(f"[poller] sendMessage error: {e}", file=sys.stderr)
            return False

    result = _post({"chat_id": chat_id, "text": html, "parse_mode": "HTML"})
    if isinstance(result, int):
        return result  # rate limit
    if not result:
        # Fallback: plain text if Telegram rejects HTML
        print("[poller] HTML parse failed, falling back to plain text", file=sys.stderr)
        return _post({"chat_id": chat_id, "text": _strip_html(html)})
    return True


class TelegramPoller:
    """Polls Telegram and processes inbound/outbound messages."""

    def __init__(self, token: str, msg_db: MessageDB):
        self.token = token
        self.msg_db = msg_db
        self._running = False
        self._thread: threading.Thread | None = None

    def start(self):
        """Start polling in a background thread."""
        self._running = True
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()

    def stop(self):
        """Stop the poller thread."""
        self._running = False
        if self._thread:
            self._thread.join(timeout=5)

    def _run(self):
        """Main polling loop."""
        self.msg_db = MessageDB()  # fresh connection owned by this thread
        try:
            while self._running:
                try:
                    self.poll_once()
                except Exception as e:
                    print(f"[poller] error in poll cycle: {e}", file=sys.stderr)
                    time.sleep(5)
        finally:
            self.msg_db.close()

    def poll_once(self):
        """Run one poll cycle: fetch updates + send outbound."""
        # Get current offset
        offset_str = self.msg_db.get_state("telegram_offset")
        offset = int(offset_str) if offset_str else 0

        # Poll for inbound
        updates, raw = telegram_get_updates(self.token, offset=offset, timeout=1)
        for update in updates:
            if not is_allowed_user(update["user_id"]):
                print(f"[poller] rejected message from non-allowed user {update['user_id']}", file=sys.stderr)
                # Still advance offset
            else:
                self.msg_db.create_inbound(
                    platform="telegram",
                    chat_id=update["chat_id"],
                    user_id=update["user_id"],
                    message_text=update["text"],
                    message_id=update["message_id"],
                    username=update["username"],
                )

            # Advance offset past this update
            new_offset = update["update_id"] + 1
            self.msg_db.set_state("telegram_offset", str(new_offset))

        # Retry unacknowledged inbound messages (delivered but not acknowledged after 3s)
        unacked = self.msg_db.get_unacknowledged_inbound(timeout_seconds=3)
        for msg in unacked:
            if msg["retry_count"] + 1 >= msg["max_retries"]:
                self.msg_db.mark_inbound_failed(msg["id"])
                # Notify user their message was lost
                self.msg_db.create_outbound(
                    "telegram", msg["chat_id"],
                    "Sorry, your message could not be delivered to the Bridge Bot. Please try again.",
                    source="system",
                )
                print(f"[poller] inbound #{msg['id']} failed after {msg['max_retries']} retries", file=sys.stderr)
            else:
                self.msg_db.increment_inbound_retry(msg["id"])

        # Send pending outbound (max 5 per cycle to avoid rate limits)
        pending = self.msg_db.get_pending_outbound()
        for msg in pending[:5]:
            result = telegram_send_message(self.token, msg["chat_id"], msg["message_text"])
            if result is True:
                self.msg_db.mark_outbound_sent(msg["id"])
            elif isinstance(result, int):
                # Rate limited — stop sending, sleep for retry_after
                print(f"[poller] backing off {result}s due to rate limit", file=sys.stderr)
                time.sleep(result)
                break
            else:
                self.msg_db.increment_outbound_retry(msg["id"])
                if msg["retry_count"] + 1 >= msg["max_retries"]:
                    self.msg_db.mark_outbound_failed(msg["id"])

        # Cleanup old sent/failed outbound messages (older than 24h)
        self.msg_db.cleanup_old_outbound(max_age_hours=24)
