#!/opt/bux/venv/bin/python
"""tg-send "your message here"        # arg form
   echo "msg" | tg-send                # stdin form (for piping output)
   claude -p "..." | tg-send           # the recurring use case

Forum-topic routing (set in env by the bot before invoking the agent):
  TG_CHAT_ID    — chat to post into (default: first line of tg-allowed.txt)
  TG_THREAD_ID  — message_thread_id, so a backgrounded `claude -p ... | tg-send &`
                  pings back into the same forum topic the user asked from
  TG_REPLY_TO   — optional reply_to_message_id
  TG_LINK_PREVIEW=1 — opt back in to TG link previews (default: suppressed)

Long payloads are split into sequential messages (~3500 chars each) at
paragraph / line / sentence / word boundaries. Telegram silently drops
sendMessage requests over 4096 chars, so this script never relies on
the API to surface oversize input.
"""
from __future__ import annotations

import json
import os
import sys
from pathlib import Path

import time
import urllib.request
import urllib.error


# Resolve the agent dir from the script's own location so a worktree
# checkout (e.g. /tmp/bux-foo/agent/tg-send) finds its sibling
# telegram_bot.py instead of the prod copy. Prod /usr/local/bin/tg-send
# is a symlink into /opt/bux/repo/agent so this still picks up the
# right file in normal installs.
_HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(_HERE))
sys.path.insert(1, "/opt/bux/repo/agent")

from telegram_bot import _chunk_for_telegram, REPLY_MAX  # noqa: E402

ENV_FILE = Path("/etc/bux/tg.env")
ALLOW_FILE = Path("/etc/bux/tg-allowed.txt")
TG_BASE = "https://api.telegram.org"


def _read_kv(path: Path) -> dict[str, str]:
    out: dict[str, str] = {}
    for line in path.read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        k, v = line.split("=", 1)
        out[k.strip()] = v.strip().strip('"').strip("'")
    return out


def _read_payload(args: list[str]) -> str:
    if args:
        return " ".join(args)
    return sys.stdin.read()


def _bound_chat_id() -> str:
    if not ALLOW_FILE.is_file():
        sys.stderr.write("tg-send: no bound chat (run /start in TG first)\n")
        sys.exit(1)
    for line in ALLOW_FILE.read_text().splitlines():
        line = line.strip()
        if line:
            return line
    sys.stderr.write(f"tg-send: empty {ALLOW_FILE}\n")
    sys.exit(1)


def _post(token: str, payload: dict, attempt: int = 0) -> None:
    body = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(
        f"{TG_BASE}/bot{token}/sendMessage",
        data=body,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    try:
        urllib.request.urlopen(req, timeout=15).read()
    except urllib.error.HTTPError as e:
        raw = e.read().decode("utf-8", "replace")
        # 429: honor TG's `retry_after` and retry once. A multi-chunk send
        # from a chatty lane can hit per-chat rate limits even though each
        # chunk is well under TG's per-second cap; one retry covers those
        # without burning the whole reply. The cap is 60s — under a real
        # flood limit TG can advertise tens of seconds, so a 5s cap would
        # come back too early and re-trip the limit. 60s bounds runaway
        # waits while letting normal flood backoffs through.
        if e.code == 429 and attempt == 0:
            wait = 1
            try:
                parsed = json.loads(raw) if raw.startswith("{") else {}
                ra = (parsed.get("parameters") or {}).get("retry_after")
                if isinstance(ra, (int, float)) and ra > 0:
                    wait = min(int(ra), 60)
            except Exception:
                pass
            time.sleep(wait)
            _post(token, payload, attempt + 1)
            return
        sys.stderr.write(f"tg-send: TG returned {e.code} {e.reason}: {raw[:300]}\n")
        sys.exit(1)


def main() -> int:
    text = _read_payload(sys.argv[1:]).rstrip("\n")
    if not text:
        sys.stderr.write("tg-send: empty payload (no args, no stdin)\n")
        return 2

    if not ENV_FILE.is_file():
        sys.stderr.write(f"tg-send: cannot read {ENV_FILE}\n")
        return 1
    env = _read_kv(ENV_FILE)
    token = env.get("TG_BOT_TOKEN") or os.environ.get("TG_BOT_TOKEN", "")
    if not token:
        sys.stderr.write("tg-send: TG_BOT_TOKEN missing\n")
        return 1

    chat_id = os.environ.get("TG_CHAT_ID") or _bound_chat_id()
    try:
        chat_id_int = int(chat_id)
    except ValueError:
        sys.stderr.write(f"tg-send: bad TG_CHAT_ID {chat_id!r}\n")
        return 1

    thread_id_env = os.environ.get("TG_THREAD_ID")
    thread_id: int | None = None
    if thread_id_env:
        try:
            thread_id = int(thread_id_env)
        except ValueError:
            sys.stderr.write(f"tg-send: bad TG_THREAD_ID {thread_id_env!r}\n")
            return 1

    reply_to_env = os.environ.get("TG_REPLY_TO")
    reply_to: int | None = None
    if reply_to_env:
        try:
            reply_to = int(reply_to_env)
        except ValueError:
            sys.stderr.write(f"tg-send: bad TG_REPLY_TO {reply_to_env!r}\n")
            return 1

    preview_disabled = os.environ.get("TG_LINK_PREVIEW", "") != "1"

    chunks = _chunk_for_telegram(text, REPLY_MAX)
    for i, chunk in enumerate(chunks):
        payload: dict = {
            "chat_id": chat_id_int,
            "text": chunk or " ",
            "link_preview_options": {"is_disabled": preview_disabled},
        }
        if thread_id is not None:
            payload["message_thread_id"] = thread_id
        # Reply only on the first chunk; the follow-up bubbles stand on
        # their own so the thread doesn't show N quoted-reply badges.
        if reply_to is not None and i == 0:
            payload["reply_to_message_id"] = reply_to
        _post(token, payload)

    return 0


if __name__ == "__main__":
    sys.exit(main())
