import re
import json
import os
import threading
import time
import sys
import traceback
import asyncio
import io
import tempfile
from pathlib import Path
import discord
from discord import SyncWebhook
from collections import deque
import aiohttp
import requests
import telegram
from telegram import Update, ReactionTypeEmoji, ReactionTypeCustomEmoji
from telegram.ext import Application, MessageHandler, filters

DIR = Path(__file__).resolve().parent.parent
LOG_FILE = "marsbot.log"
MESSAGE_HISTORY_FILE = "message_history.json"
PFP_FILE = "pfp_mappings.json"
TELEGRAM_USERS_FILE = "telegram_users.json"

LOG_WEBHOOK = ""
import queue as _log_queue
_log_q = _log_queue.Queue()
_log_worker_started = False


def _log_worker():
    while True:
        line = _log_q.get()
        if line is None:
            break
        try:
            requests.post(LOG_WEBHOOK, json={"content": line[:2000]}, timeout=10)
        except Exception:
            pass


def log(msg):
    ts = time.strftime("%Y-%m-%d %H:%M:%S")
    line = f"[{ts}] {msg}"
    print(line, flush=True)
    with open(LOG_FILE, "a") as f:
        f.write(line + "\n")
    global _log_worker_started
    if LOG_WEBHOOK and not _log_worker_started:
        _log_worker_started = True
        threading.Thread(target=_log_worker, daemon=True).start()
    if LOG_WEBHOOK:
        _log_q.put(line)


def load_env():
    env = {}
    for path in [
        DIR / ".env",
        DIR / "bridge" / ".env",
    ]:
        try:
            with open(path) as f:
                for line in f:
                    line = line.strip()
                    if not line or line.startswith("#") or "=" not in line:
                        continue
                    k, v = line.split("=", 1)
                    env[k.strip()] = v.strip()
        except FileNotFoundError:
            pass
    return env


ENV = load_env()
TELEGRAM_BOT_TOKEN = ENV.get("TELEGRAM_BOT_TOKEN", "") or os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = int(ENV.get("TELEGRAM_CHAT_ID"))
TELEGRAM_DISCORD_WEBHOOK = ENV.get("TELEGRAM_DISCORD_WEBHOOK", "") or os.getenv("TELEGRAM_DISCORD_WEBHOOK", "")
TELEGRAM_DISCORD_CHANNEL_ID = None
LOG_WEBHOOK = ENV.get("LOG_WEBHOOK", "") or os.getenv("LOG_WEBHOOK", "")

TELEGRAM_USER_BOT_MAP = {}
for mapping_path in [
    DIR / "telegram_map.json",
    DIR / "bridge" / "telegram_map.json",
]:
    try:
        with open(mapping_path) as _f:
            _raw = json.load(_f)
        for _k, _v in _raw.items():
            if isinstance(_v, str):
                _v = {"telegram": _v}
            TELEGRAM_USER_BOT_MAP[int(_k)] = _v
        break
    except (FileNotFoundError, json.JSONDecodeError):
        continue

CHANNEL_MAP = {
    "#generale": (1319031097757012009, 1319031098336084010),
    "#zerogpt": (1110601423781122111, 1518318568083558450),
}
DISCORD_IRC_MAP = {}
for irc_ch, (gid, cid) in CHANNEL_MAP.items():
    DISCORD_IRC_MAP[cid] = irc_ch

DISCORD_TOKEN = ENV.get("DISCORD_TOKEN", "") or os.getenv("DISCORD_TOKEN", "")

discord_bot_instance = None
telegram_app = None
telegram_loop = None
user_bot_cache = {}
message_history = None
pfp_map = {}
telegram_users = {}
webhook_cache = {}
_discord_http_sessions: dict = {}


async def _get_discord_session(key="_default"):
    if key not in _discord_http_sessions:
        timeout = aiohttp.ClientTimeout(total=60)
        connector = aiohttp.TCPConnector(keepalive_timeout=60)
        _discord_http_sessions[key] = aiohttp.ClientSession(timeout=timeout, connector=connector)
    return _discord_http_sessions[key]


async def close_discord_sessions():
    for s in _discord_http_sessions.values():
        await s.close()
    _discord_http_sessions.clear()


def _coerce_id(v):
    if isinstance(v, bool):
        return v
    if isinstance(v, str):
        try:
            return int(v)
        except ValueError:
            return v
    return v


def load_message_history():
    global message_history
    try:
        with open(MESSAGE_HISTORY_FILE) as f:
            message_history = deque(json.load(f), maxlen=50)
    except (FileNotFoundError, json.JSONDecodeError):
        message_history = deque(maxlen=50)


def save_message_history():
    with open(MESSAGE_HISTORY_FILE, "w") as f:
        json.dump(list(message_history), f, indent=2)


def record_msg(platform, msg_id, channel_id, counterpart_id=None):
    message_history.append({
        "platform": platform,
        "msg_id": _coerce_id(msg_id),
        "channel_id": _coerce_id(channel_id),
        "counterpart_id": _coerce_id(counterpart_id),
    })
    save_message_history()


def get_counterpart(msg_id):
    msg_id = _coerce_id(msg_id)
    for rec in reversed(message_history):
        if _coerce_id(rec["msg_id"]) == msg_id:
            return rec["counterpart_id"]
    return None


def get_counterpart_channel(msg_id):
    counterpart_id = get_counterpart(msg_id)
    if counterpart_id is None:
        return None
    counterpart_id = _coerce_id(counterpart_id)
    for rec in reversed(message_history):
        if _coerce_id(rec.get("msg_id")) == counterpart_id and rec.get("platform") == "discord":
            return rec.get("channel_id")
    return None


def set_config(token, chat_id, user_bot_map):
    global TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, TELEGRAM_USER_BOT_MAP
    TELEGRAM_BOT_TOKEN = token
    TELEGRAM_CHAT_ID = chat_id
    TELEGRAM_USER_BOT_MAP = user_bot_map


def set_telegram_app(app, loop):
    global telegram_app, telegram_loop
    telegram_app = app
    telegram_loop = loop


def get_user_bot(user_id):
    entry = TELEGRAM_USER_BOT_MAP.get(user_id)
    if not entry:
        return None
    token = entry.get("telegram")
    if not token:
        return None
    if user_id not in user_bot_cache:
        user_bot_cache[user_id] = telegram.Bot(token)
    return user_bot_cache[user_id]


def load_pfp():
    global pfp_map
    try:
        with open(PFP_FILE) as f:
            pfp_map = json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        pfp_map = {}


def save_pfp():
    with open(PFP_FILE, 'w') as f:
        json.dump(pfp_map, f, indent=2)


def load_telegram_users():
    global telegram_users
    try:
        with open(TELEGRAM_USERS_FILE) as f:
            telegram_users = json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        telegram_users = {}


def save_telegram_users():
    with open(TELEGRAM_USERS_FILE, "w") as f:
        json.dump(telegram_users, f, indent=2)


def track_telegram_user(user):
    uid = str(user.id)
    username = user.username or ""
    full_name = user.full_name or ""
    entry = telegram_users.get(uid, {})
    entry["username"] = username
    entry["full_name"] = full_name
    entry["last_seen"] = time.time()
    telegram_users[uid] = entry
    save_telegram_users()


def telegram_online_text():
    now = time.time()
    active = []
    for uid, e in telegram_users.items():
        ls = e.get("last_seen", 0)
        if now - ls < 15 * 60:
            active.append(e.get("full_name") or e.get("username") or uid)
    if active:
        return "Online on Telegram (last 15m):\n\u2022 " + ", ".join(sorted(set(active)))
    return "No recent Telegram activity."


def discord_online_text(client):
    blocks = []
    seen = set()
    for gid, cid in CHANNEL_MAP.values():
        guild = client.get_guild(gid)
        if not guild:
            continue
        if guild.name.lower() == "gabe valve":
            continue
        online = []
        for m in guild.members:
            if m.bot:
                continue
            if m.status != discord.Status.offline and m.display_name not in seen:
                seen.add(m.display_name)
                online.append(m.display_name)
        if online:
            blocks.append(f"\u2022 {guild.name}: " + ", ".join(sorted(online)))
    if blocks:
        return "Online on Discord:\n" + "\n".join(blocks)
    return "Nobody online on Discord."


def get_discord_token(user_id):
    if user_id is None:
        return None
    entry = TELEGRAM_USER_BOT_MAP.get(int(user_id)) if str(user_id).isdigit() else None
    if entry is None:
        entry = TELEGRAM_USER_BOT_MAP.get(user_id)
    if not entry:
        return None
    token = entry.get("discord")
    if token in {"", None, "none", "no fucking idea"}:
        return None
    return token


def resolve_telegram_sender_entry(user):
    user_id = getattr(user, "id", None)
    if user_id is not None:
        lookup_id = int(user_id) if str(user_id).isdigit() else user_id
        if lookup_id in TELEGRAM_USER_BOT_MAP:
            return TELEGRAM_USER_BOT_MAP[lookup_id]
    uname = (getattr(user, "username", "") or "").lower()
    if uname:
        for entry in TELEGRAM_USER_BOT_MAP.values():
            if (entry.get("telegram_username") or "").lower() == uname:
                return entry
    return None


def resolve_discord_channel_id():
    global TELEGRAM_DISCORD_CHANNEL_ID
    try:
        m = re.search(r"/webhooks/(\d+)/([\w-]+)", TELEGRAM_DISCORD_WEBHOOK)
        if not m:
            return
        wid, wtoken = m.group(1), m.group(2)
        r = requests.get(f"https://discord.com/api/v10/webhooks/{wid}/{wtoken}", timeout=30)
        TELEGRAM_DISCORD_CHANNEL_ID = r.json().get("channel_id")
    except Exception as e:
        log(f"Discord channel resolve error: {e}")



def convert_tgs_to_gif(tgs_bytes):
    import gzip
    from lottie.parsers import tgs as _tgs
    from lottie.exporters import gif as _gif
    anim = _tgs.parse_tgs(gzip.GzipFile(fileobj=io.BytesIO(tgs_bytes)))
    out = io.BytesIO()
    _gif.export_gif(anim, out)
    return out.getvalue()


def telegram_send(text):
    if not telegram_app or not TELEGRAM_BOT_TOKEN:
        return
    try:
        loop = telegram_loop
        if loop is None or not loop.is_running():
            return
        fut = asyncio.run_coroutine_threadsafe(
            telegram_app.bot.send_message(chat_id=TELEGRAM_CHAT_ID, text=text),
            loop,
        )
        fut.add_done_callback(_log_future_error)
    except Exception as e:
        log(f"Telegram send error: {e}")


async def get_telegram_avatar(user_id):
    try:
        photos = await telegram_app.bot.get_user_profile_photos(user_id)
        if photos.photos:
            f = await photos.photos[0][-1].get_file()
            token = telegram_app.bot.token
            return f"https://api.telegram.org/file/bot{token}/{f.file_path}"
    except Exception as e:
        log(f"Telegram avatar error: {e}")
    return None


async def telegram_send_to_discord(display_name, content, attachments, reply_discord_id=None, avatar_url=None):
    if not TELEGRAM_DISCORD_WEBHOOK:
        return None
    payload = {"content": content, "username": display_name}
    if avatar_url:
        payload["avatar_url"] = avatar_url
    if reply_discord_id:
        payload["message_reference"] = {"message_id": reply_discord_id}
    session = await _get_discord_session("_webhook")
    try:
        if attachments:
            form = aiohttp.FormData()
            form.add_field("payload_json", json.dumps(payload), content_type="application/json")
            for i, (fname, data, mime) in enumerate(attachments):
                form.add_field(f"file{i}", data, filename=fname, content_type=mime)
            async with session.post(
                TELEGRAM_DISCORD_WEBHOOK,
                data=form,
                timeout=aiohttp.ClientTimeout(total=60),
            ) as resp:
                data = await resp.json()
                return data.get("id")
        else:
            async with session.post(
                TELEGRAM_DISCORD_WEBHOOK,
                json=payload,
                timeout=aiohttp.ClientTimeout(total=30),
            ) as resp:
                data = await resp.json()
                return data.get("id")
    except Exception as e:
        log(f"Telegram->Discord webhook error: {e}")
        return None


async def send_via_discord_bot(token, channel_id, content, attachments, reply_message_id=None):
    session = await _get_discord_session(token)
    headers = {"Authorization": f"Bot {token}"}
    try:
        async with session.post(
            f"https://discord.com/api/v10/channels/{channel_id}/typing",
            headers=headers,
            timeout=aiohttp.ClientTimeout(total=10),
        ):
            pass
    except Exception as e:
        log(f"Discord typing error: {e}")
    payload = {"content": content}
    if reply_message_id:
        payload["message_reference"] = {"message_id": reply_message_id}
    try:
        if attachments:
            form = aiohttp.FormData()
            form.add_field("payload_json", json.dumps(payload), content_type="application/json")
            for i, (fname, data, mime) in enumerate(attachments):
                form.add_field(f"file{i}", data, filename=fname, content_type=mime)
            async with session.post(
                f"https://discord.com/api/v10/channels/{channel_id}/messages",
                headers=headers,
                data=form,
                timeout=aiohttp.ClientTimeout(total=60),
            ) as resp:
                data = await resp.json()
                return data.get("id")
        else:
            async with session.post(
                f"https://discord.com/api/v10/channels/{channel_id}/messages",
                headers=headers,
                json=payload,
                timeout=aiohttp.ClientTimeout(total=30),
            ) as resp:
                data = await resp.json()
                return data.get("id")
    except Exception as e:
        log(f"Telegram->Discord bot send error: {e}")
        return None


def send_discord_webhook_message(display_name, content):
    if not TELEGRAM_DISCORD_WEBHOOK:
        return None
    payload = {"content": content, "username": display_name}
    try:
        response = requests.post(TELEGRAM_DISCORD_WEBHOOK, json=payload, timeout=30)
        response.raise_for_status()
        try:
            return response.json().get("id")
        except Exception:
            return None
    except Exception as e:
        log(f"Discord web UI webhook error: {e}")
        return None


def _extract_message_text(message):
    if message is None:
        return ""
    if hasattr(message, "clean_content"):
        content = getattr(message, "clean_content", "")
        if content is not None:
            return content
    if hasattr(message, "content"):
        content = getattr(message, "content", "")
        if content is not None:
            return content
    if hasattr(message, "text"):
        content = getattr(message, "text", "")
        if content is not None:
            return content
    if hasattr(message, "caption"):
        content = getattr(message, "caption", "")
        if content is not None:
            return content
    return ""


def _discord_reaction_to_telegram_reaction(reaction):
    if reaction is None:
        return None
    emoji_id = getattr(reaction, "id", None)
    if emoji_id is not None:
        return ReactionTypeCustomEmoji(custom_emoji_id=str(emoji_id))
    emoji = getattr(reaction, "emoji", None)
    if emoji:
        return ReactionTypeEmoji(emoji=str(emoji))
    return None


def prepare_discord_attachments(message, text_bits):
    photos = []
    videos = []
    tb = list(text_bits)
    for a in message.attachments:
        try:
            if a.content_type and a.content_type.startswith("image"):
                photos.append(a.url)
            elif a.content_type and a.content_type.startswith("video"):
                videos.append(a.url)
            else:
                tb.append(a.url)
        except Exception as e:
            log(f"Discord attachment error: {e}")
            tb.append(a.url)
    return tb, photos, videos


async def _send_with_reply(send_call, reply_params):
    try:
        return await send_call(reply_params)
    except Exception as e:
        if reply_params and ("replied" in str(e).lower() or "not found" in str(e).lower()):
            log(f"Reply target unreachable ({e}); sending without threading")
            return await send_call(None)
        raise


async def telegram_send_media_async(bot, caption, photos, videos, reply_tg_id=None):
    try:
        await bot.send_chat_action(chat_id=TELEGRAM_CHAT_ID, action="typing")
    except Exception:
        pass
    reply_params = {"message_id": reply_tg_id} if reply_tg_id else None
    primary_tg_id = None
    if caption:
        sent = await _send_with_reply(
            lambda rp: bot.send_message(chat_id=TELEGRAM_CHAT_ID, text=caption,
                                        reply_parameters=rp),
            reply_params,
        )
        primary_tg_id = sent.message_id
        reply_params = {"message_id": primary_tg_id}
    for p in photos:
        sent = await _send_with_reply(
            lambda rp: bot.send_photo(chat_id=TELEGRAM_CHAT_ID, photo=p, caption=None,
                                      reply_parameters=rp),
            reply_params,
        )
        if primary_tg_id is None:
            primary_tg_id = sent.message_id
            reply_params = {"message_id": primary_tg_id}
    for v in videos:
        sent = await _send_with_reply(
            lambda rp: bot.send_video(chat_id=TELEGRAM_CHAT_ID, video=v, caption=None,
                                      reply_parameters=rp),
            reply_params,
        )
        if primary_tg_id is None:
            primary_tg_id = sent.message_id
            reply_params = {"message_id": primary_tg_id}
    return primary_tg_id


def telegram_send_media(caption, photos, videos, reply_tg_id=None, bot=None):
    if not telegram_app or not TELEGRAM_BOT_TOKEN:
        return None
    if bot is None:
        bot = telegram_app.bot
    try:
        loop = telegram_loop
        if loop is None or not loop.is_running():
            return None
        future = asyncio.run_coroutine_threadsafe(
            telegram_send_media_async(bot, caption, photos, videos, reply_tg_id),
            loop,
        )
        return future.result(timeout=60)
    except Exception as e:
        log(f"Telegram media send error: {e}")
        return None


def relay_discord_to_telegram(message, nick, text_bits, photos, videos, channel_id):
    if not message or not telegram_app or not TELEGRAM_BOT_TOKEN:
        return None
    try:
        text = " ".join(text_bits).strip() if text_bits else ""
        caption = text
        reply_tg_id = None
        if getattr(message, "reference", None) and getattr(message.reference, "message_id", None):
            reply_tg_id = get_counterpart(message.reference.message_id)
        send_bot = get_user_bot(message.author.id) if getattr(message, "author", None) else None
        if send_bot is None:
            send_bot = telegram_app.bot
        tg_id = telegram_send_media(caption, photos, videos, reply_tg_id, bot=send_bot)
        if tg_id is not None:
            record_msg("discord", message.id, channel_id, tg_id)
            record_msg("telegram", tg_id, TELEGRAM_CHAT_ID, message.id)
        return tg_id
    except Exception as e:
        log(f"Discord relay error: {e}")
        return None


def _log_future_error(fut):
    try:
        exc = fut.exception()
        if exc:
            log(f"Async future error: {exc}")
    except Exception as e:
        log(f"Future error callback exception: {e}")


async def _edit_telegram_message(bot, chat_id, message_id, text):
    if not text:
        return
    try:
        await bot.edit_message_text(chat_id=chat_id, message_id=message_id, text=text)
        return
    except Exception as e:
        log(f"_edit_telegram_message edit_message_text failed for msg {message_id}: {e}")
    try:
        await bot.edit_message_caption(chat_id=chat_id, message_id=message_id, caption=text)
    except Exception as e:
        log(f"_edit_telegram_message edit_message_caption also failed for msg {message_id}: {e}")


def relay_discord_edit_to_telegram(message, nick, text_bits, channel_id):
    if not message or not telegram_app or not TELEGRAM_BOT_TOKEN:
        return None
    try:
        text = " ".join(text_bits).strip() if text_bits else ""
        counterpart_id = get_counterpart(message.id)
        if not counterpart_id:
            return None
        bot = get_user_bot(message.author.id) if getattr(message, "author", None) else None
        if bot is None:
            bot = telegram_app.bot
        loop = telegram_loop
        if loop is not None and loop.is_running():
            future = asyncio.run_coroutine_threadsafe(
                _edit_telegram_message(bot, TELEGRAM_CHAT_ID, counterpart_id, text),
                loop,
            )
            future.add_done_callback(_log_future_error)
        return counterpart_id
    except Exception as e:
        log(f"Discord edit relay error: {e}")
        return None


def relay_discord_reaction_to_telegram(message, emoji, remove=False):
    if not message or not telegram_app or not TELEGRAM_BOT_TOKEN:
        return None
    try:
        counterpart_id = get_counterpart(message.id)
        if not counterpart_id:
            return None
        tg_emoji = _discord_reaction_to_telegram_reaction(emoji)
        if tg_emoji is None:
            return None
        bot = get_user_bot(message.author.id) if getattr(message, "author", None) else None
        if bot is None:
            bot = telegram_app.bot
        if remove:
            loop = telegram_loop
            if loop is not None and loop.is_running():
                fut = asyncio.run_coroutine_threadsafe(
                    bot.set_message_reaction(chat_id=TELEGRAM_CHAT_ID, message_id=counterpart_id, reaction=[]),
                    loop,
                )
                fut.add_done_callback(_log_future_error)
            return counterpart_id
        loop = telegram_loop
        if loop is not None and loop.is_running():
            fut = asyncio.run_coroutine_threadsafe(
                bot.set_message_reaction(chat_id=TELEGRAM_CHAT_ID, message_id=counterpart_id, reaction=[tg_emoji]),
                loop,
            )
            fut.add_done_callback(_log_future_error)
        return counterpart_id
    except Exception as e:
        log(f"Discord reaction relay error: {e}")
        return None


async def relay_telegram_edit_to_discord(message, content, discord_client, webhook_cache):
    if not discord_client:
        return None
    counterpart_id = get_counterpart(message.message_id)
    if not counterpart_id:
        return None
    channel_id = get_counterpart_channel(message.message_id)
    if not channel_id:
        return None
    channel = discord_client.get_channel(channel_id)
    if not channel:
        return None
    try:
        discord_message = await channel.fetch_message(counterpart_id)
        if not discord_message:
            return None
        await discord_message.edit(content=content)
        return counterpart_id
    except Exception as e:
        log(f"Discord edit relay error: {e}")
        return None


async def relay_telegram_reaction_to_discord(message_reaction, discord_client):
    if not discord_client:
        return None
    counterpart_id = get_counterpart(message_reaction.message_id)
    channel_id = get_counterpart_channel(message_reaction.message_id)
    if not counterpart_id or not channel_id:
        return None
    channel = discord_client.get_channel(channel_id)
    if not channel:
        return None
    try:
        discord_message = await channel.fetch_message(counterpart_id)
        if not discord_message:
            return None
        reactions = getattr(message_reaction, "new_reaction", None) or []
        if not reactions:
            await discord_message.clear_reactions()
            return counterpart_id
        for reaction in reactions:
            emoji = None
            if getattr(reaction, "custom_emoji_id", None):
                emoji = discord.PartialEmoji(id=int(reaction.custom_emoji_id), name=None)
            elif getattr(reaction, "emoji", None):
                emoji = reaction.emoji
            if emoji is not None:
                await discord_message.add_reaction(emoji)
    except Exception as e:
        log(f"Discord reaction relay error: {e}")
        return None
    return counterpart_id


async def telegram_message_handler(update: Update, context):
    if not update.message and not update.edited_message and not update.message_reaction:
        return
    if update.message and update.message.chat_id != TELEGRAM_CHAT_ID:
        return
    if update.edited_message and update.edited_message.chat_id != TELEGRAM_CHAT_ID:
        return
    if update.message_reaction and getattr(update.message_reaction, "chat", None) and getattr(update.message_reaction.chat, "id", None) != TELEGRAM_CHAT_ID:
        return

    if update.message_reaction:
        try:
            await relay_telegram_reaction_to_discord(update.message_reaction, discord_bot_instance)
        except Exception as e:
            log(f"Telegram reaction relay error: {e}")
        return

    if update.edited_message:
        message = update.edited_message
        content = _extract_message_text(message)
        if content is None:
            return
        try:
            await relay_telegram_edit_to_discord(message, content, discord_bot_instance, webhook_cache)
        except Exception as e:
            log(f"Telegram edit relay error: {e}")
        return

    message = update.message
    if not message:
        return

    author = message.from_user
    display_name = author.full_name or author.username or "telegram"
    content = message.text or message.caption or ""
    track_telegram_user(author)
    log(f"Telegram incoming from {display_name}: {content}")

    if content.strip() == "/online":
        text = discord_online_text(discord_bot_instance)
        try:
            await context.bot.send_message(chat_id=update.message.chat_id, text=text)
        except Exception as e:
            log(f"online cmd telegram send error: {e}")
        return

    if content.strip().startswith("/ping"):
        rest = content.strip()[5:].strip()
        if not rest:
            try:
                await context.bot.send_message(chat_id=update.message.chat_id,
                                               text="Usage: /ping <discord name> <message>")
            except Exception:
                pass
            return
        parts = rest.split(None, 1)
        arg = parts[0].lstrip("@")
        msg = parts[1] if len(parts) > 1 else ""
        target_id = None
        target_name = None
        target_cid = None
        client = discord_bot_instance
        if client:
            for gid, cid in CHANNEL_MAP.values():
                guild = client.get_guild(gid)
                if not guild:
                    continue
                for m in guild.members:
                    if m.bot:
                        continue
                    if (m.display_name and m.display_name.lower() == arg.lower()) or \
                       (m.name and m.name.lower() == arg.lower()):
                        target_id = m.id
                        target_name = m.display_name
                        target_cid = cid
                        break
                if target_id:
                    break
        if not target_id:
            for uid, entry in TELEGRAM_USER_BOT_MAP.items():
                if (entry.get("telegram_username") or "").lower() == arg.lower():
                    target_id = int(uid)
                    break
        if not target_id and arg.isdigit():
            target_id = int(arg)
        if target_id and not target_cid and client:
            for gid, cid in CHANNEL_MAP.values():
                guild = client.get_guild(gid)
                if guild and guild.get_member(target_id):
                    target_cid = cid
                    target_name = guild.get_member(target_id).display_name
                    break
        if not target_id:
            try:
                await context.bot.send_message(chat_id=update.message.chat_id,
                                               text=f"No Discord user matching '{arg}' found.")
            except Exception:
                pass
            return
        sender_entry = resolve_telegram_sender_entry(author)
        token = sender_entry.get("discord") if sender_entry else None
        mention = f"<@{target_id}>"
        payload = f"{mention} {msg}".strip() if msg else mention
        cmd_msg_id = update.message.message_id
        if token and target_cid:
            discord_id = await send_via_discord_bot(token, target_cid, payload, [])
            if discord_id:
                record_msg("discord", discord_id, target_cid, cmd_msg_id)
                record_msg("telegram", cmd_msg_id, TELEGRAM_CHAT_ID, discord_id)
            note = f"Pinged {target_name} on Discord (as your bot)."
        else:
            discord_id = await telegram_send_to_discord(display_name, payload)
            if discord_id:
                record_msg("discord", discord_id,
                           TELEGRAM_DISCORD_CHANNEL_ID or 0, cmd_msg_id)
                record_msg("telegram", cmd_msg_id, TELEGRAM_CHAT_ID, discord_id)
            note = f"Pinged {target_name} on Discord (via webhook)."
        try:
            await context.bot.send_message(chat_id=update.message.chat_id, text=note)
        except Exception as e:
            log(f"ping cmd telegram send error: {e}")
        return

    reply_discord_id = None
    if message.reply_to_message:
        reply_discord_id = get_counterpart(message.reply_to_message.message_id)

    attachments = []
    msg = message
    try:
        if msg.photo:
            f = await (await msg.photo[-1].get_file()).download_as_bytearray()
            attachments.append(("image.jpg", bytes(f), "image/jpeg"))
        if msg.sticker:
            st = msg.sticker
            f = await (await st.get_file()).download_as_bytearray()
            if getattr(st, "is_video", False):
                attachments.append(("sticker.webm", bytes(f), "video/webm"))
            elif getattr(st, "is_animated", False):
                try:
                    gif_bytes = await asyncio.to_thread(convert_tgs_to_gif, bytes(f))
                    attachments.append(("sticker.gif", gif_bytes, "image/gif"))
                except Exception as e:
                    log(f"tgs->gif convert error: {e}")
                    attachments.append(("sticker.tgs", bytes(f), "application/x-tgsticker"))
            else:
                attachments.append(("sticker.webp", bytes(f), "image/webp"))
            if not content:
                content = ""
        if msg.video:
            f = await (await msg.video.get_file()).download_as_bytearray()
            attachments.append(("video.mp4", bytes(f), "video/mp4"))
        if msg.animation:
            f = await (await msg.animation.get_file()).download_as_bytearray()
            attachments.append(("animation.mp4", bytes(f), "video/mp4"))
        if msg.document:
            f = await (await msg.document.get_file()).download_as_bytearray()
            attachments.append((msg.document.file_name or "file", bytes(f),
                               msg.document.mime_type or "application/octet-stream"))
    except Exception as e:
        log(f"Telegram media download error: {e}")

    avatar_url = await get_telegram_avatar(author.id)
    sender_entry = resolve_telegram_sender_entry(author)
    discord_token = get_discord_token(author.id) if getattr(author, "id", None) is not None else None
    if sender_entry and not discord_token:
        discord_token = sender_entry.get("discord")
    if discord_token in {"", None, "none", "no fucking idea"}:
        discord_token = None
    target_channel_id = None
    if TELEGRAM_DISCORD_CHANNEL_ID:
        target_channel_id = TELEGRAM_DISCORD_CHANNEL_ID
    elif CHANNEL_MAP.get("#generale"):
        target_channel_id = CHANNEL_MAP["#generale"][1]
    if discord_token and target_channel_id:
        discord_id = await send_via_discord_bot(discord_token, target_channel_id,
                                           content, attachments, reply_discord_id)
    else:
        discord_id = await telegram_send_to_discord(display_name, content, attachments, reply_discord_id, avatar_url)
    if discord_id:
        record_msg("telegram", msg.message_id, TELEGRAM_CHAT_ID, discord_id)
        record_msg("discord", discord_id, TELEGRAM_DISCORD_CHANNEL_ID or 0, msg.message_id)


class DiscordBot(discord.Client):
    def __init__(self):
        intents = discord.Intents.default()
        intents.message_content = True
        intents.members = True
        intents.presences = True
        intents.reactions = True
        super().__init__(intents=intents)

    async def on_ready(self):
        global webhook_cache
        log(f"Discord bot ready as {self.user}")
        log(f"Guilds available: {[g.name for g in self.guilds]}")
        try:
            for irc_ch, (gid, cid) in CHANNEL_MAP.items():
                log(f"Setting up webhook for {irc_ch} (guild={gid}, channel={cid})")
                guild = self.get_guild(gid)
                if not guild:
                    log(f"Guild {gid} not found. Available: {[(g.id, g.name) for g in self.guilds]}")
                    continue
                channel = guild.get_channel(cid)
                if not channel:
                    log(f"Channel {cid} not found in guild {guild.name}")
                    continue
                existing = [w for w in await channel.webhooks() if w.name == "IRC Bridge"]
                if existing:
                    webhook = existing[0]
                else:
                    webhook = await channel.create_webhook(name="IRC Bridge")
                webhook_cache[cid] = SyncWebhook.from_url(webhook.url)
                log(f"Webhook ready for #{channel.name}")
        except Exception as e:
            log(f"on_ready error: {e}")
            log(traceback.format_exc())

    async def on_message(self, message):
        if message.author == self.user or message.webhook_id:
            return
        is_bot = message.author.bot
        cid = message.channel.id
        irc_ch = DISCORD_IRC_MAP.get(cid)
        if not irc_ch:
            return

        if message.content and message.content.strip() == r"\online":
            text = telegram_online_text()
            try:
                await message.channel.send(text)
            except Exception as e:
                log(f"online cmd discord send error: {e}")
            telegram_send(text)
            return

        if message.content and message.content.strip().startswith(r"\ping"):
            rest = message.content.strip()[5:].strip()
            if not rest:
                try:
                    await message.channel.send("Usage: \\ping <telegram name> <message>")
                except Exception:
                    pass
                return
            parts = rest.split(None, 1)
            arg = parts[0].lstrip("@")
            msg = parts[1] if len(parts) > 1 else ""
            resolved_username = None
            resolved_name = None
            for uid, entry in TELEGRAM_USER_BOT_MAP.items():
                if (entry.get("telegram_username") or "").lower() == arg.lower():
                    resolved_username = entry.get("telegram_username")
                    resolved_name = entry.get("telegram_username")
                    break
            if not resolved_username:
                for uid, e in telegram_users.items():
                    if (e.get("username") or "").lower() == arg.lower() or \
                       (e.get("full_name") or "").lower() == arg.lower():
                        resolved_username = e.get("username")
                        resolved_name = e.get("full_name") or e.get("username")
                        break
            if not resolved_name and arg.lstrip("@").isdigit():
                e = telegram_users.get(arg.lstrip("@"))
                if e:
                    resolved_username = e.get("username")
                    resolved_name = e.get("full_name") or e.get("username")
            if not resolved_name and discord_bot_instance:
                for gid, cid in CHANNEL_MAP.values():
                    guild = discord_bot_instance.get_guild(gid)
                    if not guild:
                        continue
                    for m in guild.members:
                        if (m.display_name and m.display_name.lower() == arg.lower()) or \
                           (m.name and m.name.lower() == arg.lower()):
                            entry = TELEGRAM_USER_BOT_MAP.get(m.id)
                            if entry:
                                resolved_username = entry.get("telegram_username")
                                resolved_name = entry.get("telegram_username") or m.display_name
                                break
                    if resolved_name:
                        break
            if not resolved_name:
                cand = arg.lstrip("@")
                e = None
                if cand.isdigit():
                    e = telegram_users.get(cand)
                else:
                    for _uid, _e in telegram_users.items():
                        if (_e.get("username") or "").lower() == cand.lower():
                            e = _e
                            break
                resolved_username = cand
                resolved_name = (e.get("full_name") or cand) if e else cand
            if not resolved_username:
                try:
                    await message.channel.send(f"No Telegram user matching '{arg}' found.")
                except Exception:
                    pass
                return
            send_bot = None
            discord_token = get_discord_token(message.author.id)
            if discord_token:
                send_bot = telegram_app.bot
            if not send_bot:
                send_bot = get_user_bot(message.author.id) or telegram_app.bot
            mention = f"@{resolved_username}" if resolved_username else resolved_name
            payload = f"{mention} {msg}".strip() if msg else mention
            try:
                tg_id = telegram_send_media(payload, [], [], None, bot=send_bot)
                if tg_id:
                    record_msg("telegram", tg_id, TELEGRAM_CHAT_ID, message.id)
                    record_msg("discord", message.id, cid, tg_id)
                note = f"Pinged {resolved_name} on Telegram" + \
                       ("" if resolved_username else " (no @username, name only)")
            except Exception as e:
                log(f"ping cmd discord->telegram error: {e}")
                note = f"Failed to ping {resolved_name} on Telegram: {e}"
            try:
                await message.channel.send(note)
            except Exception:
                pass
            return

        if is_bot:
            if message.reference and message.reference.message_id:
                ref_tg = get_counterpart(message.reference.message_id)
                if ref_tg is not None:
                    record_msg("discord", message.id, cid, ref_tg)
            return

        nick = message.author.display_name
        log(f"Discord incoming from {nick} (id={message.author.id}) in {irc_ch}, mapped_bot={'yes' if get_user_bot(message.author.id) else 'no'}")
        reply_info = None
        if message.reference and message.reference.resolved:
            ref = message.reference.resolved
            if isinstance(ref, discord.Message):
                reply_info = f"{ref.author.display_name}: {ref.clean_content}"

        text_bits = []
        if message.clean_content:
            text_bits.append(message.clean_content)

        text_bits, photos, videos = prepare_discord_attachments(message, text_bits)

        try:
            relay_discord_to_telegram(message, nick, text_bits, photos, videos, cid)
        except Exception as e:
            log(f"Discord->Telegram relay error: {e}")

    async def on_message_edit(self, before, after):
        if after.author == self.user or after.webhook_id:
            return
        cid = after.channel.id
        if cid not in DISCORD_IRC_MAP:
            return
        if after.author.bot:
            return
        nick = after.author.display_name
        text_bits = [after.clean_content or ""]
        if not text_bits[0]:
            text_bits = [_extract_message_text(after)]
        try:
            relay_discord_edit_to_telegram(after, nick, text_bits, cid)
        except Exception as e:
            log(f"Discord edit relay error: {e}")

    async def on_raw_message_edit(self, payload):
        cid = payload.channel_id
        if cid not in DISCORD_IRC_MAP:
            return
        data = payload.data
        if "content" not in data:
            return
        content = data.get("content", "") or ""
        if not content.strip():
            return
        if payload.cached_message:
            if payload.cached_message.author.bot:
                return
            author_id = payload.cached_message.author.id
        else:
            author = data.get("author", {})
            if author.get("bot", False):
                return
            author_id = int(author.get("id", 0))
            if not author_id:
                return
        mock_msg = type("MockMsg", (), {"id": payload.message_id, "author": type("MockAuthor", (), {"id": author_id})()})()
        try:
            relay_discord_edit_to_telegram(mock_msg, "Unknown", [content], cid)
        except Exception as e:
            log(f"Discord raw edit relay error: {e}")

    async def on_raw_reaction_add(self, payload):
        if payload.user_id == self.user.id:
            return
        channel = self.get_channel(payload.channel_id)
        if not channel:
            return
        if getattr(channel, "id", None) not in DISCORD_IRC_MAP:
            return
        message = None
        try:
            message = await channel.fetch_message(payload.message_id)
        except Exception as e:
            log(f"Discord reaction fetch error: {e}")
            return
        if message.author.bot:
            return
        try:
            relay_discord_reaction_to_telegram(message, payload.emoji, remove=False)
        except Exception as e:
            log(f"Discord reaction relay error: {e}")

    async def on_raw_reaction_remove(self, payload):
        if payload.user_id == self.user.id:
            return
        channel = self.get_channel(payload.channel_id)
        if not channel:
            return
        if getattr(channel, "id", None) not in DISCORD_IRC_MAP:
            return
        message = None
        try:
            message = await channel.fetch_message(payload.message_id)
        except Exception as e:
            log(f"Discord reaction remove fetch error: {e}")
            return
        if message.author.bot:
            return
        try:
            relay_discord_reaction_to_telegram(message, payload.emoji, remove=True)
        except Exception as e:
            log(f"Discord reaction remove relay error: {e}")



def telegram_run():
    global telegram_app
    if not TELEGRAM_BOT_TOKEN:
        log("No TELEGRAM_BOT_TOKEN set, skipping Telegram relay")
        return
    telegram_app = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
    telegram_app.add_handler(MessageHandler(filters.ALL, telegram_message_handler))
    log("Starting Telegram relay")

    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    global telegram_loop
    telegram_loop = loop
    set_telegram_app(telegram_app, telegram_loop)
    try:
        loop.run_until_complete(telegram_app.initialize())
        loop.run_until_complete(telegram_app.start())
        resolve_discord_channel_id()
        loop.run_until_complete(telegram_app.updater.start_polling())
        loop.run_forever()
    except (KeyboardInterrupt, SystemExit):
        pass
    except Exception as e:
        log(f"Telegram run error: {e}")
    finally:
        try:
            if telegram_app.updater and telegram_app.updater.running:
                loop.run_until_complete(telegram_app.updater.stop())
        except Exception:
            pass
        try:
            loop.run_until_complete(telegram_app.stop())
        except Exception:
            pass
        try:
            loop.run_until_complete(telegram_app.shutdown())
        except Exception:
            pass
        try:
            loop.run_until_complete(close_discord_sessions())
        except Exception:
            pass


def discord_run():
    global discord_bot_instance
    bot = DiscordBot()
    discord_bot_instance = bot
    bot.run(DISCORD_TOKEN, log_handler=None)


def main():
    load_pfp()
    load_telegram_users()
    load_message_history()
    t = threading.Thread(target=discord_run, daemon=True)
    t.start()
    tt = threading.Thread(target=telegram_run, daemon=True)
    tt.start()
    time.sleep(3)
    while True:
        time.sleep(60)


if __name__ == "__main__":
    raise SystemExit(main())
