#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ربات بازی «پت‌های انیمه‌ای» — تک‌فایلی، پایتون
نیازمندی: python-telegram-bot >= 22.7   (pip install "python-telegram-bot>=22.7")

امکانات (نسخهٔ به‌روزشده):
- به هر کاربر جدید یکی از ایموجی‌های پرمیوم (کاراکتر) به صورت رندوم داده می‌شود.
- کاربر برای کاراکترش اسم می‌گذارد، آن را با «غذا دادن» بزرگ می‌کند، سطح و قدرت می‌گیرد.
- سیستم یخچال: غذا باید از سوپرمارکت خریده و در یخچال نگه‌داری بشه، برای غذا دادن مصرف می‌شه.
- سوپرمارکت: بخش غذا / اسلحه / لباس — هر آیتم یک ردیف با ۳ دکمه (اسم | قیمت | خرید).
- اسلحه قدرت حمله و لباس قدرت دفاع کاراکتر رو تو مبارزه بالا می‌بره (سطح‌بندی ضعیف تا قوی).
- ارز داخل بازی: «فمبوی پوینت» — از مبارزه (برد = ۱۰ تا) و کمی هم از غذا دادن به‌دست میاد.
- مبارزه بین کاربران (PvP رندوم) با احتساب قدرت کاراکتر + اسلحه/لباس.
- بازی «دوز (XO)» و «سنگ‌کاغذقیچی» بین دو کاربر، هم داخل پیوی ربات (صف انتظار) و هم داخل گروه (چالش/پیوستن).
- لیدربورد، بخش VIP.
- دکمه‌های رنگی (style=primary/success/danger) و آیکون ایموجی پرمیوم روی دکمه‌ها (Bot API 9.4).
- جوین اجباری چند کاناله قابل افزودن/حذف توسط ادمین.
- پنل ادمین کامل: مدیریت کاراکترها، ویرایش متن‌ها، ویرایش دکمه‌ها (متن/رنگ/آیکون/فعال-غیرفعال)،
  ویرایش آیکون آیتم‌های فروشگاه، لیست کاربران به صورت صفحه‌بندی، دادن امتیاز دستی، آمار، پیام همگانی.

نکتهٔ فنی مهم:
برای نمایش ایموجی پرمیوم داخل متن پیام، تلگرام نیاز به یک کاراکتر placeholder (همان اموجی fallback)
به‌همراه یک MessageEntity از نوع custom_emoji دارد؛ به همین دلیل وقتی ادمین کاراکتر جدید اضافه می‌کند،
باید خود ایموجی پرمیوم را در چت به ربات بفرستد تا ربات هم custom_emoji_id و هم placeholder را استخراج کند.
برای آیکون روی دکمه‌ها هم از پارامتر icon_custom_emoji_id استفاده می‌شود (هر دو، از Bot API 9.4 /
python-telegram-bot v22.7 به بعد در دسترس‌اند و به پرمیوم بودن اکانت صاحب ربات نیاز دارند).
"""

import asyncio
import json
import logging
import os
import random
import string
import time
from datetime import datetime, timezone

from telegram import (
    InlineKeyboardButton,
    InlineKeyboardMarkup,
    Message,
    MessageEntity,
    Update,
)
from telegram.constants import ChatMemberStatus, ChatType, ParseMode
from telegram.error import BadRequest, Forbidden
from telegram.ext import (
    Application,
    CallbackQueryHandler,
    CommandHandler,
    ContextTypes,
    ConversationHandler,
    MessageHandler,
    filters,
)

# ----------------------------------------------------------------------------
# تنظیمات ثابت
# ----------------------------------------------------------------------------
BOT_TOKEN = "8734310434:AAHvsVFbWHiX2C0bgIOLwBkOZhad9WSNlYA"
ADMIN_ID = 8155895113

DATA_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bot_data.json")

LEVEL_XP_BASE = 100
LEVEL_XP_GROWTH = 1.22

FEED_COOLDOWN = 5 * 60            # ۵ دقیقه
BATTLE_COOLDOWN = 30 * 60         # ۳۰ دقیقه

BATTLE_POINTS_WIN = 10
BATTLE_XP_WIN = 22
BATTLE_XP_LOSE = 6

VIP_POWER_MULT = 1.15
VIP_FEED_MULT = 1.25

USERS_PER_PAGE = 8

CURRENCY_NAME = "فمبوی پوینت"
DONATE_URL = "https://daramet.com/Qufel"

GAME_NAMES_FA = {"xo": "دوز (XO)", "rps": "سنگ‌کاغذقیچی", "dice": "تاس دوئل"}
GAME_IDLE_TIMEOUT = 5 * 60  # ۵ دقیقه بی‌فعالیتی -> بستن خودکار بازی
GROUP_PANEL_TIMEOUT = 3 * 60  # ۳ دقیقه بی‌فعالیتی -> بستن خودکار پنل «فمبوی» تو گروه

logging.basicConfig(
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
logger = logging.getLogger("anime_pet_bot")

# این کتابخونه‌ها به‌ازای هر درخواست تلگرام (که با یه ربات پرکاربر خیلی زیاده: هر
# getUpdates/sendMessage/editMessageText/answerCallbackQuery) یه خط لاگ INFO می‌نویسن.
# این حجم لاگ روی هاست‌های کم‌منبع (CPU/RAM/دیسک محدود) خودش یه منبع مصرف مداوم
# CPU و I/O می‌شه و می‌تونه یکی از دلایل کشته شدن مکرر پروسه توسط هاست باشه؛
# سطحشون رو می‌بریم بالا تا فقط خطاهای واقعی لاگ بشن.
for noisy_logger in ("httpx", "httpcore", "apscheduler", "telegram", "telegram.ext"):
    logging.getLogger(noisy_logger).setLevel(logging.WARNING)

# ----------------------------------------------------------------------------
# آیتم‌های فروشگاه (غذا / اسلحه / لباس)
# ----------------------------------------------------------------------------
DEFAULT_FOOD_ITEMS = [
    {"key": "food_apple", "name": "🍎 سیب", "price": 5, "xp": 15, "points": 2},
    {"key": "food_fish", "name": "🐟 ماهی", "price": 10, "xp": 25, "points": 3},
    {"key": "food_meat", "name": "🍖 گوشت", "price": 15, "xp": 35, "points": 4},
    {"key": "food_cake", "name": "🍰 کیک ویژه", "price": 30, "xp": 60, "points": 8},
]

DEFAULT_WEAPON_ITEMS = [
    {"key": "weapon_stick", "name": "🥢 چوب", "price": 20, "atk": 5, "level": 1},
    {"key": "weapon_sword", "name": "🗡 شمشیر", "price": 60, "atk": 15, "level": 2},
    {"key": "weapon_axe", "name": "🪓 تبر جنگی", "price": 120, "atk": 28, "level": 3},
    {"key": "weapon_legend", "name": "⚔️ شمشیر افسانه‌ای", "price": 250, "atk": 45, "level": 4},
]

DEFAULT_CLOTHES_ITEMS = [
    {"key": "cloth_shirt", "name": "👕 پیراهن ساده", "price": 20, "defense": 5, "level": 1},
    {"key": "cloth_leather", "name": "🛡 زره چرمی", "price": 60, "defense": 15, "level": 2},
    {"key": "cloth_iron", "name": "🥋 زره آهنی", "price": 120, "defense": 28, "level": 3},
    {"key": "cloth_legend", "name": "👑 زره افسانه‌ای", "price": 250, "defense": 45, "level": 4},
]


def find_item(items_list, key):
    for it in items_list:
        if it["key"] == key:
            return it
    return None


def get_shop_items(category):
    """لیست زندهٔ آیتم‌های فروشگاه (غذا/اسلحه/لباس) که ادمین می‌تونه ویرایششون کنه."""
    data = load_data()
    return data["shop"][category]


def eligible_characters(is_vip, exclude_id=None):
    """کاراکترهایی که یه کاربر (بسته به VIP بودنش) مجاز به گرفتنشونه به‌صورت رایگان/رندومه.
    کاراکترهای پولی (price > 0) هیچ‌وقت رندوم داده نمی‌شن، فقط باید خریده بشن."""
    data = load_data()
    pool = data["characters"] if is_vip else [c for c in data["characters"] if not c.get("vip_only")]
    pool = [c for c in pool if not c.get("price")]
    if exclude_id is not None:
        pool = [c for c in pool if c["id"] != exclude_id]
    return pool


def grant_character(user, char_id):
    """کاراکتر رو به‌عنوان کاراکتر فعلی کاربر ثبت می‌کنه و برای همیشه به «کاراکترهای من»ش اضافه می‌کنه
    (تا هروقت خواست بتونه رایگان بین کاراکترهایی که یه‌بار داشته سوییچ کنه)."""
    user["character_id"] = char_id
    if char_id not in user.get("owned_characters", []):
        user.setdefault("owned_characters", []).append(char_id)


# ----------------------------------------------------------------------------
# دیتای پیش‌فرض
# ----------------------------------------------------------------------------
DEFAULT_TEXTS = {
    "welcome": "به دنیای پت‌های انیمه‌ای خوش اومدی! 🌸\nاینجا یه کاراکتر مخصوص خودت می‌گیری، بزرگش می‌کنی، قدرتمندش می‌کنی و می‌تونی با بقیه مبارزه کنی.",
    "force_join_prompt": "برای استفاده از ربات باید اول تو کانال(های) زیر عضو بشی 👇\nبعد از عضویت رو دکمهٔ «✅ عضو شدم» بزن.",
    "not_joined_yet": "هنوز تو همهٔ کانال‌ها عضو نشدی! لطفاً عضو شو و دوباره امتحان کن.",
    "no_characters_available": "فعلاً هیچ کاراکتری تو ربات تعریف نشده. به ادمین اطلاع بده تا کاراکتر اضافه کنه.",
    "assign_character": "یه کاراکتر جدید بهت داده شد! 🎉\nحالا براش یه اسم انتخاب کن (فقط کافیه اسمش رو برام بفرستی):",
    "name_set_confirm": "اسم کاراکترت با موفقیت ثبت شد ✅\nحالا می‌تونی از منوی اصلی باهاش بازی کنی.",
    "profile_template": "پروفایل {name}\n\nسطح: {level}\nتجربه: {xp} / {next_xp}\n{progress_bar} {percent}%\nقدرت پایه: {power}\nاسلحه: {weapon}\nلباس: {clothes}\n{currency_name}: {points}\nبرد / باخت: {wins} / {losses}\nوضعیت: {vip_badge}",
    "feed_success": "بهش غذا دادی! 🍖\n+{xp} تجربه | +{points} " + CURRENCY_NAME,
    "feed_cooldown": "کاراکترت سیره! ⏳ {minutes} دقیقهٔ دیگه دوباره می‌تونی بهش غذا بدی.",
    "feed_no_food": "یخچالت خالیه! 🧊 اول باید از سوپرمارکت غذا بخری.",
    "level_up": "🎊 تبریک! کاراکترت رفت سطح {level}!\nقدرت جدید: {power}",
    "battle_no_opponent": "فعلاً حریفی برای مبارزه پیدا نشد، بعداً دوباره امتحان کن.",
    "battle_cooldown": "برای مبارزهٔ بعدی باید {minutes} دقیقه صبر کنی.",
    "battle_win": "🏆 بردی!\nحریف: {opponent_name}\n+{points} " + CURRENCY_NAME + " | +{xp} تجربه",
    "battle_lose": "😵 این دفعه باختی...\nحریف: {opponent_name}\n+{xp} تجربه (تلاش قابل تقدیره!)",
    "help_text": "راهنمای ربات:\n🍖 یخچال: غذاهاتو ببین و به کاراکترت بده.\n🛒 سوپرمارکت: غذا/اسلحه/لباس بخر.\n🎒 کوله‌پشتی: اسلحه و لباست رو تجهیز کن.\n⚔️ مبارزه: با یه کاربر رندوم دیگه مبارزه کن.\n👤 پروفایل: وضعیت کاراکترت رو ببین.\n🏆 لیدربورد: برترین بازیکن‌ها.\n🎮 بازی: دوز یا سنگ‌کاغذقیچی با یه کاربر دیگه.\n💎 VIP: امکانات ویژه.",
    "vip_info_vip": "تو یه کاربر VIP هستی! 💎\nسرعت رشد و قدرت کاراکترت بیشتره.",
    "vip_info_not_vip": "هنوز VIP نیستی. برای گرفتن VIP با ادمین در ارتباط باش. 💎",
    "admin_welcome": "به پنل مدیریت خوش اومدی 👑",
    # ---- تغییر اسم / تعویض کاراکتر ----
    "rename_char_prompt": "اسم جدید کاراکترت رو بفرست:",
    "rename_char_success": "✅ اسم کاراکترت به «{name}» تغییر کرد.",
    "swap_char_menu": "چطور می‌خوای کاراکترت رو عوض کنی؟",
    "swap_char_random_success": "کاراکترت عوض شد! 🎉",
    "swap_char_admin_sent": "درخواستت برای ادمین ارسال شد. صبر کن تا ادمین یه کاراکتر جدید برات بفرسته. ⏳",
    "swap_char_admin_notify": "📩 کاربر {user_name} (آیدی: {user_id}) درخواست تعویض کاراکتر داده.\nبرای پاسخ دادن روی دکمهٔ زیر بزن و بعد ایموجی پرمیوم جدید رو بفرست.",
    "swap_char_admin_done": "✅ کاراکتر جدید برای {user_name} ثبت شد.",
    "swap_char_user_notify": "🎉 ادمین برات یه کاراکتر جدید فرستاد!",
    # ---- بازی‌ها ----
    "game_waiting_pm": "⏳ منتظر یه حریف برای «{game_name}» هستیم... هر وقت یکی دیگه همین بازی رو انتخاب کنه، بازی شروع می‌شه.",
    "game_matched_pm": "حریف پیدا شد! بازی شروع شد 🎮",
    "game_group_new": "🎮 {host_name} بازی «{game_name}» رو انتخاب کرد!\nمنتظر یه حریفه... برای پیوستن دکمهٔ زیر رو بزن.",
    "game_group_joined": "پیوستی! بازی شروع شد 🎮",
    "xo_turn_status": "❌⭕ دوز — {p1} (❌) در برابر {p2} (⭕)\nنوبت: {turn}",
    "xo_draw": "🤝 بازی مساوی شد!",
    "xo_win": "🏆 برنده: {winner}",
    "rps_prompt": "✊✋✌️ سنگ‌کاغذقیچی — {p1} در برابر {p2}\nانتخابت رو بزن:",
    "rps_draw": "🤝 مساوی شد! هر دو {choice} انتخاب کردید.",
    "rps_win": "🏆 برنده: {winner}",
    "game_timeout": "⌛️ این بازی به‌خاطر عدم فعالیت بیش از ۵ دقیقه بسته شد.",
    "game_private_only": "این گزینه فقط تو پیوی ربات کار می‌کنه.",
    "game_already_playing": "تو الان وسط یه بازی دیگه‌ای!",
    "game_pick_prompt": "🎮 یه بازی انتخاب کن:",
    "game_group_pick_prompt": "🎮 یه بازی انتخاب کن تا منتظر حریف بمونیم:",
    # ---- مدیریت فروشگاه (ادمین) ----
    "admin_shop_add_name": "اسم آیتم جدید رو بفرست (می‌تونی ایموجی معمولی یا پرمیوم هم داخلش بذاری):",
    "admin_shop_add_price": "قیمت این آیتم رو به {currency_name} بفرست (فقط عدد):",
    "admin_shop_add_stat": "مقدار {stat_name} این آیتم رو بفرست (فقط عدد):",
    "admin_shop_add_done": "✅ آیتم «{name}» با موفقیت به فروشگاه اضافه شد.",
    "admin_shop_edit_done": "✅ آیتم به‌روزرسانی شد.",
    "admin_shop_delete_done": "🗑 آیتم از فروشگاه حذف شد.",
    # ---- پارتنر ----
    "partner_no_partner": "💞 تو الان پارتنر نداری.\nبرای پیدا کردن پارتنر، لینک زیر رو به کسی که می‌خوای پارتنرت باشه بده تا با همون لینک /start بزنه:\n{ref_link}",
    "partner_has_partner": "💞 پارتنر تو: {partner_name}\nسطح پارتنر: {partner_level}",
    "partner_self_link": "نمی‌تونی با لینک خودت پارتنر خودت بشی!",
    "partner_already_this": "شما همین الان هم پارتنر همدیگه‌اید. 💞",
    "partner_you_have_one": "تو همین الان پارتنر داری! اگه می‌خوای پارتنرت رو عوض کنی، اول باید طلاق بگیری. 💔",
    "partner_cheat_alert": "🚨 پارتنرت ({name}) داره یه پارتنر دیگه اضافه می‌کنه (خیانت)!",
    "partner_third_party": "این دو نفر همین الان با هم تو رابطه‌ان 💞\n{p1_name} + {p2_name}",
    "partner_registered_you": "🎉 تبریک! حالا با {partner_name} پارتنر شدید 💞",
    "partner_registered_other": "🎉 تبریک! {partner_name} با لینک تو پارتنرت شد 💞",
    "divorce_request_sent": "درخواست طلاقت برای پارتنرت ارسال شد. منتظر پاسخش باش. 💔",
    "divorce_request_notify": "💔 پارتنرت ({name}) درخواست طلاق داده. قبول می‌کنی؟",
    "divorce_accepted_requester": "پارتنرت درخواست طلاقت رو قبول کرد. رابطه‌تون تموم شد. 💔",
    "divorce_accepted_other": "طلاق گرفتید. رابطه‌تون تموم شد. 💔",
    "divorce_rejected": "پارتنرت نمی‌خواد طلاق بگیره. رابطه‌تون ادامه داره. 💞",
    "divorce_no_partner": "تو الان پارتنر نداری که بخوای ازش طلاق بگیری.",
    "divorce_already_pending": "یه درخواست طلاق دیگه از قبل در جریانه.",
    # ---- بچه‌دار شدن ----
    "child_no_partner": "برای بچه‌دار شدن اول باید پارتنر داشته باشی. 💞",
    "child_already_have": "شما همین الان هم یه بچه دارید 👶",
    "child_no_characters": "فعلاً کاراکتری برای بچه تعریف نشده، به ادمین بگو یه کاراکتر اضافه کنه.",
    "child_born": "🎉 تبریک! صاحب یه بچهٔ جدید شدید 👶\nاسمش رو انتخاب کنید (پیام بعدی رو بفرستید):",
    "child_named": "✅ اسم بچه‌تون «{name}» ثبت شد.",
    "child_status": "👶 بچهٔ {p1_name} و {p2_name}\nاسم: {name}\nسطح: {level}\nتجربه: {xp}/{next_xp}\nقدرت: {power}",
    "child_feed_success": "به بچه غذا دادید! +{xp} تجربه",
    "child_feed_cooldown": "بچه سیره! ⏳ {minutes} دقیقهٔ دیگه دوباره بهش غذا بدید.",
    "child_feed_no_food": "نه شما و نه پارتنرتون تو یخچالتون غذا ندارید!",
    "child_level_up": "🎊 بچه رفت سطح {level}! قدرتش شد {power}.",
    # ---- رول‌پلی ----
    "rp_start": "💬 چت رول‌پلی با پارتنرت شروع شد! از این به بعد هر پیامی که بفرستی مستقیم برای پارتنرت می‌ره (به‌نام کاراکترت). برای پایان دادن، دوباره روی دکمهٔ رول‌پلی بزن.",
    "rp_stop": "💬 چت رول‌پلی تموم شد.",
    "rp_no_partner": "برای رول‌پلی اول باید پارتنر داشته باشی. 💞",
    # ---- مبارزهٔ سه‌نفره + انیمیشن ----
    "battle_intro": "⚔️ مبارزه شروع شد!\n\n{p1_name}\nاسلحه: {p1_weapon}\nلباس: {p1_clothes}\n\nدر برابر\n\n{p2_name}\nاسلحه: {p2_weapon}\nلباس: {p2_clothes}\n\n🥊 مبارزه در جریانه...",
    "battle_round": "⚔️ مبارزه در جریانه...\n\n{p1_name} در برابر {p2_name}\n\n🥊 {actor} یه ضربهٔ محکم زد!",
    "battle_family_note": "\n(پارتنر و بچه‌ش هم تو این مبارزه کمکش کردن 👨‍👩‍👦)",
    # ---- تاس دوئل ----
    "dice_prompt": "🎲 تاس دوئل — {p1} در برابر {p2}\nامتیاز: {score1} - {score2} (راند {round})\nهر دو نفر باید تاس بندازن:",
    "dice_waiting_other": "🎲 تاسـت رو انداختی ({value})! منتظر حریف باش...",
    "dice_round_tie": "🎲 این راند مساوی شد ({value} - {value})! دوباره پرتاب کنید.",
    "dice_round_result": "🎲 نتیجهٔ راند: {p1} ({v1}) در برابر {p2} ({v2}) — برنده این راند: {round_winner}",
    "dice_win": "🏆 برندهٔ نهایی: {winner}",
    # ---- کلیک سریع (گروه) ----
    "speedclick_prompt": "⚡️ اولین نفری که رو دکمه بزنه، جایزهٔ نقدی می‌بره!",
    "speedclick_won": "🎉 {name} زودتر از همه کلیک کرد و {amount} " + CURRENCY_NAME + " گرفت!",
    "speedclick_too_late": "دیر کردی! یکی دیگه زودتر کلیک کرد.",
    # ---- فمبوی کوچولو ----
    "fambo_kid_reward": "🎁 {amount} " + CURRENCY_NAME + " گرفتی!",
    "fambo_kid_cooldown": "⏳ فقط هر ۵ دقیقه یه‌بار می‌تونی این جمله رو بگی. {minutes} دقیقهٔ دیگه دوباره امتحان کن.",
    # ---- قمار (بازی با ورودی/شرط‌بندی) ----
    "gamble_ask_stake": "🎰 چند تا {currency_name} ورودی این بازی باشه؟ یه عدد بفرست:",
    "gamble_invalid_stake": "لطفاً فقط یه عدد مثبت بفرست.",
    "gamble_insufficient_funds": "😅 برای این ورودی ({amount} " + CURRENCY_NAME + ") پول کافی نداری.",
    "gamble_waiting": "🎰 {host_name} یه قمار «{game_name}» با ورودی {stake} {currency_name} راه انداخته!\nهرکی می‌خواد بپیوندی، دکمهٔ زیر رو بزن (ورودیت از حسابت کم می‌شه):",
    # ---- انتقال فمبوی پوینت ----
    "transfer_ask_amount": "چند تا " + CURRENCY_NAME + " می‌خوای به {target_name} بفرستی؟ یه عدد بفرست:",
    "transfer_invalid_amount": "لطفاً فقط یه عدد مثبت بفرست.",
    "transfer_insufficient_funds": "😅 {amount} " + CURRENCY_NAME + " نداری که بفرستی.",
    "transfer_success_sender": "✅ {amount} " + CURRENCY_NAME + " به {target_name} فرستادی.",
    "transfer_success_receiver": "🎁 {sender_name} بهت {amount} " + CURRENCY_NAME + " فرستاد!",
    "transfer_no_reply": "باید رو پیام همون کسی که می‌خوای بهش بفرستی ریپلای بزنی و بگی «انتقال فمبوی پوینت».",
    "transfer_self": "نمی‌تونی به خودت انتقال بدی!",
    "transfer_target_not_registered": "این کاربر هنوز تو ربات ثبت‌نام نکرده (باید یه‌بار پیوی ربات /start بزنه).",
    # ---- راهنما ----
    "help_group_text": (
        "📖 راهنمای گروه:\n\n"
        "🔸 فمبوی — پنل شخصی خودت (پروفایل، یخچال، سوپرمارکت، بازی‌ها، پارتنر و...) رو باز می‌کنه؛ فقط خودت می‌تونی دکمه‌هاشو بزنی.\n"
        "🔸 بازی — یه بازی رایگان (دوز/سنگ‌کاغذقیچی/تاس) با یکی دیگه شروع می‌کنی.\n"
        "🔸 قمار — مثل بازی، ولی با ورودی؛ برنده هر دو ورودی رو می‌بره.\n"
        "🔸 فمبوی کوچولو — هر ۵ دقیقه یه‌بار، جایزهٔ رندوم " + CURRENCY_NAME + " می‌گیری.\n"
        "🔸 (روی پیام کسی ریپلای بزن و بگو) انتقال فمبوی پوینت — بهش " + CURRENCY_NAME + " هدیه می‌دی.\n"
        "🔸 راهنما — همین پیام رو دوباره نشون می‌ده.\n\n"
        "هر ۱۰ دقیقه هم یه دکمهٔ ⚡جایزهٔ سریع تو گروه میاد؛ اولین کلیک برنده‌ست!"
    ),
}

DEFAULT_BUTTONS = {
    "feed": {"text": "🍖 یخچال", "style": "success", "icon": None, "enabled": True},
    "battle": {"text": "⚔️ مبارزه", "style": "danger", "icon": None, "enabled": True},
    "profile": {"text": "👤 پروفایل من", "style": "primary", "icon": None, "enabled": True},
    "leaderboard": {"text": "🏆 لیدربورد", "style": "primary", "icon": None, "enabled": True},
    "vip": {"text": "💎 VIP", "style": "primary", "icon": None, "enabled": True},
    "help": {"text": "❓ راهنما", "style": "primary", "icon": None, "enabled": True},
    "donate": {"text": "❤️ حمایت از ربات", "style": "success", "icon": None, "enabled": True},
    "market": {"text": "🛒 سوپرمارکت", "style": "success", "icon": None, "enabled": True},
    "inventory": {"text": "🎒 کوله‌پشتی", "style": "primary", "icon": None, "enabled": True},
    "games": {"text": "🎮 بازی", "style": "danger", "icon": None, "enabled": True},
    "rename_char": {"text": "✏️ تغییر اسم کاراکتر", "style": "primary", "icon": None, "enabled": True},
    "swap_char": {"text": "🔄 تعویض کاراکتر", "style": "danger", "icon": None, "enabled": True},
    "swap_random": {"text": "🎲 انتخاب رندوم", "style": "success", "icon": None, "enabled": True},
    "swap_admin": {"text": "👑 درخواست از ادمین", "style": "primary", "icon": None, "enabled": True},
    "admin_answer_charreq": {"text": "📩 پاسخ به درخواست", "style": "success", "icon": None, "enabled": True},
    "check_join": {"text": "✅ عضو شدم", "style": "success", "icon": None, "enabled": True},
    "back": {"text": "🔙 بازگشت", "style": "primary", "icon": None, "enabled": True},
    "cancel": {"text": "❌ انصراف", "style": "danger", "icon": None, "enabled": True},
    "admin_panel": {"text": "👑 پنل ادمین", "style": "primary", "icon": None, "enabled": True},
    "admin_channels": {"text": "📢 کانال‌های جوین اجباری", "style": "primary", "icon": None, "enabled": True},
    "admin_chars": {"text": "🎭 مدیریت کاراکترها", "style": "primary", "icon": None, "enabled": True},
    "admin_texts": {"text": "✏️ ویرایش متن‌ها", "style": "primary", "icon": None, "enabled": True},
    "admin_buttons_menu": {"text": "🔘 ویرایش دکمه‌ها", "style": "primary", "icon": None, "enabled": True},
    "admin_shop_icons": {"text": "🖼 آیکون آیتم‌های فروشگاه", "style": "primary", "icon": None, "enabled": True},
    "admin_users": {"text": "👥 لیست کاربران", "style": "primary", "icon": None, "enabled": True},
    "admin_stats": {"text": "📊 آمار ربات", "style": "primary", "icon": None, "enabled": True},
    "admin_broadcast": {"text": "📣 پیام همگانی", "style": "primary", "icon": None, "enabled": True},
    "admin_add": {"text": "➕ افزودن", "style": "success", "icon": None, "enabled": True},
    "admin_delete": {"text": "🗑 حذف", "style": "danger", "icon": None, "enabled": True},
    "admin_channel_add": {"text": "➕ افزودن کانال جدید", "style": "success", "icon": None, "enabled": True},
    "admin_char_add": {"text": "➕ افزودن یک کاراکتر (دستی)", "style": "success", "icon": None, "enabled": True},
    "admin_char_import_pack": {"text": "📥 وارد کردن کل یک پک ایموجی", "style": "success", "icon": None, "enabled": True},
    "admin_char_vip_yes": {"text": "🌟 بله، مخصوص VIP باشه", "style": "danger", "icon": None, "enabled": True},
    "admin_char_vip_no": {"text": "👤 نه، برای همه باشه", "style": "primary", "icon": None, "enabled": True},
    "admin_user_chat": {"text": "💬 چت با این کاربر", "style": "success", "icon": None, "enabled": True},
    "swap_vip": {"text": "🌟 کاراکترهای VIP", "style": "danger", "icon": None, "enabled": True},
    "my_characters": {"text": "🎭 کاراکترهای من", "style": "primary", "icon": None, "enabled": True},
    "market_character": {"text": "🎭 کاراکتر ویژه", "style": "danger", "icon": None, "enabled": True},
    "admin_char_price": {"text": "✏️ قیمت", "style": "primary", "icon": None, "enabled": True},
    "select_char": {"text": "✅ انتخاب", "style": "success", "icon": None, "enabled": True},
    # سوپرمارکت
    "market_food": {"text": "🍔 غذا", "style": "success", "icon": None, "enabled": True},
    "market_weapon": {"text": "🗡 اسلحه", "style": "danger", "icon": None, "enabled": True},
    "market_clothes": {"text": "👕 لباس", "style": "primary", "icon": None, "enabled": True},
    "buy": {"text": "🛒 خرید", "style": "success", "icon": None, "enabled": True},
    "feed_item": {"text": "🍖 تغذیه", "style": "success", "icon": None, "enabled": True},
    "sell_item": {"text": "💰 فروش", "style": "danger", "icon": None, "enabled": True},
    "equip_item": {"text": "🛡 تجهیز", "style": "success", "icon": None, "enabled": True},
    "unequip_item": {"text": "🚫 خلع تجهیز", "style": "danger", "icon": None, "enabled": True},
    "inv_weapons": {"text": "🗡 اسلحه‌های من", "style": "primary", "icon": None, "enabled": True},
    "inv_clothes": {"text": "👕 لباس‌های من", "style": "primary", "icon": None, "enabled": True},
    # بازی‌ها
    "game_xo": {"text": "❌⭕ دوز (XO)", "style": "primary", "icon": None, "enabled": True},
    "game_rps": {"text": "✊✋✌️ سنگ‌کاغذقیچی", "style": "danger", "icon": None, "enabled": True},
    "game_join": {"text": "🤝 پیوستن به بازی", "style": "success", "icon": None, "enabled": True},
    "game_cancel_queue": {"text": "🚫 لغو انتظار", "style": "danger", "icon": None, "enabled": True},
    # مدیریت فروشگاه
    "admin_shop_manage": {"text": "🛍 مدیریت سوپرمارکت", "style": "primary", "icon": None, "enabled": True},
    "admin_shop_add": {"text": "➕ افزودن آیتم جدید", "style": "success", "icon": None, "enabled": True},
    "admin_shop_edit": {"text": "✏️ ویرایش", "style": "primary", "icon": None, "enabled": True},
    "admin_shop_delete": {"text": "🗑 حذف", "style": "danger", "icon": None, "enabled": True},
    # پارتنر
    "partner": {"text": "💞 پارتنر", "style": "danger", "icon": None, "enabled": True},
    "partner_game": {"text": "🎮 بازی با پارتنر", "style": "primary", "icon": None, "enabled": True},
    "partner_rp": {"text": "💬 رول‌پلی با پارتنر", "style": "success", "icon": None, "enabled": True},
    "partner_child": {"text": "👶 بچه‌دار شدن", "style": "success", "icon": None, "enabled": True},
    "partner_child_view": {"text": "👶 مشاهدهٔ بچه", "style": "primary", "icon": None, "enabled": True},
    "partner_child_feed": {"text": "🍼 تغذیهٔ بچه", "style": "success", "icon": None, "enabled": True},
    "partner_divorce": {"text": "💔 درخواست طلاق", "style": "danger", "icon": None, "enabled": True},
    "divorce_accept": {"text": "✅ قبول می‌کنم", "style": "danger", "icon": None, "enabled": True},
    "divorce_reject": {"text": "❌ قبول نمی‌کنم", "style": "success", "icon": None, "enabled": True},
    "game_dice": {"text": "🎲 تاس دوئل", "style": "success", "icon": None, "enabled": True},
    "dice_roll": {"text": "🎲 پرتاب تاس", "style": "danger", "icon": None, "enabled": True},
    "speedclick": {"text": "⚡ زودتر کلیک کن!", "style": "success", "icon": None, "enabled": True},
    "game_hokm": {"text": "🃏 حکم (۴نفره)", "style": "danger", "icon": None, "enabled": True},
    "game_cancel": {"text": "🚫 لغو بازی", "style": "danger", "icon": None, "enabled": True},
    "game_chat": {"text": "💬 چت با رقیب", "style": "primary", "icon": None, "enabled": True},
    "game_rematch": {"text": "🤝 دعوت مجدد حریف قبلی", "style": "primary", "icon": None, "enabled": True},
    "invite_accept": {"text": "✅ قبول دعوت", "style": "success", "icon": None, "enabled": True},
    "invite_reject": {"text": "❌ رد دعوت", "style": "danger", "icon": None, "enabled": True},
    "admin_games_manage": {"text": "🎮 مدیریت بازی‌ها", "style": "primary", "icon": None, "enabled": True},
    "hokm_join": {"text": "🃏 پیوستن به میز", "style": "success", "icon": None, "enabled": True},
    "hokm_pick_suit": {"text": "🎴 انتخاب حکم", "style": "danger", "icon": None, "enabled": True},
}
BUTTONS_STYLE_VERSION = 2  # هر بار که رنگ‌های پیش‌فرض بالا رو عوض کردم این عدد رو یکی زیاد می‌کنم

DEFAULT_DATA = {
    "users": {},
    "characters": [],
    "channels": [],
    "texts": dict(DEFAULT_TEXTS),
    "texts_entities": {},  # {text_key: [{"offset":int,"length":int,"custom_emoji_id":str}, ...]}
    "buttons": {k: dict(v) for k, v in DEFAULT_BUTTONS.items()},
    "item_icons": {},  # {item_key: custom_emoji_id}  -> آیکون کنار دکمهٔ اسم آیتم فروشگاه
    "shop": {"food": [], "weapon": [], "clothes": []},  # کاتالوگ زندهٔ فروشگاه (قابل‌ویرایش توسط ادمین)
    "next_char_id": 1,
    "next_item_id": 1,
    "stats": {"total_battles": 0, "total_feeds": 0},
    "buttons_style_version": 0,
    "games": {},          # game_id -> game dict
    "game_queue": {"xo": [], "rps": [], "dice": []},  # {"xo":[{"uid":int,"ts":int}, ...], ...}
    "next_game_id": 1,
    "char_requests": {},  # req_id -> {"user_id": int, "created_at": int}
    "next_charreq_id": 1,
    "couples": {},         # couple_key ("uid1_uid2" sorted) -> couple dict
    "divorce_requests": {},  # target_uid(str) -> {"from": uid, "ts": int}
    "known_groups": [],
    "admin_active_chat": None,
    "fambo_kid_last": {},
    "speedclick_events": {},
    "next_speedclick_id": 1,
    "group_panel_owners": {},   # {message_id(str): {"uid","chat_id","created_at"}} -- پنل «فمبوی» تو گروه
    "pending_transfers": {},    # (رزرو، فعلاً از ConversationHandler استفاده می‌کنیم)
    "game_enabled": {"xo": True, "rps": True, "dice": True, "hokm": True},
    "played_with": {},
    "game_invites": {},
    "next_invite_id": 1,
    "hokm_tables": {},
    "hokm_queue": [],
    "next_hokm_id": 1,
}

# ----------------------------------------------------------------------------
# ذخیره‌سازی
# ----------------------------------------------------------------------------
_data_cache = None


def _default_user_fields():
    return {
        "fridge": {},
        "weapons_owned": [],
        "clothes_owned": [],
        "weapon_equipped": None,
        "clothes_equipped": None,
        "current_game": None,
        "partner_id": None,
        "rp_active": False,
        "owned_characters": [],
    }


def load_data():
    global _data_cache
    if _data_cache is not None:
        return _data_cache
    if os.path.exists(DATA_FILE):
        with open(DATA_FILE, "r", encoding="utf-8") as f:
            try:
                loaded = json.load(f)
            except json.JSONDecodeError:
                loaded = {}
    else:
        loaded = {}
    data = json.loads(json.dumps(DEFAULT_DATA))  # deep copy of defaults
    data.update({k: v for k, v in loaded.items() if k in data or True})
    # اطمینان از وجود کلیدهای پایه
    for key, default_val in DEFAULT_DATA.items():
        if key not in data:
            data[key] = default_val
    for key, val in DEFAULT_TEXTS.items():
        data["texts"].setdefault(key, val)
    for key, val in DEFAULT_BUTTONS.items():
        if key not in data["buttons"]:
            data["buttons"][key] = dict(val)
        else:
            data["buttons"][key].setdefault("enabled", True)
            data["buttons"][key].setdefault("icon", None)
            data["buttons"][key].setdefault("style", val.get("style"))
    data.setdefault("item_icons", {})
    data.setdefault("games", {})
    data.setdefault("game_queue", {"xo": [], "rps": []})
    data["game_queue"].setdefault("xo", [])
    data["game_queue"].setdefault("rps", [])
    data["game_queue"].setdefault("dice", [])
    data.setdefault("next_game_id", 1)
    data.setdefault("char_requests", {})
    data.setdefault("next_charreq_id", 1)
    data.setdefault("next_item_id", 1)
    data.setdefault("couples", {})
    data.setdefault("divorce_requests", {})
    data.setdefault("shop", {"food": [], "weapon": [], "clothes": []})
    for cat in ("food", "weapon", "clothes"):
        data["shop"].setdefault(cat, [])
    # اولین اجرا: کاتالوگ فروشگاه رو با آیتم‌های پیش‌فرض پر می‌کنیم (فقط یه‌بار)
    if not data["shop"]["food"] and not data["shop"]["weapon"] and not data["shop"]["clothes"]:
        data["shop"]["food"] = [dict(it) for it in DEFAULT_FOOD_ITEMS]
        data["shop"]["weapon"] = [dict(it) for it in DEFAULT_WEAPON_ITEMS]
        data["shop"]["clothes"] = [dict(it) for it in DEFAULT_CLOTHES_ITEMS]
    # مهاجرت صف انتظار قدیمی (لیست ساده از uid) به فرمت جدید {"uid":..,"ts":..}
    for gt in ("xo", "rps", "dice"):
        fixed_queue = []
        for entry in data["game_queue"].get(gt, []):
            if isinstance(entry, dict):
                fixed_queue.append(entry)
            else:
                fixed_queue.append({"uid": entry, "ts": now_ts()})
        data["game_queue"][gt] = fixed_queue

    # مهاجرت کاربرهای قدیمی: فیلدهای جدید (یخچال، اسلحه، لباس، بازی فعلی)
    for u in data["users"].values():
        for fk, fv in _default_user_fields().items():
            if fk not in u:
                u[fk] = fv if not isinstance(fv, (dict, list)) else (dict(fv) if isinstance(fv, dict) else list(fv))

    # مهاجرت کاراکترهای قدیمی: فیلد vip_only
    for c in data["characters"]:
        c.setdefault("vip_only", False)

    data.setdefault("known_groups", [])
    data.setdefault("admin_active_chat", None)
    data.setdefault("fambo_kid_last", {})  # {uid_str: ts} -- کول‌داون شخصی «فمبوی کوچولو»
    data.setdefault("speedclick_events", {})  # msg-ref -> {"chat_id","message_id","claimed"}
    data.setdefault("next_speedclick_id", 1)
    data.setdefault("group_panel_owners", {})
    data.setdefault("pending_transfers", {})
    data.setdefault("game_enabled", {})
    for gt in ("xo", "rps", "dice", "hokm"):
        data["game_enabled"].setdefault(gt, True)
    data.setdefault("played_with", {})   # {uid_str: [uid, ...]} -- تاریخچهٔ حریف‌های قبلی برای دعوت مجدد
    data.setdefault("game_invites", {})  # invite_id -> {"from","to","game_type","ts"}
    data.setdefault("next_invite_id", 1)
    data.setdefault("hokm_tables", {})   # table_id -> حکم در حال تشکیل/بازی
    data.setdefault("hokm_queue", [])    # صف انتظار پیوی برای ۴نفره شدن
    data.setdefault("next_hokm_id", 1)
    # مهاجرت کاراکترهای قدیمی: فیلد price (قیمت خرید؛ 0 یعنی رایگان/غیرقابل‌خرید)
    for c in data["characters"]:
        c.setdefault("price", 0)

    # مهاجرت یک‌بارهٔ رنگ دکمه‌ها: اگه هنوز دستی رنگی نکرده بودی (style خالی بود)،
    # رنگ‌های پیش‌فرض جدید رو اعمال می‌کنیم. این کار فقط یه‌بار انجام می‌شه.
    if data.get("buttons_style_version", 0) < BUTTONS_STYLE_VERSION:
        for key, default_val in DEFAULT_BUTTONS.items():
            if key in data["buttons"] and not data["buttons"][key].get("style"):
                data["buttons"][key]["style"] = default_val["style"]
        data["buttons_style_version"] = BUTTONS_STYLE_VERSION
        _data_cache = data
        save_data()
    else:
        _data_cache = data
    return data


_data_dirty = False
_autoflush_enabled = True  # اگه job_queue در دسترس نباشه، False می‌شه و save_data() فوری می‌نویسه


def save_data():
    """این تابع دیگه مستقیم روی دیسک نمی‌نویسه (چون قبلاً هر تعامل کوچیک، کل دیتابیس
    JSON رو با indent=2 دوباره سریالایز و روی دیسک می‌نوشت — دقیقاً همون چیزی که باعث
    مصرف بالای CPU و کشته‌شدن مکرر ربات توسط هاست می‌شد). فقط دیتای این‌مموری رو
    «کثیف» علامت می‌زنه؛ یه جاب دوره‌ای (flush_data_job) هر ۲ ثانیه یه‌بار، اگه چیزی
    عوض شده باشه، توی یه ترد جدا (بدون بلاک کردن event loop) می‌نویستش روی دیسک.
    اگه به هر دلیلی job_queue در دسترس نباشه، برمی‌گرده به همون رفتار قدیمیِ نوشتن فوری،
    تا هیچ دیتایی گم نشه."""
    global _data_dirty
    _data_dirty = True
    if not _autoflush_enabled:
        _write_data_to_disk()
        _data_dirty = False


def _write_data_to_disk():
    if _data_cache is None:
        return
    tmp_path = DATA_FILE + ".tmp"
    with open(tmp_path, "w", encoding="utf-8") as f:
        json.dump(_data_cache, f, ensure_ascii=False, separators=(",", ":"))
    os.replace(tmp_path, DATA_FILE)


async def flush_data_job(context: ContextTypes.DEFAULT_TYPE):
    """هر ۲ ثانیه: اگه چیزی تغییر کرده، توی یه ترد جدا (نه تو event loop اصلی) ذخیره‌ش کن."""
    global _data_dirty
    if not _data_dirty:
        return
    _data_dirty = False
    try:
        await asyncio.to_thread(_write_data_to_disk)
    except Exception:
        logger.exception("خطا در ذخیره‌سازی دیتا روی دیسک")
        _data_dirty = True  # دوباره تلاش کن دفعهٔ بعد


def force_flush_data_sync():
    """فقط برای لحظهٔ خاموش شدن ربات: یه ذخیرهٔ سینک آخر، تا هیچ تغییری از دست نره."""
    global _data_dirty
    if _data_dirty:
        _data_dirty = False
        try:
            _write_data_to_disk()
        except Exception:
            logger.exception("خطا در ذخیرهٔ نهایی دیتا هنگام خاموش شدن")


# ----------------------------------------------------------------------------
# توابع کمکی عمومی
# ----------------------------------------------------------------------------
def now_ts():
    return int(time.time())


def utf16_len(s: str) -> int:
    return len(s.encode("utf-16-le")) // 2


class TextBuilder:
    """کمک به ساخت متن + entities برای نمایش ایموجی پرمیوم داخل پیام."""

    def __init__(self):
        self.text = ""
        self.entities = []

    def add_text(self, s: str):
        self.text += s
        return self

    def add_custom_emoji(self, placeholder: str, custom_emoji_id: str):
        offset = utf16_len(self.text)
        self.text += placeholder
        self.entities.append(
            MessageEntity(
                type=MessageEntity.CUSTOM_EMOJI,
                offset=offset,
                length=utf16_len(placeholder),
                custom_emoji_id=custom_emoji_id,
            )
        )
        return self

    def add_rendered(self, text: str, entities: list):
        """اضافه کردن متنی که از render_text() اومده (با entityهای خودش) به بافر فعلی،
        با جابه‌جا کردن offset entityها متناسب با محل الحاق."""
        offset_shift = utf16_len(self.text)
        self.text += text
        for e in entities:
            self.entities.append(
                MessageEntity(
                    type=e.type,
                    offset=e.offset + offset_shift,
                    length=e.length,
                    custom_emoji_id=e.custom_emoji_id,
                )
            )
        return self

    def build(self):
        return self.text, self.entities


def render_text(key: str, **kwargs):
    """متن یک کلید ویرایش‌پذیر رو با placeholderهاش جایگزین می‌کنه و در همون حال
    ایموجی‌های پرمیومی که ادمین موقع ویرایش متن داخلش گذاشته رو (با offset درست‌شده) نگه می‌داره.
    خروجی: (text, entities) — دقیقاً همون فرمتی که reply_text/edit_text با پارامتر entities می‌خوان."""
    data = load_data()
    template = data["texts"].get(key, "")
    raw_entities = data.get("texts_entities", {}).get(key, [])

    if not raw_entities:
        try:
            return template.format(**kwargs), []
        except Exception:
            return template, []

    formatter = string.Formatter()
    try:
        parsed = list(formatter.parse(template))
    except Exception:
        return template, []

    output_parts = []
    output_entities = []
    template_pos = 0  # واحد utf16 مصرف‌شده تو template
    output_pos = 0    # واحد utf16 تولید‌شده تو خروجی

    for literal_text, field_name, format_spec, conversion in parsed:
        lit_len = utf16_len(literal_text)
        for ent in raw_entities:
            if ent["offset"] >= template_pos and ent["offset"] + ent["length"] <= template_pos + lit_len:
                output_entities.append(
                    MessageEntity(
                        type=MessageEntity.CUSTOM_EMOJI,
                        offset=output_pos + (ent["offset"] - template_pos),
                        length=ent["length"],
                        custom_emoji_id=ent["custom_emoji_id"],
                    )
                )
        output_parts.append(literal_text)
        template_pos += lit_len
        output_pos += lit_len

        if field_name is not None:
            field_repr = "{" + field_name
            if conversion:
                field_repr += "!" + conversion
            if format_spec:
                field_repr += ":" + format_spec
            field_repr += "}"
            template_pos += utf16_len(field_repr)

            try:
                value = kwargs.get(field_name, "")
                if conversion == "r":
                    value = repr(value)
                elif conversion == "s":
                    value = str(value)
                value_str = format(value, format_spec) if format_spec else str(value)
            except Exception:
                value_str = str(kwargs.get(field_name, ""))

            output_parts.append(value_str)
            output_pos += utf16_len(value_str)

    return "".join(output_parts), output_entities


def get_char(char_id):
    data = load_data()
    for c in data["characters"]:
        if c["id"] == char_id:
            return c
    return None


def xp_for_level(level: int) -> int:
    return int(LEVEL_XP_BASE * (LEVEL_XP_GROWTH ** (level - 1)))


def power_for_level(level: int, vip: bool) -> int:
    base = 10 + (level - 1) * 8
    if vip:
        base = int(base * VIP_POWER_MULT)
    return base


def get_user(user_id: int):
    data = load_data()
    return data["users"].get(str(user_id))


def ensure_user(update: Update):
    data = load_data()
    uid = str(update.effective_user.id)
    if uid not in data["users"]:
        u = {
            "user_id": update.effective_user.id,
            "username": update.effective_user.username,
            "first_name": update.effective_user.first_name,
            "character_id": None,
            "custom_name": None,
            "level": 1,
            "xp": 0,
            "power": 0,
            "points": 0,
            "wins": 0,
            "losses": 0,
            "vip": False,
            "last_feed": 0,
            "last_battle": 0,
            "joined_at": now_ts(),
        }
        u.update(_default_user_fields())
        data["users"][uid] = u
        save_data()
    else:
        data["users"][uid]["username"] = update.effective_user.username
        data["users"][uid]["first_name"] = update.effective_user.first_name
    return data["users"][uid]


def weapon_atk(user):
    key = user.get("weapon_equipped")
    if not key:
        return 0
    item = find_item(get_shop_items("weapon"), key)
    return item["atk"] if item else 0


def clothes_defense(user):
    key = user.get("clothes_equipped")
    if not key:
        return 0
    item = find_item(get_shop_items("clothes"), key)
    return item["defense"] if item else 0


def total_attack(user):
    return user["power"] + weapon_atk(user)


def family_attack_bonus(user):
    """اگه کاربر پارتنر و بچه داشته باشه، تو مبارزه به‌عنوان یه تیم سه‌نفره کمکش می‌کنن."""
    data = load_data()
    partner_id = user.get("partner_id")
    if not partner_id:
        return 0, False
    partner = data["users"].get(str(partner_id))
    bonus = 0
    if partner:
        bonus += int(total_attack(partner) * 0.3)
    couple = data["couples"].get(couple_key(user["user_id"], partner_id))
    has_family_help = partner is not None
    if couple and couple.get("child"):
        bonus += int(couple["child"]["power"] * 0.3)
        has_family_help = True
    return bonus, has_family_help


def kb_button(key, callback_data=None, url=None, override_text=None, icon_override=None):
    """icon_override: آیکون پیش‌فرضِ خودکار (مثلاً ایموجی کاراکتر کاربر) که فقط وقتی استفاده می‌شه
    که خودِ ادمین برای این دکمه از پنل «ویرایش دکمه‌ها» آیکون دستی ست نکرده باشه.
    یعنی: اگه ادمین رو یه دکمه آیکون گذاشت، همیشه اون آیکون دستی اولویت داره.
    اگه دکمه توسط ادمین غیرفعال شده باشه، None برمی‌گرده (فراخوان باید فیلترش کنه)."""
    data = load_data()
    cfg = data["buttons"].get(key, {"text": key, "style": None, "icon": None, "enabled": True})
    if not cfg.get("enabled", True):
        return None
    kwargs = {}
    if cfg.get("style"):
        kwargs["style"] = cfg["style"]
    icon = cfg.get("icon") or icon_override
    if icon:
        kwargs["icon_custom_emoji_id"] = icon
    text = override_text or cfg["text"]
    if url:
        return InlineKeyboardButton(text, url=url, **kwargs)
    return InlineKeyboardButton(text, callback_data=callback_data or key, **kwargs)


def rows_clean(row_specs):
    """فیلتر کردن دکمه‌های None (غیرفعال) از هر ردیف و حذف ردیف‌های خالی."""
    rows = []
    for row in row_specs:
        filtered = [b for b in row if b is not None]
        if filtered:
            rows.append(filtered)
    return rows


def register_group_panel(chat_id, message_id, owner_uid):
    """موقع باز شدن پنل «فمبوی» تو گروه، مالکیت + زمانش رو ثبت می‌کنه تا هم فقط
    خودش بتونه دکمه‌هاشو بزنه، هم بعد از GROUP_PANEL_TIMEOUT خودکار بسته بشه."""
    data = load_data()
    owners = data["group_panel_owners"]
    owners[str(message_id)] = {"uid": owner_uid, "chat_id": chat_id, "created_at": now_ts()}
    # جلوگیری از رشد بی‌نهایت: فقط ۵۰۰ تای آخر رو نگه می‌داریم
    if len(owners) > 500:
        for k in list(owners.keys())[: len(owners) - 500]:
            owners.pop(k, None)
    save_data()


def is_group_panel_blocked(update):
    """تو پیوی همیشه False (چون فقط یه کاربر تو اون چته). تو گروه، پنل «فمبوی» فقط
    مال همون کسیه که گفته «فمبوی» — اگه یکی دیگه رو دکمه‌هاش بزنه، بلاک می‌شه."""
    if not update.callback_query or update.effective_chat.type == ChatType.PRIVATE:
        return False
    data = load_data()
    entry = data.get("group_panel_owners", {}).get(str(update.callback_query.message.message_id))
    owner = entry.get("uid") if isinstance(entry, dict) else entry
    return owner is not None and owner != update.effective_user.id


def touch_group_panel(update):
    """با هر تعامل موفق کاربر با پنلش، تایمر ۳ دقیقه‌ای بستن خودکار رو ریست می‌کنه
    تا وقتی داره ازش استفاده می‌کنه، یهو از زیر دستش بسته نشه."""
    if not update.callback_query or update.effective_chat.type == ChatType.PRIVATE:
        return
    data = load_data()
    entry = data.get("group_panel_owners", {}).get(str(update.callback_query.message.message_id))
    if isinstance(entry, dict):
        entry["created_at"] = now_ts()
        save_data()


async def close_group_panels_job(context: ContextTypes.DEFAULT_TYPE):
    """جاب دوره‌ای: هر پنل «فمبوی»ای که تو گروه بیش از GROUP_PANEL_TIMEOUT بدون
    فعالیت مونده رو حذف می‌کنه تا گروه و ربات شلوغ نشه."""
    try:
        data = load_data()
        owners = data.get("group_panel_owners", {})
        now = now_ts()
        expired = [mid for mid, entry in owners.items() if isinstance(entry, dict) and now - entry.get("created_at", now) > GROUP_PANEL_TIMEOUT]
        if not expired:
            return
        for mid in expired:
            entry = owners.pop(mid, None)
            if not entry:
                continue
            try:
                await context.bot.delete_message(chat_id=entry["chat_id"], message_id=int(mid))
            except (BadRequest, Forbidden):
                pass
        save_data()
    except Exception:
        logger.exception("خطا تو جاب close_group_panels_job")


def main_menu_keyboard(user=None):
    """اگه user پاس داده بشه و کاراکتر داشته باشه، ایموجی پرمیوم خودِ کاراکترش
    به‌عنوان آیکون روی دکمه‌های «یخچال»، «مبارزه» و «پروفایل» نشون داده می‌شه
    (مگراینکه ادمین برای اون دکمه از قبل آیکون دستی ست کرده باشه)."""
    char_icon = None
    if user and user.get("character_id"):
        char = get_char(user["character_id"])
        if char:
            char_icon = char["custom_emoji_id"]
    rows = rows_clean([
        [
            kb_button("feed", "menu_feed", icon_override=char_icon),
            kb_button("battle", "menu_battle", icon_override=char_icon),
        ],
        [
            kb_button("profile", "menu_profile", icon_override=char_icon),
            kb_button("leaderboard", "menu_leaderboard"),
        ],
        [kb_button("market", "menu_market"), kb_button("inventory", "menu_inventory")],
        [kb_button("games", "menu_games"), kb_button("vip", "menu_vip")],
        [kb_button("rename_char", "menu_rename_char"), kb_button("swap_char", "menu_swap_char")],
        [kb_button("my_characters", "menu_mychars")],
        [kb_button("partner", "menu_partner")],
        [kb_button("help", "menu_help"), kb_button("donate", url=DONATE_URL)],
    ])
    if user and user.get("user_id") == ADMIN_ID:
        rows.append([b for b in [kb_button("admin_panel", "menu_admin_panel")] if b])
    return InlineKeyboardMarkup(rows)


async def is_member_of_channel(bot, channel_id, user_id) -> bool:
    try:
        member = await bot.get_chat_member(chat_id=channel_id, user_id=user_id)
        return member.status in (
            ChatMemberStatus.MEMBER,
            ChatMemberStatus.ADMINISTRATOR,
            ChatMemberStatus.OWNER,
        )
    except (BadRequest, Forbidden):
        return False


async def check_force_join(bot, user_id) -> list:
    """برمی‌گردونه لیست کانال‌هایی که کاربر عضو نیست.
    اگه کانالی از قبل (نسخهٔ قدیمی‌تر ربات) لینک درست نداشت و به‌جاش آیدی عددی ذخیره شده
    بود، همین‌جا خودکار سعی می‌کنه لینک درست رو بسازه و تو دیتا ذخیره کنه."""
    data = load_data()
    missing = []
    fixed_any = False
    for ch in data["channels"]:
        if not ch.get("link"):
            try:
                chat = await bot.get_chat(ch["chat_id"])
                if chat.username:
                    ch["link"] = f"https://t.me/{chat.username}"
                else:
                    ch["link"] = await bot.export_chat_invite_link(chat.id)
                fixed_any = True
            except (BadRequest, Forbidden):
                logger.warning(
                    "نتونستم لینک کانال %s رو بسازم؛ ربات باید تو اون کانال ادمین با دسترسی دعوت باشه.",
                    ch.get("title", ch["chat_id"]),
                )
        joined = await is_member_of_channel(bot, ch["chat_id"], user_id)
        if not joined:
            missing.append(ch)
    if fixed_any:
        save_data()
    return missing


def force_join_keyboard(missing_channels):
    rows = []
    for ch in missing_channels:
        link = ch.get("link")
        if not link:
            continue
        rows.append([InlineKeyboardButton(f"📢 {ch['title']}", url=link)])
    btn = kb_button("check_join", "check_join")
    if btn:
        rows.append([btn])
    return InlineKeyboardMarkup(rows)


def cooldown_remaining_minutes(last_ts, cooldown_seconds):
    elapsed = now_ts() - last_ts
    remaining = cooldown_seconds - elapsed
    if remaining <= 0:
        return 0
    return remaining // 60 + 1


def xp_progress_bar(xp, next_xp, length=10):
    if next_xp <= 0:
        percent = 0
    else:
        percent = max(0, min(100, int(xp * 100 / next_xp)))
    filled = round(length * percent / 100)
    bar = "▓" * filled + "░" * (length - filled)
    return bar, percent


# ----------------------------------------------------------------------------
# استیت‌های Conversation
# ----------------------------------------------------------------------------
(
    NAME_CHAR,
    ADD_CHANNEL,
    ADD_CHAR_EMOJI,
    ADD_CHAR_NAME,
    EDIT_TEXT,
    EDIT_BUTTON_TEXT,
    GIVE_POINTS_UID,
    GIVE_POINTS_AMOUNT,
    BROADCAST_MSG,
    IMPORT_PACK,
    SET_ITEM_ICON,
    RENAME_CHAR,
    ADMIN_CHAR_REQUEST,
    SHOP_ADD_NAME,
    SHOP_ADD_PRICE,
    SHOP_ADD_STAT,
    SHOP_EDIT_FIELD,
    CHILD_NAME,
    ADD_CHAR_VIP,
    IMPORT_PACK_VIP,
    ADMIN_CHAT_RELAY,
    ADD_CHAR_PRICE,
    GAMBLE_STAKE,
    TRANSFER_AMOUNT,
) = range(24)


async def cancel_conv(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.effective_message.reply_text("لغو شد.")
    return ConversationHandler.END


# ----------------------------------------------------------------------------
# /start و جریان اصلی کاربر
# ----------------------------------------------------------------------------
async def start_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
    data = load_data()
    user = ensure_user(update)

    missing = await check_force_join(context.bot, update.effective_user.id)
    if missing:
        text, entities = render_text("force_join_prompt")
        await update.message.reply_text(text, entities=entities, reply_markup=force_join_keyboard(missing))
        return ConversationHandler.END

    ref_referrer_id = None
    if context.args and context.args[0].startswith("ref_"):
        try:
            ref_referrer_id = int(context.args[0][4:])
        except ValueError:
            ref_referrer_id = None

    if user["character_id"] is None:
        if ref_referrer_id:
            data["users"][str(update.effective_user.id)]["pending_ref"] = ref_referrer_id
            save_data()
        return await assign_new_character(update.message, update.effective_user.id, context)

    if ref_referrer_id:
        await process_referral(context, update.effective_user.id, ref_referrer_id)

    text, entities = render_text("welcome")
    await update.message.reply_text(text, entities=entities, reply_markup=main_menu_keyboard(user))
    return ConversationHandler.END


async def assign_new_character(message: Message, user_id: int, context: ContextTypes.DEFAULT_TYPE):
    data = load_data()
    if not data["characters"]:
        text, entities = render_text("no_characters_available")
        await message.reply_text(text, entities=entities)
        return ConversationHandler.END

    user = data["users"].get(str(user_id), {})
    pool = eligible_characters(user.get("vip", False)) or data["characters"]
    char = random.choice(pool)
    grant_character(data["users"][str(user_id)], char["id"])
    save_data()

    tb = TextBuilder()
    tb.add_custom_emoji(char["placeholder"], char["custom_emoji_id"])
    tb.add_text("\n")
    tb.add_rendered(*render_text("assign_character"))
    text, entities = tb.build()
    await message.reply_text(text, entities=entities)
    return NAME_CHAR


async def receive_char_name(update: Update, context: ContextTypes.DEFAULT_TYPE):
    data = load_data()
    uid = str(update.effective_user.id)
    name = update.message.text.strip()[:32]
    if uid not in data["users"] or data["users"][uid]["character_id"] is None:
        await update.message.reply_text("اول باید کاراکتر بگیری، دستور /start رو بزن.")
        return ConversationHandler.END
    if not name or name.lower().startswith(("http://", "https://", "t.me/", "@")):
        await update.message.reply_text("این اسم معتبر نیست (نباید لینک یا یوزرنیم باشه). یه اسم دیگه بفرست:")
        return NAME_CHAR
    data["users"][uid]["custom_name"] = name
    data["users"][uid]["power"] = power_for_level(1, data["users"][uid]["vip"])
    pending_ref = data["users"][uid].pop("pending_ref", None)
    save_data()
    text, entities = render_text("name_set_confirm")
    await update.message.reply_text(text, entities=entities, reply_markup=main_menu_keyboard(data["users"][uid]))
    if pending_ref:
        await process_referral(context, update.effective_user.id, pending_ref)
    return ConversationHandler.END


async def check_join_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await safe_answer(query, )
    data = load_data()
    missing = await check_force_join(context.bot, update.effective_user.id)
    if missing:
        alert_text, _ = render_text("not_joined_yet")
        await safe_answer(query, alert_text, show_alert=True)
        return
    user = ensure_user(update)
    if user["character_id"] is None:
        await query.message.delete()
        await assign_new_character(query.message, update.effective_user.id, context)
        return
    text, entities = render_text("welcome")
    await safe_edit_text(query.message, text, entities=entities, reply_markup=main_menu_keyboard(user))


# ----------------------------------------------------------------------------
# منوی اصلی: یخچال / پروفایل / لیدربورد / مبارزه / VIP / راهنما / سوپرمارکت / کوله‌پشتی / بازی
# ----------------------------------------------------------------------------
def vip_badge(user):
    return "💎 VIP" if user["vip"] else "عادی"


def weapon_label(user):
    key = user.get("weapon_equipped")
    if not key:
        return "—"
    item = find_item(get_shop_items("weapon"), key)
    return f"{item['name']} (+{item['atk']} قدرت)" if item else "—"


def clothes_label(user):
    key = user.get("clothes_equipped")
    if not key:
        return "—"
    item = find_item(get_shop_items("clothes"), key)
    return f"{item['name']} (+{item['defense']} دفاع)" if item else "—"


async def show_profile(query_or_msg, user_id, edit=False):
    data = load_data()
    user = data["users"].get(str(user_id))
    if not user or user["character_id"] is None:
        target = query_or_msg.message if hasattr(query_or_msg, "message") else query_or_msg
        await target.reply_text("هنوز کاراکتر نداری! /start رو بزن.")
        return
    char = get_char(user["character_id"])
    next_xp = xp_for_level(user["level"])
    bar, percent = xp_progress_bar(user["xp"], next_xp)
    tb = TextBuilder()
    if char:
        tb.add_custom_emoji(char["placeholder"], char["custom_emoji_id"])
        tb.add_text(" ")
    tb.add_rendered(
        *render_text(
            "profile_template",
            name=user["custom_name"] or "بدون‌نام",
            level=user["level"],
            xp=user["xp"],
            next_xp=next_xp,
            progress_bar=bar,
            percent=percent,
            power=user["power"],
            weapon=weapon_label(user),
            clothes=clothes_label(user),
            currency_name=CURRENCY_NAME,
            points=user["points"],
            wins=user["wins"],
            losses=user["losses"],
            vip_badge=vip_badge(user),
        )
    )
    feed_left = cooldown_remaining_minutes(user["last_feed"], FEED_COOLDOWN)
    battle_left = cooldown_remaining_minutes(user["last_battle"], BATTLE_COOLDOWN)
    tb.add_text("\n\n⏳ زمان باقی‌مانده:\n")
    tb.add_text(f"🍖 غذا دادن: {'الان می‌تونی' if feed_left == 0 else str(feed_left) + ' دقیقه'}\n")
    tb.add_text(f"⚔️ مبارزه: {'الان می‌تونی' if battle_left == 0 else str(battle_left) + ' دقیقه'}")
    text, entities = tb.build()
    btn = kb_button("back", "menu_back")
    kb = InlineKeyboardMarkup([[btn]] if btn else [])
    if edit and hasattr(query_or_msg, "message"):
        try:
            await safe_edit_text(query_or_msg.message, text, entities=entities, reply_markup=kb)
            return
        except BadRequest:
            pass
    target = query_or_msg.message if hasattr(query_or_msg, "message") else query_or_msg
    await target.reply_text(text, entities=entities, reply_markup=kb)


async def do_feed_action(update: Update, context: ContextTypes.DEFAULT_TYPE, food_key=None):
    query = update.callback_query
    await safe_answer(query, )
    data = load_data()
    uid = str(update.effective_user.id)
    user = data["users"].get(uid)
    if not user or user["character_id"] is None:
        await safe_answer(query, "اول /start رو بزن.", show_alert=True)
        return

    elapsed = now_ts() - user["last_feed"]
    if elapsed < FEED_COOLDOWN:
        minutes = (FEED_COOLDOWN - elapsed) // 60 + 1
        text, _ = render_text("feed_cooldown", minutes=minutes)
        await safe_answer(query, text, show_alert=True)
        return

    fridge = user.get("fridge", {})
    chosen_key = food_key
    if chosen_key is None or fridge.get(chosen_key, 0) <= 0:
        chosen_key = next((k for k, v in fridge.items() if v > 0), None)

    if not chosen_key:
        text, entities = render_text("feed_no_food")
        kb_btn = kb_button("market_food", "menu_market_food")
        kb = InlineKeyboardMarkup([[kb_btn]] if kb_btn else [])
        await query.message.reply_text(text, entities=entities, reply_markup=kb)
        return

    item = find_item(get_shop_items("food"), chosen_key)
    if not item:
        await query.message.reply_text("این غذا دیگه معتبر نیست.")
        return

    fridge[chosen_key] -= 1
    if fridge[chosen_key] <= 0:
        del fridge[chosen_key]
    user["fridge"] = fridge

    xp_gain = item["xp"]
    pts_gain = item["points"]
    if user["vip"]:
        xp_gain = int(xp_gain * VIP_FEED_MULT)
        pts_gain = int(pts_gain * VIP_FEED_MULT)

    user["xp"] += xp_gain
    user["points"] += pts_gain
    user["last_feed"] = now_ts()
    data["stats"]["total_feeds"] += 1

    leveled_up = False
    while user["xp"] >= xp_for_level(user["level"]):
        user["xp"] -= xp_for_level(user["level"])
        user["level"] += 1
        user["power"] = power_for_level(user["level"], user["vip"])
        leveled_up = True
    save_data()

    tb = TextBuilder()
    tb.add_text(f"{item['name']} رو به کاراکترت دادی!\n")
    tb.add_rendered(*render_text("feed_success", xp=xp_gain, points=pts_gain))
    if leveled_up:
        tb.add_text("\n\n")
        tb.add_rendered(*render_text("level_up", level=user["level"], power=user["power"]))
    text, entities = tb.build()
    await query.message.reply_text(text, entities=entities)


async def do_battle(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await safe_answer(query, )
    data = load_data()
    uid = str(update.effective_user.id)
    user = data["users"].get(uid)
    if not user or user["character_id"] is None:
        await safe_answer(query, "اول /start رو بزن.", show_alert=True)
        return

    elapsed = now_ts() - user["last_battle"]
    if elapsed < BATTLE_COOLDOWN:
        minutes = (BATTLE_COOLDOWN - elapsed) // 60 + 1
        text, _ = render_text("battle_cooldown", minutes=minutes)
        await safe_answer(query, text, show_alert=True)
        return

    candidates = [
        u for k, u in data["users"].items()
        if k != uid and u["character_id"] is not None
    ]
    if not candidates:
        text, entities = render_text("battle_no_opponent")
        await query.message.reply_text(text, entities=entities)
        return

    opponent = random.choice(candidates)

    # قدرت مؤثر = قدرت پایه + قدرت اسلحه (+ کمک پارتنر/بچه اگه داشته باشه)، در برابر دفاع لباس حریف
    user_bonus, user_family_help = family_attack_bonus(user)
    opp_bonus, opp_family_help = family_attack_bonus(opponent)
    atk_user = total_attack(user) + user_bonus
    atk_opp = total_attack(opponent) + opp_bonus
    net_user = atk_user - clothes_defense(opponent) * 0.5
    net_opp = atk_opp - clothes_defense(user) * 0.5

    if abs(net_user - net_opp) < 0.01:
        user_wins = random.random() < 0.5
    else:
        user_wins = net_user > net_opp

    user["last_battle"] = now_ts()
    data["stats"]["total_battles"] += 1
    opponent_char = get_char(opponent["character_id"])
    opponent_name = opponent["custom_name"] or "حریف"
    save_data()

    # ---------- نمایش مقدمه: کاراکترها، اسلحه، لباس ----------
    intro_text, intro_entities = render_text(
        "battle_intro",
        p1_name=user_display_name(user), p1_weapon=weapon_label(user), p1_clothes=clothes_label(user),
        p2_name=user_display_name(opponent), p2_weapon=weapon_label(opponent), p2_clothes=clothes_label(opponent),
    )
    msg = await query.message.reply_text(intro_text, entities=intro_entities)

    # ---------- انیمیشن سادهٔ مبارزه (چند مرحله، حدود یک دقیقه) ----------
    round_actors = [user_display_name(user), user_display_name(opponent)]
    for i in range(4):
        await asyncio.sleep(13)
        actor = round_actors[i % 2]
        round_text, round_entities = render_text(
            "battle_round",
            p1_name=user_display_name(user), p2_name=user_display_name(opponent), actor=actor,
        )
        try:
            await safe_edit_text(msg, round_text, entities=round_entities)
        except BadRequest:
            pass

    # ---------- نتیجهٔ نهایی ----------
    tb = TextBuilder()
    if user_wins:
        pts = BATTLE_POINTS_WIN
        xp = BATTLE_XP_WIN
        if user["vip"]:
            pts = int(pts * VIP_FEED_MULT)
        user["points"] += pts
        user["xp"] += xp
        user["wins"] += 1
        opponent["losses"] += 1
        tb.add_rendered(*render_text("battle_win", opponent_name=opponent_name, points=pts, xp=xp))
    else:
        xp = BATTLE_XP_LOSE
        user["xp"] += xp
        user["losses"] += 1
        opponent["wins"] += 1
        tb.add_rendered(*render_text("battle_lose", opponent_name=opponent_name, xp=xp))

    if user_family_help or opp_family_help:
        tb.add_rendered(*render_text("battle_family_note"))

    # جایگزین کردن نام حریف تو متن با «ایموجی پرمیوم حریف + اسمش»
    if opponent_char:
        idx = tb.text.find(opponent_name)
        if idx != -1:
            insert_utf16 = utf16_len(tb.text[:idx])
            inserted_len = utf16_len(opponent_char["placeholder"]) + 1
            new_entities = []
            for e in tb.entities:
                off = e.offset + inserted_len if e.offset >= insert_utf16 else e.offset
                new_entities.append(
                    MessageEntity(type=e.type, offset=off, length=e.length, custom_emoji_id=e.custom_emoji_id)
                )
            new_entities.append(
                MessageEntity(
                    type=MessageEntity.CUSTOM_EMOJI,
                    offset=insert_utf16,
                    length=utf16_len(opponent_char["placeholder"]),
                    custom_emoji_id=opponent_char["custom_emoji_id"],
                )
            )
            tb.text = tb.text[:idx] + opponent_char["placeholder"] + " " + tb.text[idx:]
            tb.entities = new_entities

    leveled_up = False
    while user["xp"] >= xp_for_level(user["level"]):
        user["xp"] -= xp_for_level(user["level"])
        user["level"] += 1
        user["power"] = power_for_level(user["level"], user["vip"])
        leveled_up = True
    save_data()

    if leveled_up:
        tb.add_text("\n\n")
        tb.add_rendered(*render_text("level_up", level=user["level"], power=user["power"]))

    text, entities = tb.build()
    try:
        await safe_edit_text(msg, text, entities=entities)
    except BadRequest:
        await query.message.reply_text(text, entities=entities)

def leaderboard_score(u):
    """امتیاز رتبه‌بندی: سطح خیلی بیشتر از فمبوی پوینت اثر داره؛ فمبوی پوینت فقط جدا کنندهٔ هم‌سطح‌هاست."""
    return u["level"] * 1000 + min(u["points"], 999)


async def show_leaderboard(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await safe_answer(query, )
    data = load_data()
    ranked = sorted(
        [u for u in data["users"].values() if u["character_id"] is not None],
        key=leaderboard_score,
        reverse=True,
    )[:10]
    if not ranked:
        text = "هنوز کسی امتیازی نداره!"
    else:
        lines = ["🏆 لیدربورد برترین بازیکن‌ها (بر اساس سطح):\n"]
        medals = ["🥇", "🥈", "🥉"]
        for i, u in enumerate(ranked):
            medal = medals[i] if i < 3 else f"{i+1}."
            name = u["custom_name"] or (u["first_name"] or "بازیکن")
            lines.append(f"{medal} {name} — سطح {u['level']} — {u['points']} {CURRENCY_NAME}")
        text = "\n".join(lines)
    btn = kb_button("back", "menu_back")
    kb = InlineKeyboardMarkup([[btn]] if btn else [])
    await safe_edit_text(query.message, text, reply_markup=kb)


async def show_vip(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await safe_answer(query, )
    data = load_data()
    user = data["users"].get(str(update.effective_user.id))
    key = "vip_info_vip" if (user and user["vip"]) else "vip_info_not_vip"
    text, entities = render_text(key)
    btn = kb_button("back", "menu_back")
    kb = InlineKeyboardMarkup([[btn]] if btn else [])
    await safe_edit_text(query.message, text, entities=entities, reply_markup=kb)


async def show_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await safe_answer(query, )
    btn = kb_button("back", "menu_back")
    kb = InlineKeyboardMarkup([[btn]] if btn else [])
    text, entities = render_text("help_text")
    await safe_edit_text(query.message, text, entities=entities, reply_markup=kb)


async def safe_answer(query, *args, **kwargs):
    """جایگزین امنِ query.answer() — وقتی کال‌بک خیلی قدیمی/منقضی شده باشه
    (مثلاً بعد از ری‌استارت ربات، یه عالمه آپدیت قدیمی تو صف مونده) تلگرام
    ارور «Query is too old» می‌ده؛ این خطا رو بی‌سروصدا نادیده می‌گیریم به‌جای
    اینکه کل پردازش اون آپدیت با یه ارور تو لاگ متوقف بشه."""
    try:
        await query.answer(*args, **kwargs)
    except BadRequest as e:
        msg = str(e).lower()
        if "query is too old" in msg or "query id is invalid" in msg or "query id invalid" in msg:
            logger.debug("یه callback query منقضی‌شده نادیده گرفته شد: %s", e)
        else:
            raise


async def safe_edit_text(message, *args, **kwargs):
    """جایگزین امنِ message.edit_text() — وقتی محتوای جدید دقیقاً همون محتوای فعلی پیام باشه،
    تلگرام ارور «Message is not modified» می‌ده؛ این خطا رو بی‌سروصدا نادیده می‌گیریم."""
    try:
        return await message.edit_text(*args, **kwargs)
    except BadRequest as e:
        if "message is not modified" in str(e).lower():
            return message
        raise


async def noop_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await safe_answer(update.callback_query)


# ----------------------------------------------------------------------------
# سوپرمارکت
# ----------------------------------------------------------------------------
def item_icon(key):
    data = load_data()
    return data.get("item_icons", {}).get(key)


def market_main_keyboard():
    rows = rows_clean([
        [kb_button("market_food", "menu_market_food"), kb_button("market_weapon", "menu_market_weapon")],
        [kb_button("market_clothes", "menu_market_clothes"), kb_button("market_character", "menu_market_character")],
        [kb_button("back", "menu_back")],
    ])
    return InlineKeyboardMarkup(rows)


def build_shop_page_rows(items, category):
    data = load_data()
    rows = []
    for it in items:
        name_btn = InlineKeyboardButton(it["name"], callback_data="noop", icon_custom_emoji_id=item_icon(it["key"])) \
            if item_icon(it["key"]) else InlineKeyboardButton(it["name"], callback_data="noop")
        price_btn = InlineKeyboardButton(f"{it['price']} 💰", callback_data="noop")
        buy_btn = kb_button("buy", f"buy:{category}:{it['key']}")
        row = [b for b in [name_btn, price_btn, buy_btn] if b is not None]
        if row:
            rows.append(row)
    back_btn = kb_button("back", "menu_market")
    if back_btn:
        rows.append([back_btn])
    return rows


async def show_market_main(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await safe_answer(query, )
    text = f"🛒 سوپرمارکت\nموجودی تو: {get_user(update.effective_user.id)['points']} {CURRENCY_NAME}\n\nیه بخش رو انتخاب کن:"
    await safe_edit_text(query.message, text, reply_markup=market_main_keyboard())


async def show_market_category(update: Update, context: ContextTypes.DEFAULT_TYPE, category):
    query = update.callback_query
    await safe_answer(query, )
    items = {"food": get_shop_items("food"), "weapon": get_shop_items("weapon"), "clothes": get_shop_items("clothes")}[category]
    title = {"food": "🍔 غذا", "weapon": "🗡 اسلحه", "clothes": "👕 لباس"}[category]
    text = f"{title}\nموجودی تو: {get_user(update.effective_user.id)['points']} {CURRENCY_NAME}"
    rows = build_shop_page_rows(items, category)
    await safe_edit_text(query.message, text, reply_markup=InlineKeyboardMarkup(rows))


async def show_market_characters(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await safe_answer(query, )
    data = load_data()
    user = data["users"].get(str(update.effective_user.id))
    owned = user.get("owned_characters", []) if user else []
    paid_chars = [c for c in data["characters"] if c.get("price")]
    rows = []
    for c in paid_chars:
        is_owned = c["id"] in owned
        star = "🌟" if c.get("vip_only") else ""
        name_btn = InlineKeyboardButton(
            f"{star}{c['base_name']}" + (" ✅" if is_owned else ""),
            callback_data="noop", icon_custom_emoji_id=c.get("custom_emoji_id"),
        )
        price_btn = InlineKeyboardButton(f"{c['price']} 💰", callback_data="noop")
        buy_btn = InlineKeyboardButton(
            "دارمش ✅" if is_owned else "🛒 خرید",
            callback_data="noop" if is_owned else f"buy:character:{c['id']}",
        )
        rows.append([name_btn, price_btn, buy_btn])
    points = user["points"] if user else 0
    text = f"🎭 کاراکترهای ویژه (پولی)\nموجودی تو: {points} {CURRENCY_NAME}" if paid_chars \
        else "فعلاً هیچ کاراکتر پولی‌ای تو فروشگاه نیست."
    back_btn = kb_button("back", "menu_market")
    if back_btn:
        rows.append([back_btn])
    await safe_edit_text(query.message, text, reply_markup=InlineKeyboardMarkup(rows))


async def buy_item_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if is_group_panel_blocked(update):
        await safe_answer(query, "این پنل مال خودت نیست! بگو «فمبوی» تا پنل خودتو بگیری.", show_alert=True)
        return
    touch_group_panel(update)
    _, category, key = query.data.split(":", 2)
    data = load_data()
    uid = str(update.effective_user.id)
    user = data["users"].get(uid)
    if not user or user["character_id"] is None:
        await safe_answer(query, "اول /start رو بزن.", show_alert=True)
        return

    if category == "character":
        char_id = int(key)
        c = get_char(char_id)
        if not c or not c.get("price"):
            await safe_answer(query, "این کاراکتر دیگه موجود نیست.", show_alert=True)
            return
        if char_id in user.get("owned_characters", []):
            await safe_answer(query, "این کاراکتر رو قبلاً داری!", show_alert=True)
            return
        if user["points"] < c["price"]:
            await safe_answer(query, f"{CURRENCY_NAME} کافی نداری! قیمت: {c['price']}", show_alert=True)
            return
        user["points"] -= c["price"]
        grant_character(user, char_id)
        save_data()
        await safe_answer(query, f"✅ کاراکتر «{c['base_name']}» خریداری و فعال شد! از «کاراکترهای من» می‌تونی هروقت خواستی بین کاراکترهات سوییچ کنی.", show_alert=True)
        return

    items = {"food": get_shop_items("food"), "weapon": get_shop_items("weapon"), "clothes": get_shop_items("clothes")}[category]
    item = find_item(items, key)
    if not item:
        await safe_answer(query, "این آیتم دیگه موجود نیست.", show_alert=True)
        return

    if category != "food" and key in user.get(f"{category}s_owned" if category == "weapon" else "clothes_owned", []):
        await safe_answer(query, "این آیتم رو قبلاً خریدی!", show_alert=True)
        return

    if user["points"] < item["price"]:
        await safe_answer(query, f"{CURRENCY_NAME} کافی نداری! قیمت: {item['price']}", show_alert=True)
        return

    user["points"] -= item["price"]
    if category == "food":
        user["fridge"][key] = user["fridge"].get(key, 0) + 1
        await safe_answer(query, f"✅ {item['name']} خریدی شد و رفت تو یخچال!", show_alert=True)
    elif category == "weapon":
        user["weapons_owned"].append(key)
        await safe_answer(query, f"✅ {item['name']} خریدی شد! از کوله‌پشتی می‌تونی تجهیزش کنی.", show_alert=True)
    elif category == "clothes":
        user["clothes_owned"].append(key)
        await safe_answer(query, f"✅ {item['name']} خریدی شد! از کوله‌پشتی می‌تونی تجهیزش کنی.", show_alert=True)
    save_data()


# ----------------------------------------------------------------------------
# یخچال (غذاها)
# ----------------------------------------------------------------------------
async def show_fridge(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await safe_answer(query, )
    data = load_data()
    user = data["users"].get(str(update.effective_user.id))
    if not user:
        return
    fridge = user.get("fridge", {})
    rows = []
    if not fridge:
        text = "🧊 یخچالت خالیه! برو سوپرمارکت یه چیزی بخر."
    else:
        text = "🧊 یخچال تو:"
        for key, count in fridge.items():
            if count <= 0:
                continue
            item = find_item(get_shop_items("food"), key)
            if not item:
                continue
            name_btn = InlineKeyboardButton(f"{item['name']} × {count}", callback_data="noop")
            feed_btn = kb_button("feed_item", f"feeduse:{key}")
            sell_btn = kb_button("sell_item", f"sell:food:{key}")
            row = [b for b in [name_btn, feed_btn, sell_btn] if b is not None]
            if row:
                rows.append(row)
    mkt_btn = kb_button("market_food", "menu_market_food")
    if mkt_btn:
        rows.append([mkt_btn])
    back_btn = kb_button("back", "menu_back")
    if back_btn:
        rows.append([back_btn])
    await safe_edit_text(query.message, text, reply_markup=InlineKeyboardMarkup(rows))


async def sell_item_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if is_group_panel_blocked(update):
        await safe_answer(query, "این پنل مال خودت نیست! بگو «فمبوی» تا پنل خودتو بگیری.", show_alert=True)
        return
    touch_group_panel(update)
    _, category, key = query.data.split(":", 2)
    data = load_data()
    uid = str(update.effective_user.id)
    user = data["users"].get(uid)
    if not user:
        await safe_answer(query, "اول /start رو بزن.", show_alert=True)
        return

    if category == "food":
        item = find_item(get_shop_items("food"), key)
        fridge = user.get("fridge", {})
        if not item or fridge.get(key, 0) <= 0:
            await safe_answer(query, "این غذا رو تو یخچال نداری.", show_alert=True)
            return
        fridge[key] -= 1
        if fridge[key] <= 0:
            del fridge[key]
        refund = max(1, item["price"] // 2)
        user["points"] += refund
        save_data()
        await safe_answer(query, f"💰 {item['name']} فروخته شد. +{refund} {CURRENCY_NAME}", show_alert=True)
        await show_fridge(update, context)
    elif category in ("weapon", "clothes"):
        owned_key = "weapons_owned" if category == "weapon" else "clothes_owned"
        equipped_key = "weapon_equipped" if category == "weapon" else "clothes_equipped"
        items = get_shop_items("weapon") if category == "weapon" else get_shop_items("clothes")
        item = find_item(items, key)
        if not item or key not in user.get(owned_key, []):
            await safe_answer(query, "این آیتم رو نداری.", show_alert=True)
            return
        user[owned_key].remove(key)
        if user.get(equipped_key) == key:
            user[equipped_key] = None
        refund = max(1, item["price"] // 2)
        user["points"] += refund
        save_data()
        await safe_answer(query, f"💰 {item['name']} فروخته شد. +{refund} {CURRENCY_NAME}", show_alert=True)
        await show_inventory_category(update, context, category)


# ----------------------------------------------------------------------------
# کوله‌پشتی (اسلحه/لباس)
# ----------------------------------------------------------------------------
def inventory_main_keyboard():
    rows = rows_clean([
        [kb_button("inv_weapons", "menu_inv_weapon"), kb_button("inv_clothes", "menu_inv_clothes")],
        [kb_button("back", "menu_back")],
    ])
    return InlineKeyboardMarkup(rows)


async def show_inventory_main(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await safe_answer(query, )
    await safe_edit_text(query.message, "🎒 کوله‌پشتی — یه بخش رو انتخاب کن:", reply_markup=inventory_main_keyboard())


async def show_inventory_category(update: Update, context: ContextTypes.DEFAULT_TYPE, category):
    query = update.callback_query
    if query and not query._answered if hasattr(query, "_answered") else False:
        pass
    data = load_data()
    user = data["users"].get(str(update.effective_user.id))
    if not user:
        return
    items_all = get_shop_items("weapon") if category == "weapon" else get_shop_items("clothes")
    owned_key = "weapons_owned" if category == "weapon" else "clothes_owned"
    equipped_key = "weapon_equipped" if category == "weapon" else "clothes_equipped"
    owned = user.get(owned_key, [])
    rows = []
    if not owned:
        text = "چیزی تو این بخش نداری! برو از سوپرمارکت بخر."
    else:
        title = "🗡 اسلحه‌های من:" if category == "weapon" else "👕 لباس‌های من:"
        text = title
        for key in owned:
            item = find_item(items_all, key)
            if not item:
                continue
            is_equipped = user.get(equipped_key) == key
            label = f"{item['name']} {'✅ تجهیزشده' if is_equipped else ''}"
            name_btn = InlineKeyboardButton(label, callback_data="noop")
            action_btn = kb_button("unequip_item", f"unequip:{category}") if is_equipped \
                else kb_button("equip_item", f"equip:{category}:{key}")
            sell_btn = kb_button("sell_item", f"sell:{category}:{key}")
            row = [b for b in [name_btn, action_btn, sell_btn] if b is not None]
            if row:
                rows.append(row)
    back_btn = kb_button("back", "menu_inventory")
    if back_btn:
        rows.append([back_btn])
    await safe_edit_text(query.message, text, reply_markup=InlineKeyboardMarkup(rows))


async def equip_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if is_group_panel_blocked(update):
        await safe_answer(query, "این پنل مال خودت نیست! بگو «فمبوی» تا پنل خودتو بگیری.", show_alert=True)
        return
    touch_group_panel(update)
    await safe_answer(query, )
    _, category, key = query.data.split(":", 2)
    data = load_data()
    user = data["users"].get(str(update.effective_user.id))
    if not user:
        return
    owned_key = "weapons_owned" if category == "weapon" else "clothes_owned"
    equipped_key = "weapon_equipped" if category == "weapon" else "clothes_equipped"
    if key not in user.get(owned_key, []):
        await safe_answer(query, "این آیتم رو نداری.", show_alert=True)
        return
    user[equipped_key] = key
    save_data()
    await show_inventory_category(update, context, category)


async def unequip_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if is_group_panel_blocked(update):
        await safe_answer(query, "این پنل مال خودت نیست! بگو «فمبوی» تا پنل خودتو بگیری.", show_alert=True)
        return
    touch_group_panel(update)
    await safe_answer(query, )
    _, category = query.data.split(":", 1)
    data = load_data()
    user = data["users"].get(str(update.effective_user.id))
    if not user:
        return
    equipped_key = "weapon_equipped" if category == "weapon" else "clothes_equipped"
    user[equipped_key] = None
    save_data()
    await show_inventory_category(update, context, category)


# ----------------------------------------------------------------------------
# منوی اصلی روتر
# ----------------------------------------------------------------------------
async def menu_router(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    action = query.data
    if is_group_panel_blocked(update):
        await safe_answer(query, "این پنل مال خودت نیست! بگو «فمبوی» تا پنل خودتو بگیری.", show_alert=True)
        return
    touch_group_panel(update)
    if action == "menu_feed":
        await show_fridge(update, context)
    elif action == "menu_battle":
        await do_battle(update, context)
    elif action == "menu_profile":
        await safe_answer(query, )
        await show_profile(query, update.effective_user.id, edit=True)
    elif action == "menu_leaderboard":
        await show_leaderboard(update, context)
    elif action == "menu_vip":
        await show_vip(update, context)
    elif action == "menu_help":
        await show_help(update, context)
    elif action == "menu_market":
        await show_market_main(update, context)
    elif action == "menu_market_food":
        await show_market_category(update, context, "food")
    elif action == "menu_market_weapon":
        await show_market_category(update, context, "weapon")
    elif action == "menu_market_clothes":
        await show_market_category(update, context, "clothes")
    elif action == "menu_market_character":
        await show_market_characters(update, context)
    elif action == "menu_inventory":
        await show_inventory_main(update, context)
    elif action == "menu_inv_weapon":
        await safe_answer(query, )
        await show_inventory_category(update, context, "weapon")
    elif action == "menu_inv_clothes":
        await safe_answer(query, )
        await show_inventory_category(update, context, "clothes")
    elif action == "menu_games":
        await show_games_menu(update, context)
    elif action == "menu_rematch":
        await show_rematch_menu(update, context)
    elif action == "menu_swap_char":
        await show_swap_char_menu(update, context)
    elif action == "menu_partner":
        await show_partner_menu(update, context)
    elif action == "menu_partner_child_view":
        await show_child_status(update, context)
    elif action == "menu_mychars":
        await show_my_characters(update, context)
    elif action == "menu_back":
        await safe_answer(query, )
        data = load_data()
        user = data["users"].get(str(update.effective_user.id))
        text, entities = render_text("welcome")
        await safe_edit_text(query.message, text, entities=entities, reply_markup=main_menu_keyboard(user))
    elif action == "menu_admin_panel":
        if update.effective_user.id != ADMIN_ID:
            await safe_answer(query, "دسترسی نداری.", show_alert=True)
            return
        await safe_answer(query, )
        text, entities = render_text("admin_welcome")
        await query.message.reply_text(text, entities=entities, reply_markup=admin_main_keyboard())


async def feeduse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if is_group_panel_blocked(update):
        await safe_answer(update.callback_query, "این پنل مال خودت نیست! بگو «فمبوی» تا پنل خودتو بگیری.", show_alert=True)
        return
    touch_group_panel(update)
    key = update.callback_query.data.split(":", 1)[1]
    await do_feed_action(update, context, food_key=key)


# ============================================================================
# بازی‌ها: دوز (XO) و سنگ‌کاغذقیچی — پیوی (صف انتظار) و گروه (چالش/پیوستن)
# ============================================================================
def new_game_id():
    data = load_data()
    gid = str(data["next_game_id"])
    data["next_game_id"] += 1
    return gid


def is_game_enabled(game_type):
    data = load_data()
    return data.get("game_enabled", {}).get(game_type, True)


def games_menu_keyboard():
    rows = rows_clean([
        [
            kb_button("game_xo", "game_pick:xo") if is_game_enabled("xo") else None,
            kb_button("game_rps", "game_pick:rps") if is_game_enabled("rps") else None,
        ],
        [
            kb_button("game_dice", "game_pick:dice") if is_game_enabled("dice") else None,
            kb_button("game_hokm", "hokm_pick") if is_game_enabled("hokm") else None,
        ],
        [kb_button("game_rematch", "menu_rematch")],
        [kb_button("back", "menu_back")],
    ])
    return InlineKeyboardMarkup(rows)


async def show_games_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await safe_answer(query, )
    text, entities = render_text("game_pick_prompt")
    await safe_edit_text(query.message, text, entities=entities, reply_markup=games_menu_keyboard())


async def show_rematch_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if update.effective_chat.type != ChatType.PRIVATE:
        await safe_answer(query, "دعوت مجدد فقط تو پیوی ربات کار می‌کنه.", show_alert=True)
        return
    await safe_answer(query, )
    data = load_data()
    uid = update.effective_user.id
    prev_opponents = data["played_with"].get(str(uid), [])
    rows = []
    for opp_id in prev_opponents[:15]:
        opp = data["users"].get(str(opp_id))
        if not opp or opp.get("character_id") is None:
            continue
        rows.append([InlineKeyboardButton(f"🤝 {user_display_name(opp)}", callback_data=f"rematch_pick:{opp_id}")])
    has_opponents = bool(rows)
    back_btn = kb_button("back", "menu_games")
    if back_btn:
        rows.append([back_btn])
    text = "🤝 با کدوم حریف قبلی می‌خوای دوباره بازی کنی؟" if has_opponents else "هنوز با کسی بازی نکردی که بشه دوباره دعوتش کرد."
    await safe_edit_text(query.message, text, reply_markup=InlineKeyboardMarkup(rows))


async def rematch_pick_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await safe_answer(query, )
    opp_id = int(query.data.split(":", 1)[1])
    rows = rows_clean([[
        kb_button("game_xo", f"rematch_game:{opp_id}:xo") if is_game_enabled("xo") else None,
        kb_button("game_rps", f"rematch_game:{opp_id}:rps") if is_game_enabled("rps") else None,
        kb_button("game_dice", f"rematch_game:{opp_id}:dice") if is_game_enabled("dice") else None,
    ]])
    if not rows:
        await query.message.reply_text("فعلاً هیچ بازی‌ای فعال نیست.")
        return
    await safe_edit_text(query.message, "کدوم بازی رو می‌خوای پیشنهاد بدی؟", reply_markup=InlineKeyboardMarkup(rows))


async def rematch_game_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    _, opp_id_s, game_type = query.data.split(":")
    opp_id = int(opp_id_s)
    data = load_data()
    uid = update.effective_user.id
    user = data["users"].get(str(uid))
    opp = data["users"].get(str(opp_id))
    if not is_game_enabled(game_type):
        await safe_answer(query, "این بازی الان توسط ادمین غیرفعاله.", show_alert=True)
        return
    if not user or not opp or opp.get("character_id") is None:
        await safe_answer(query, "این کاربر دیگه در دسترس نیست.", show_alert=True)
        return
    if user.get("current_game") or opp.get("current_game"):
        text, _ = render_text("game_already_playing")
        await safe_answer(query, text, show_alert=True)
        return

    invite_id = str(data["next_invite_id"])
    data["next_invite_id"] += 1
    data["game_invites"][invite_id] = {"from": uid, "to": opp_id, "game_type": game_type, "ts": now_ts()}
    save_data()

    await safe_answer(query, "دعوتت فرستاده شد! منتظر پاسخش باش.", show_alert=True)
    rows = rows_clean([[
        kb_button("invite_accept", f"invite_accept:{invite_id}"),
        kb_button("invite_reject", f"invite_reject:{invite_id}"),
    ]])
    try:
        await context.bot.send_message(
            chat_id=opp_id,
            text=f"🤝 {user_display_name(user)} می‌خواد باهات «{GAME_NAMES_FA[game_type]}» بازی کنه!",
            reply_markup=InlineKeyboardMarkup(rows),
        )
    except (BadRequest, Forbidden):
        await query.message.reply_text("نتونستم بهش پیام بدم؛ شاید ربات رو بلاک کرده.")


async def invite_decision_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    accepted = query.data.startswith("invite_accept:")
    invite_id = query.data.split(":", 1)[1]
    data = load_data()
    invite = data["game_invites"].pop(invite_id, None)
    save_data()
    if not invite:
        await safe_answer(query, "این دعوت دیگه معتبر نیست.", show_alert=True)
        return
    from_id, to_id, game_type = invite["from"], invite["to"], invite["game_type"]
    if update.effective_user.id != to_id:
        await safe_answer(query, "این دعوت برای تو نیست.", show_alert=True)
        return

    if not accepted:
        await safe_answer(query, "دعوت رد شد.", show_alert=True)
        try:
            await context.bot.send_message(chat_id=from_id, text="😅 حریفت دعوتت رو رد کرد.")
        except (BadRequest, Forbidden):
            pass
        await safe_edit_text(query.message, "❌ دعوت رو رد کردی.")
        return

    from_user = data["users"].get(str(from_id))
    to_user = data["users"].get(str(to_id))
    if not from_user or not to_user or from_user.get("current_game") or to_user.get("current_game"):
        await safe_answer(query, "الان امکان شروع بازی نیست (شاید یکیتون وسط یه بازی دیگه‌ست).", show_alert=True)
        return

    await safe_answer(query, "بازی شروع شد! 🎮", show_alert=True)
    await safe_edit_text(query.message, "✅ دعوت رو قبول کردی، بازی شروع شد!")
    await start_game_between(context, from_id, to_id, game_type, "pm")


def user_display_name(u):
    return u.get("custom_name") or u.get("first_name") or str(u.get("user_id"))


def get_uname(uid):
    data = load_data()
    u = data["users"].get(str(uid))
    return user_display_name(u) if u else str(uid)


def touch_game(game):
    game["last_activity"] = now_ts()


def settle_game_reward(data, game, winner_uid):
    """جایزهٔ پایان بازی رو حساب می‌کنه: اگه بازی قماری بود (stake>0)، برنده کل «پات» (دو برابر
    ورودی) رو می‌گیره؛ وگرنه همون جایزهٔ ثابت رایگان. یه متن کوتاه برای اضافه‌شدن به پیام برمی‌گردونه."""
    stake = game.get("stake", 0)
    w = data["users"].get(str(winner_uid))
    if stake:
        pot = stake * 2
        if w:
            w["points"] += pot
        return f"\n💰 {pot} {CURRENCY_NAME} پات رو برد!"
    else:
        if w:
            w["points"] += 8
        return f"\n+8 {CURRENCY_NAME}"


def settle_game_draw_refund(data, game):
    """موقع مساوی: اگه قماری بود، ورودی هر دو نفر برمی‌گرده بهشون."""
    stake = game.get("stake", 0)
    if not stake:
        return ""
    for uid in game["players"]:
        u = data["users"].get(str(uid))
        if u:
            u["points"] += stake
    return f"\n💰 چون مساوی شد، {stake} {CURRENCY_NAME} ورودی هر دو نفر برگشت."


def record_played_with(data, uid1, uid2):
    """تاریخچهٔ حریف‌های قبلی هرکس رو نگه می‌داره تا بعداً بشه دوباره دعوتشون کرد."""
    pw = data["played_with"]
    for a, b in ((uid1, uid2), (uid2, uid1)):
        lst = pw.setdefault(str(a), [])
        if b in lst:
            lst.remove(b)
        lst.insert(0, b)
        del lst[20:]  # فقط ۲۰ حریف اخیر رو نگه می‌داریم


async def game_cancel_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    gid = query.data.split(":", 1)[1]
    data = load_data()
    game = data["games"].get(gid)
    if not game:
        await safe_answer(query, "این بازی دیگه فعال نیست.", show_alert=True)
        return
    uid = update.effective_user.id
    if uid not in game["players"]:
        await safe_answer(query, "تو بازیکن این بازی نیستی.", show_alert=True)
        return

    stake = game.get("stake", 0)
    if stake and game.get("status") == "active":
        # اگه هر دو نفر وارد بازی شده بودن، ورودی هر دو برمی‌گرده (لغو = مساوی)
        for pid in game["players"]:
            u = data["users"].get(str(pid))
            if u:
                u["points"] += stake
    elif stake and game.get("status") == "waiting":
        host = data["users"].get(str(game["players"][0]))
        if host:
            host["points"] += stake

    for pid in game["players"]:
        u = data["users"].get(str(pid))
        if u:
            u["current_game"] = None
    data["games"].pop(gid, None)
    save_data()

    canceller_name = get_uname(uid)
    cancel_text = f"🚫 بازی توسط {canceller_name} لغو شد." + (f"\n💰 ورودی‌ها برگشت." if stake else "")
    try:
        if game["scope"] == "group":
            mid = game["message_ids"].get("group")
            if mid:
                await context.bot.edit_message_text(chat_id=game["chat_id"], message_id=mid, text=cancel_text)
        else:
            for pid in game["players"]:
                mid = game["message_ids"].get(str(pid))
                if mid:
                    await context.bot.edit_message_text(chat_id=pid, message_id=mid, text=cancel_text)
    except BadRequest:
        pass
    await safe_answer(query, "بازی لغو شد.", show_alert=True)


async def game_chat_toggle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    gid = query.data.split(":", 1)[1]
    data = load_data()
    game = data["games"].get(gid)
    if not game or game.get("scope") != "pm":
        await safe_answer(query, "چت با رقیب فقط تو پیوی و وسط یه بازی فعاله.", show_alert=True)
        return
    uid = update.effective_user.id
    if uid not in game["players"]:
        await safe_answer(query, "تو بازیکن این بازی نیستی.", show_alert=True)
        return

    game["chat_active"] = not game.get("chat_active", False)
    save_data()
    state_text = "💬 چت با رقیب فعال شد! هر پیامی بفرستی مستقیم براش می‌ره." if game["chat_active"] \
        else "💬 چت با رقیب خاموش شد."
    await safe_answer(query, state_text, show_alert=True)

    # کیبورد رو دوباره بساز تا برچسب دکمهٔ چت آپدیت بشه
    for pid in game["players"]:
        mid = game["message_ids"].get(str(pid))
        if not mid:
            continue
        if game["type"] == "xo":
            kb = xo_render_keyboard(game)
        elif game["type"] == "dice":
            kb = dice_keyboard(game)
        else:
            kb = rps_keyboard(game)
        try:
            await context.bot.edit_message_reply_markup(chat_id=pid, message_id=mid, reply_markup=kb)
        except BadRequest:
            pass


# ---------- XO ----------
def xo_check_winner(board):
    lines = [
        (0, 1, 2), (3, 4, 5), (6, 7, 8),
        (0, 3, 6), (1, 4, 7), (2, 5, 8),
        (0, 4, 8), (2, 4, 6),
    ]
    for a, b, c in lines:
        if board[a] and board[a] == board[b] == board[c]:
            return board[a]
    if all(board):
        return "draw"
    return None


def game_control_row(game):
    """ردیف دکمه‌های کنترلی زیر هر بازی: لغو بازی (همیشه) + چت با رقیب (فقط تو پیوی)."""
    row = []
    cancel_btn = kb_button("game_cancel", f"game_cancel:{game['id']}")
    if cancel_btn:
        row.append(cancel_btn)
    if game.get("scope") == "pm":
        chat_label = "💬 پایان چت" if game.get("chat_active") else None
        chat_btn = kb_button("game_chat", f"game_chat:{game['id']}", override_text=chat_label) if chat_label \
            else kb_button("game_chat", f"game_chat:{game['id']}")
        if chat_btn:
            row.append(chat_btn)
    return [row] if row else []


def xo_render_keyboard(game):
    symbols = {"X": "❌", "O": "⭕", None: "▫️"}
    rows = []
    board = game["board"]
    game_id = game["id"]
    for r in range(3):
        row = []
        for c in range(3):
            idx = r * 3 + c
            row.append(
                InlineKeyboardButton(symbols[board[idx]], callback_data=f"xo_move:{game_id}:{idx}")
            )
        rows.append(row)
    rows.extend(game_control_row(game))
    return InlineKeyboardMarkup(rows)


def xo_status_text(game):
    p1, p2 = game["players"]
    return render_text(
        "xo_turn_status",
        p1=get_uname(p1), p2=get_uname(p2), turn=get_uname(game["turn"]),
    )


async def xo_broadcast(context, game):
    text, entities = xo_status_text(game)
    kb = xo_render_keyboard(game)
    if game["scope"] == "group":
        chat_id = game["chat_id"]
        msg_id = game["message_ids"].get("group")
        try:
            await context.bot.edit_message_text(chat_id=chat_id, message_id=msg_id, text=text, entities=entities, reply_markup=kb)
        except BadRequest:
            pass
    else:
        for uid in game["players"]:
            chat_id = uid
            msg_id = game["message_ids"].get(str(uid))
            try:
                await context.bot.edit_message_text(chat_id=chat_id, message_id=msg_id, text=text, entities=entities, reply_markup=kb)
            except BadRequest:
                pass


async def xo_finish(context, game, result):
    data = load_data()
    p1, p2 = game["players"]
    tb = TextBuilder()
    if result == "draw":
        tb.add_rendered(*xo_status_text(game))
        tb.add_text("\n\n")
        tb.add_rendered(*render_text("xo_draw"))
        tb.add_text(settle_game_draw_refund(data, game))
        save_data()
    else:
        winner_uid = p1 if result == "X" else p2
        note = settle_game_reward(data, game, winner_uid)
        save_data()
        base_text, base_entities = xo_status_text(game)
        header = base_text.split("\nنوبت")[0]
        tb.add_text(header)
        tb.add_text("\n\n")
        tb.add_rendered(*render_text("xo_win", winner=get_uname(winner_uid)))
        tb.add_text(note)
    final_text, final_entities = tb.build()
    kb = xo_render_keyboard(game)
    if game["scope"] == "group":
        try:
            await context.bot.edit_message_text(
                chat_id=game["chat_id"], message_id=game["message_ids"].get("group"),
                text=final_text, entities=final_entities, reply_markup=kb,
            )
        except BadRequest:
            pass
    else:
        for uid in game["players"]:
            try:
                await context.bot.edit_message_text(
                    chat_id=uid, message_id=game["message_ids"].get(str(uid)),
                    text=final_text, entities=final_entities, reply_markup=kb,
                )
            except BadRequest:
                pass
    for uid in game["players"]:
        u = data["users"].get(str(uid))
        if u:
            u["current_game"] = None
    data["games"].pop(game["id"], None)
    save_data()


async def xo_move_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    _, game_id, idx_s = query.data.split(":")
    idx = int(idx_s)
    data = load_data()
    game = data["games"].get(game_id)
    if not game or game.get("status") != "active":
        await safe_answer(query, "این بازی دیگه فعال نیست.", show_alert=True)
        return
    uid = update.effective_user.id
    if uid not in game["players"]:
        await safe_answer(query, "تو بازیکن این بازی نیستی.", show_alert=True)
        return
    if game["turn"] != uid:
        await safe_answer(query, "نوبت تو نیست!", show_alert=True)
        return
    if game["board"][idx] is not None:
        await safe_answer(query, "این خونه قبلاً پر شده.", show_alert=True)
        return

    symbol = "X" if game["players"][0] == uid else "O"
    game["board"][idx] = symbol
    touch_game(game)
    result = xo_check_winner(game["board"])
    if result:
        await safe_answer(query, )
        await xo_finish(context, game, result)
        return

    other = game["players"][1] if game["players"][0] == uid else game["players"][0]
    game["turn"] = other
    save_data()
    await safe_answer(query, )
    await xo_broadcast(context, game)


# ---------- سنگ‌کاغذقیچی ----------
RPS_EMOJI = {"rock": "✊", "paper": "✋", "scissors": "✌️"}
RPS_NAME = {"rock": "سنگ", "paper": "کاغذ", "scissors": "قیچی"}
RPS_BEATS = {"rock": "scissors", "paper": "rock", "scissors": "paper"}


def rps_keyboard(game):
    game_id = game["id"]
    rows = [[
        InlineKeyboardButton("✊ سنگ", callback_data=f"rps_choice:{game_id}:rock"),
        InlineKeyboardButton("✋ کاغذ", callback_data=f"rps_choice:{game_id}:paper"),
        InlineKeyboardButton("✌️ قیچی", callback_data=f"rps_choice:{game_id}:scissors"),
    ]]
    rows.extend(game_control_row(game))
    return InlineKeyboardMarkup(rows)


def rps_status_text(game):
    p1, p2 = game["players"]
    return render_text("rps_prompt", p1=get_uname(p1), p2=get_uname(p2))


async def rps_finish(context, game):
    data = load_data()
    p1, p2 = game["players"]
    c1 = game["choices"].get(str(p1))
    c2 = game["choices"].get(str(p2))
    if c1 == c2:
        winner_uid = None
    elif RPS_BEATS[c1] == c2:
        winner_uid = p1
    else:
        winner_uid = p2

    tb = TextBuilder()
    tb.add_text(f"{get_uname(p1)}: {RPS_EMOJI[c1]} {RPS_NAME[c1]}\n")
    tb.add_text(f"{get_uname(p2)}: {RPS_EMOJI[c2]} {RPS_NAME[c2]}\n\n")
    if winner_uid:
        note = settle_game_reward(data, game, winner_uid)
        save_data()
        tb.add_rendered(*render_text("rps_win", winner=get_uname(winner_uid)))
        tb.add_text(note)
    else:
        tb.add_rendered(*render_text("rps_draw", choice=RPS_NAME[c1]))
        tb.add_text(settle_game_draw_refund(data, game))
        save_data()
    result_text, result_entities = tb.build()

    if game["scope"] == "group":
        try:
            await context.bot.edit_message_text(
                chat_id=game["chat_id"], message_id=game["message_ids"].get("group"),
                text=result_text, entities=result_entities,
            )
        except BadRequest:
            pass
    else:
        for uid in game["players"]:
            try:
                await context.bot.edit_message_text(
                    chat_id=uid, message_id=game["message_ids"].get(str(uid)),
                    text=result_text, entities=result_entities,
                )
            except BadRequest:
                pass

    for uid in game["players"]:
        u = data["users"].get(str(uid))
        if u:
            u["current_game"] = None
    data["games"].pop(game["id"], None)
    save_data()


async def rps_choice_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    _, game_id, choice = query.data.split(":")
    data = load_data()
    game = data["games"].get(game_id)
    if not game or game.get("status") != "active":
        await safe_answer(query, "این بازی دیگه فعال نیست.", show_alert=True)
        return
    uid = update.effective_user.id
    if uid not in game["players"]:
        await safe_answer(query, "تو بازیکن این بازی نیستی.", show_alert=True)
        return
    if str(uid) in game["choices"]:
        await safe_answer(query, "قبلاً انتخاب کردی، منتظر حریف باش.", show_alert=True)
        return
    game["choices"][str(uid)] = choice
    touch_game(game)
    save_data()
    await safe_answer(query, f"انتخاب کردی: {RPS_EMOJI[choice]}", show_alert=True)

    p1, p2 = game["players"]
    if str(p1) in game["choices"] and str(p2) in game["choices"]:
        await rps_finish(context, game)


# ---------- تاس دوئل ----------
DICE_WIN_SCORE = 2  # بهترین از ۳ راند


def dice_keyboard(game):
    game_id = game["id"]
    rows = []
    btn = kb_button("dice_roll", f"dice_roll:{game_id}")
    if btn:
        rows.append([btn])
    rows.extend(game_control_row(game))
    return InlineKeyboardMarkup(rows)


def dice_status_text(game):
    p1, p2 = game["players"]
    return render_text(
        "dice_prompt",
        p1=get_uname(p1), p2=get_uname(p2),
        score1=game["scores"][str(p1)], score2=game["scores"][str(p2)],
        round=game["round"],
    )


async def dice_broadcast(context, game, text=None, entities=None):
    if text is None:
        text, entities = dice_status_text(game)
    kb = dice_keyboard(game)
    if game["scope"] == "group":
        try:
            await context.bot.edit_message_text(
                chat_id=game["chat_id"], message_id=game["message_ids"].get("group"),
                text=text, entities=entities, reply_markup=kb,
            )
        except BadRequest:
            pass
    else:
        for uid in game["players"]:
            try:
                await context.bot.edit_message_text(
                    chat_id=uid, message_id=game["message_ids"].get(str(uid)),
                    text=text, entities=entities, reply_markup=kb,
                )
            except BadRequest:
                pass


async def dice_finish(context, game, winner_uid):
    data = load_data()
    note = settle_game_reward(data, game, winner_uid)
    save_data()
    text, entities = render_text("dice_win", winner=get_uname(winner_uid))
    text += note
    if game["scope"] == "group":
        try:
            await context.bot.edit_message_text(
                chat_id=game["chat_id"], message_id=game["message_ids"].get("group"), text=text, entities=entities,
            )
        except BadRequest:
            pass
    else:
        for uid in game["players"]:
            try:
                await context.bot.edit_message_text(
                    chat_id=uid, message_id=game["message_ids"].get(str(uid)), text=text, entities=entities,
                )
            except BadRequest:
                pass
    for uid in game["players"]:
        u = data["users"].get(str(uid))
        if u:
            u["current_game"] = None
    data["games"].pop(game["id"], None)
    save_data()


async def dice_roll_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    gid = query.data.split(":", 1)[1]
    data = load_data()
    game = data["games"].get(gid)
    if not game or game.get("status") != "active" or game.get("type") != "dice":
        await safe_answer(query, "این بازی دیگه فعال نیست.", show_alert=True)
        return
    uid = update.effective_user.id
    if uid not in game["players"]:
        await safe_answer(query, "تو بازیکن این بازی نیستی.", show_alert=True)
        return
    if str(uid) in game["rolls"]:
        await safe_answer(query, "قبلاً تاست رو انداختی، منتظر حریف باش.", show_alert=True)
        return

    value = random.randint(1, 6)
    game["rolls"][str(uid)] = value
    touch_game(game)
    save_data()
    text, _ = render_text("dice_waiting_other", value=value)
    await safe_answer(query, text, show_alert=True)

    p1, p2 = game["players"]
    if str(p1) not in game["rolls"] or str(p2) not in game["rolls"]:
        return

    v1, v2 = game["rolls"][str(p1)], game["rolls"][str(p2)]
    if v1 == v2:
        game["rolls"] = {}
        save_data()
        text, entities = render_text("dice_round_tie", value=v1)
        await dice_broadcast(context, game, text=text, entities=entities)
        return

    round_winner_uid = p1 if v1 > v2 else p2
    game["scores"][str(round_winner_uid)] += 1
    round_result_text, round_result_entities = render_text(
        "dice_round_result", p1=get_uname(p1), p2=get_uname(p2), v1=v1, v2=v2, round_winner=get_uname(round_winner_uid),
    )

    if game["scores"][str(round_winner_uid)] >= DICE_WIN_SCORE:
        await dice_broadcast(context, game, text=round_result_text, entities=round_result_entities)
        await asyncio.sleep(3)
        await dice_finish(context, game, round_winner_uid)
        return

    game["round"] += 1
    game["rolls"] = {}
    save_data()
    await dice_broadcast(context, game, text=round_result_text, entities=round_result_entities)
    await asyncio.sleep(3)
    await dice_broadcast(context, game)


# ============================================================================
# حکم (Hokm) — بازی پاسور ۴نفرهٔ تیمی (پیوی صف انتظار + گروه)
# ============================================================================
HOKM_SUITS = ["♠", "♥", "♦", "♣"]
HOKM_RANKS = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]
HOKM_RANK_VALUE = {r: i for i, r in enumerate(HOKM_RANKS)}
HOKM_TRICKS_TO_WIN_HAND = 7
HOKM_HANDS_TO_WIN_MATCH = 3  # ساده‌سازی: بهترین‌از‌۵ دست، نه کُت کامل واقعی


def hokm_new_deck():
    deck = [rank + suit for suit in HOKM_SUITS for rank in HOKM_RANKS]
    random.shuffle(deck)
    return deck


def hokm_card_suit(card):
    return card[-1]


def hokm_card_rank(card):
    return card[:-1]


def hokm_sort_hand(hand):
    return sorted(hand, key=lambda c: (HOKM_SUITS.index(hokm_card_suit(c)), HOKM_RANK_VALUE[hokm_card_rank(c)]))


def hokm_legal_cards(hand, led_suit):
    if led_suit is None:
        return hand
    same_suit = [c for c in hand if hokm_card_suit(c) == led_suit]
    return same_suit if same_suit else hand


def touch_hokm(table):
    table["last_activity"] = now_ts()


def hokm_team_of(table, uid):
    return "A" if uid in (table["players"][0], table["players"][2]) else "B"


async def hokm_broadcast_public(context, table, text):
    if table["scope"] == "group":
        try:
            await context.bot.send_message(chat_id=table["chat_id"], text=text)
        except (BadRequest, Forbidden):
            pass
    else:
        for p in table["players"]:
            try:
                await context.bot.send_message(chat_id=p, text=text)
            except (BadRequest, Forbidden):
                pass


async def hokm_send_trump_prompt(context, table, hakem_id):
    hand_text = "🃏 کارت‌های تو (۵ تا): " + " ".join(table["hands"][str(hakem_id)]) + "\n\nحکم رو انتخاب کن:"
    rows = [[InlineKeyboardButton(s, callback_data=f"hokm_trump:{table['id']}:{s}") for s in HOKM_SUITS]]
    try:
        await context.bot.send_message(chat_id=hakem_id, text=hand_text, reply_markup=InlineKeyboardMarkup(rows))
    except (BadRequest, Forbidden):
        pass


async def hokm_send_turn_prompt(context, table):
    """به بازیکنی که نوبتشه، دستش رو با دکمهٔ کارت‌های مجاز می‌فرسته."""
    uid = table["players"][table["turn_index"]]
    hand = table["hands"][str(uid)]
    led_suit = None
    if table["current_trick"]:
        first_card = list(table["current_trick"].values())[0]
        led_suit = hokm_card_suit(first_card)
    legal = hokm_legal_cards(hand, led_suit)

    rows, row = [], []
    for card in hand:
        if card in legal:
            row.append(InlineKeyboardButton(card, callback_data=f"hokm_play:{table['id']}:{card}"))
        else:
            row.append(InlineKeyboardButton(f"·{card}·", callback_data="noop"))
        if len(row) == 4:
            rows.append(row)
            row = []
    if row:
        rows.append(row)

    trick_lines = "\n".join(f"{get_uname(int(p))}: {c}" for p, c in table["current_trick"].items())
    text = (
        f"🃏 حکم: {table['trump']} | امتیاز دست: تیم۱ {table['tricks_won']['A']} - تیم۲ {table['tricks_won']['B']}\n"
        f"دست فعلی:\n{trick_lines or '—'}\n\nنوبت توئه! یه کارت انتخاب کن:"
    )
    try:
        msg = await context.bot.send_message(chat_id=uid, text=text, reply_markup=InlineKeyboardMarkup(rows))
        table["message_ids"][str(uid)] = msg.message_id
        save_data()
    except (BadRequest, Forbidden):
        pass


async def hokm_start_table(context, players, scope, chat_id):
    data = load_data()
    table_id = str(data["next_hokm_id"])
    data["next_hokm_id"] += 1
    random.shuffle(players)  # چینش رندوم برای تیم‌بندی منصفانه (نفر ۰و۲ یه تیم، ۱و۳ تیم دیگه)
    hakem_index = 0
    deck = hokm_new_deck()
    hakem_id = players[hakem_index]

    table = {
        "id": table_id, "scope": scope, "chat_id": chat_id,
        "players": players, "status": "choosing_trump",
        "message_ids": {}, "last_activity": now_ts(),
        "hakem_index": hakem_index, "trump": None,
        "hands": {str(p): [] for p in players},
        "deck": deck[5:],
        "current_trick": {}, "trick_leader": hakem_id, "turn_index": hakem_index,
        "tricks_won": {"A": 0, "B": 0}, "hands_won": {"A": 0, "B": 0},
    }
    table["hands"][str(hakem_id)] = hokm_sort_hand(deck[:5])
    data["hokm_tables"][table_id] = table

    for p in players:
        u = data["users"].get(str(p))
        if u:
            u["current_game"] = f"hokm:{table_id}"
    for i in range(4):
        record_played_with(data, players[i], players[(i + 1) % 4])
    save_data()

    team_a, team_b = [players[0], players[2]], [players[1], players[3]]
    intro = (
        "🃏 بازی حکم شروع شد!\n\n"
        f"تیم ۱: {get_uname(team_a[0])} + {get_uname(team_a[1])}\n"
        f"تیم ۲: {get_uname(team_b[0])} + {get_uname(team_b[1])}\n\n"
        f"حاکم: {get_uname(hakem_id)} — داره حکم رو انتخاب می‌کنه..."
    )
    await hokm_broadcast_public(context, table, intro)
    await hokm_send_trump_prompt(context, table, hakem_id)


async def hokm_pick_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if update.effective_chat.type != ChatType.PRIVATE:
        await safe_answer(query, "برای صف پیوی حکم باید تو پیوی ربات باشی.", show_alert=True)
        return
    if not is_game_enabled("hokm"):
        await safe_answer(query, "بازی حکم الان توسط ادمین غیرفعاله.", show_alert=True)
        return
    data = load_data()
    uid = update.effective_user.id
    user = data["users"].get(str(uid))
    if not user or user.get("character_id") is None:
        await safe_answer(query, "اول /start رو بزن.", show_alert=True)
        return
    if user.get("current_game") or uid in data["hokm_queue"]:
        await safe_answer(query, "تو الان تو یه بازی یا صف دیگه‌ای هستی.", show_alert=True)
        return
    data["hokm_queue"].append(uid)
    save_data()
    await safe_answer(query, )
    if len(data["hokm_queue"]) >= 4:
        players = data["hokm_queue"][:4]
        data["hokm_queue"] = data["hokm_queue"][4:]
        save_data()
        await hokm_start_table(context, players, "pm", None)
    else:
        await query.message.reply_text(f"🃏 وارد صف حکم شدی! {len(data['hokm_queue'])}/4 نفر آماده‌ان...")


async def hokm_group_start_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_game_enabled("hokm"):
        await safe_answer(query, "بازی حکم الان توسط ادمین غیرفعاله.", show_alert=True)
        return
    data = load_data()
    host_id = update.effective_user.id
    host = data["users"].get(str(host_id))
    if not host or host.get("character_id") is None:
        await safe_answer(query, "اول باید تو پیوی ربات /start بزنی.", show_alert=True)
        return
    if host.get("current_game"):
        text, _ = render_text("game_already_playing")
        await safe_answer(query, text, show_alert=True)
        return
    table_id = str(data["next_hokm_id"])
    data["next_hokm_id"] += 1
    table = {
        "id": table_id, "scope": "group", "chat_id": update.effective_chat.id,
        "players": [host_id], "status": "waiting", "message_ids": {}, "last_activity": now_ts(),
    }
    data["hokm_tables"][table_id] = table
    save_data()
    await safe_answer(query, )
    rows = rows_clean([[kb_button("hokm_join", f"hokm_join:{table_id}")]])
    text = f"🃏 {user_display_name(host)} میز حکم راه انداخت! (1/4)\nبرای پیوستن دکمه رو بزن (باید تو پیوی ربات /start زده باشی)."
    msg = await query.message.reply_text(text, reply_markup=InlineKeyboardMarkup(rows))
    table["message_ids"]["group"] = msg.message_id
    save_data()


async def hokm_join_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    table_id = query.data.split(":", 1)[1]
    data = load_data()
    table = data["hokm_tables"].get(table_id)
    if not table or table.get("status") != "waiting":
        await safe_answer(query, "این میز دیگه در دسترس نیست.", show_alert=True)
        return
    uid = update.effective_user.id
    if uid in table["players"]:
        await safe_answer(query, "قبلاً پیوستی!", show_alert=True)
        return
    user = data["users"].get(str(uid))
    if not user or user.get("character_id") is None:
        await safe_answer(query, "اول باید تو پیوی ربات /start بزنی.", show_alert=True)
        return
    if user.get("current_game"):
        text, _ = render_text("game_already_playing")
        await safe_answer(query, text, show_alert=True)
        return

    table["players"].append(uid)
    touch_hokm(table)
    save_data()
    await safe_answer(query, "پیوستی! 🃏", show_alert=True)

    if len(table["players"]) < 4:
        rows = rows_clean([[kb_button("hokm_join", f"hokm_join:{table_id}")]])
        names = "، ".join(get_uname(p) for p in table["players"])
        text = f"🃏 میز حکم ({len(table['players'])}/4):\n{names}\nبرای پیوستن دکمه رو بزن."
        try:
            await context.bot.edit_message_text(
                chat_id=table["chat_id"], message_id=table["message_ids"]["group"], text=text, reply_markup=InlineKeyboardMarkup(rows)
            )
        except BadRequest:
            pass
        return

    players = table["players"]
    chat_id = table["chat_id"]
    data["hokm_tables"].pop(table_id, None)
    save_data()
    await hokm_start_table(context, players, "group", chat_id)


async def hokm_trump_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    _, table_id, suit = query.data.split(":")
    data = load_data()
    table = data["hokm_tables"].get(table_id)
    if not table or table.get("status") != "choosing_trump":
        await safe_answer(query, "این میز الان منتظر انتخاب حکم نیست.", show_alert=True)
        return
    hakem_id = table["players"][table["hakem_index"]]
    if update.effective_user.id != hakem_id:
        await safe_answer(query, "فقط حاکم می‌تونه حکم رو انتخاب کنه.", show_alert=True)
        return

    table["trump"] = suit
    table["status"] = "playing"
    touch_hokm(table)

    deck = table["deck"]
    players = table["players"]
    idx = 0
    for i, p in enumerate(players):
        need = 8 if i == table["hakem_index"] else 13
        cards = deck[idx: idx + need]
        idx += need
        table["hands"][str(p)] = hokm_sort_hand(table["hands"][str(p)] + cards)
    table["deck"] = []
    save_data()

    await safe_answer(query, f"حکم شد {suit}!", show_alert=True)
    announce = f"🎴 حکم این دوره: {suit}\nحاکم: {get_uname(hakem_id)}\n\nنوبت {get_uname(table['trick_leader'])}ه."
    await hokm_broadcast_public(context, table, announce)
    await hokm_send_turn_prompt(context, table)


async def hokm_finish_match(context, table, winning_team):
    data = load_data()
    winners = [table["players"][0], table["players"][2]] if winning_team == "A" else [table["players"][1], table["players"][3]]
    for w in winners:
        u = data["users"].get(str(w))
        if u:
            u["points"] += 20
    for p in table["players"]:
        u = data["users"].get(str(p))
        if u:
            u["current_game"] = None
    data["hokm_tables"].pop(table["id"], None)
    save_data()
    announce = f"🏆🏆 تیم {'۱' if winning_team == 'A' else '۲'} برندهٔ کل بازی حکم شد! (+20 {CURRENCY_NAME} برای هرکدوم از اعضاش)"
    await hokm_broadcast_public(context, table, announce)


async def hokm_finish_hand(context, table, winning_team):
    data = load_data()
    table["hands_won"][winning_team] += 1
    announce = (
        f"🎉 تیم {'۱' if winning_team == 'A' else '۲'} این دور رو با {table['tricks_won'][winning_team]} دست برد!\n"
        f"امتیاز کل: تیم۱ {table['hands_won']['A']} - تیم۲ {table['hands_won']['B']}"
    )
    await hokm_broadcast_public(context, table, announce)

    if table["hands_won"][winning_team] >= HOKM_HANDS_TO_WIN_MATCH:
        await hokm_finish_match(context, table, winning_team)
        return

    table["hakem_index"] = (table["hakem_index"] + 1) % 4
    table["trump"] = None
    table["status"] = "choosing_trump"
    table["tricks_won"] = {"A": 0, "B": 0}
    table["current_trick"] = {}
    deck = hokm_new_deck()
    hakem_id = table["players"][table["hakem_index"]]
    table["hands"] = {str(p): [] for p in table["players"]}
    table["hands"][str(hakem_id)] = hokm_sort_hand(deck[:5])
    table["deck"] = deck[5:]
    table["trick_leader"] = hakem_id
    table["turn_index"] = table["hakem_index"]
    touch_hokm(table)
    save_data()
    await hokm_send_trump_prompt(context, table, hakem_id)


async def hokm_play_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    _, table_id, card = query.data.split(":", 2)
    data = load_data()
    table = data["hokm_tables"].get(table_id)
    if not table or table.get("status") != "playing":
        await safe_answer(query, "این بازی دیگه فعال نیست.", show_alert=True)
        return
    uid = update.effective_user.id
    if table["players"][table["turn_index"]] != uid:
        await safe_answer(query, "نوبت تو نیست!", show_alert=True)
        return
    hand = table["hands"][str(uid)]
    if card not in hand:
        await safe_answer(query, "این کارت رو نداری.", show_alert=True)
        return

    led_suit = None
    if table["current_trick"]:
        led_suit = hokm_card_suit(list(table["current_trick"].values())[0])
    legal = hokm_legal_cards(hand, led_suit)
    if card not in legal:
        await safe_answer(query, "باید همون خالی که زمینه رو بازی کنی (اگه داری).", show_alert=True)
        return

    hand.remove(card)
    table["current_trick"][str(uid)] = card
    touch_hokm(table)
    await safe_answer(query, f"{card} رو بازی کردی.")

    if len(table["current_trick"]) < 4:
        table["turn_index"] = (table["turn_index"] + 1) % 4
        save_data()
        await hokm_send_turn_prompt(context, table)
        return

    trump = table["trump"]
    led_suit = hokm_card_suit(list(table["current_trick"].values())[0])

    def card_strength(c):
        suit, rank = hokm_card_suit(c), hokm_card_rank(c)
        if suit == trump:
            return (2, HOKM_RANK_VALUE[rank])
        elif suit == led_suit:
            return (1, HOKM_RANK_VALUE[rank])
        return (0, -1)

    winner_uid = int(max(table["current_trick"].items(), key=lambda kv: card_strength(kv[1]))[0])
    team = hokm_team_of(table, winner_uid)
    table["tricks_won"][team] += 1
    trick_summary = "\n".join(f"{get_uname(int(p))}: {c}" for p, c in table["current_trick"].items())
    table["current_trick"] = {}
    table["trick_leader"] = winner_uid
    table["turn_index"] = table["players"].index(winner_uid)
    save_data()

    announce = (
        f"🏆 برندهٔ این دست: {get_uname(winner_uid)}\n{trick_summary}\n"
        f"امتیاز: تیم۱ {table['tricks_won']['A']} - تیم۲ {table['tricks_won']['B']}"
    )
    await hokm_broadcast_public(context, table, announce)

    if table["tricks_won"][team] >= HOKM_TRICKS_TO_WIN_HAND:
        await hokm_finish_hand(context, table, team)
        return

    await hokm_send_turn_prompt(context, table)


# ---------- ساخت/شروع بازی ----------
async def start_game_between(context, p1, p2, game_type, scope, chat_id=None, stake=0):
    data = load_data()
    gid = new_game_id()
    game = {
        "id": gid,
        "type": game_type,
        "scope": scope,
        "chat_id": chat_id,
        "players": [p1, p2],
        "status": "active",
        "message_ids": {},
        "last_activity": now_ts(),
        "stake": stake,
        "chat_active": False,
    }
    if game_type == "xo":
        game["board"] = [None] * 9
        game["turn"] = p1
    elif game_type == "dice":
        game["scores"] = {str(p1): 0, str(p2): 0}
        game["round"] = 1
        game["rolls"] = {}
    else:
        game["choices"] = {}
    data["games"][gid] = game

    for uid in (p1, p2):
        u = data["users"].get(str(uid))
        if u:
            u["current_game"] = gid
    record_played_with(data, p1, p2)
    save_data()

    def render_for(gtype):
        if gtype == "xo":
            t, e = xo_status_text(game)
            return t, e, xo_render_keyboard(game)
        elif gtype == "dice":
            t, e = dice_status_text(game)
            return t, e, dice_keyboard(game)
        else:
            t, e = rps_status_text(game)
            return t, e, rps_keyboard(game)

    if scope == "group":
        text, entities, kb = render_for(game_type)
        msg = await context.bot.send_message(chat_id=chat_id, text=text, entities=entities, reply_markup=kb)
        game["message_ids"]["group"] = msg.message_id
    else:
        for uid in (p1, p2):
            text, entities, kb = render_for(game_type)
            try:
                msg = await context.bot.send_message(chat_id=uid, text=text, entities=entities, reply_markup=kb)
                game["message_ids"][str(uid)] = msg.message_id
            except (BadRequest, Forbidden):
                pass
    save_data()


async def game_pick_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """انتخاب بازی از منوی پیوی -> رفتن تو صف انتظار یا جفت شدن با نفر منتظر."""
    query = update.callback_query
    if update.effective_chat.type != ChatType.PRIVATE:
        text, _ = render_text("game_private_only")
        await safe_answer(query, text, show_alert=True)
        return
    game_type = query.data.split(":", 1)[1]
    if not is_game_enabled(game_type):
        await safe_answer(query, "این بازی الان توسط ادمین غیرفعاله.", show_alert=True)
        return
    data = load_data()
    uid = update.effective_user.id
    user = data["users"].get(str(uid))
    if not user or user["character_id"] is None:
        await safe_answer(query, "اول /start رو بزن.", show_alert=True)
        return
    if user.get("current_game"):
        text, _ = render_text("game_already_playing")
        await safe_answer(query, text, show_alert=True)
        return
    for gt in ("xo", "rps", "dice"):
        data["game_queue"][gt] = [e for e in data["game_queue"][gt] if e["uid"] != uid]

    queue = data["game_queue"][game_type]
    if queue:
        opponent_entry = queue.pop(0)
        save_data()
        text, _ = render_text("game_matched_pm")
        await safe_answer(query, text, show_alert=True)
        await start_game_between(context, opponent_entry["uid"], uid, game_type, "pm")
    else:
        queue.append({"uid": uid, "ts": now_ts()})
        save_data()
        btn = kb_button("game_cancel_queue", f"game_cancelq:{game_type}")
        kb = InlineKeyboardMarkup([[btn]] if btn else [])
        await safe_answer(query, )
        text, entities = render_text("game_waiting_pm", game_name=GAME_NAMES_FA[game_type])
        await query.message.reply_text(text, entities=entities, reply_markup=kb)


async def game_cancel_queue_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    game_type = query.data.split(":", 1)[1]
    data = load_data()
    uid = update.effective_user.id
    queue = data["game_queue"].get(game_type, [])
    if any(e["uid"] == uid for e in queue):
        data["game_queue"][game_type] = [e for e in queue if e["uid"] != uid]
        save_data()
        await safe_answer(query, "از صف انتظار خارج شدی.", show_alert=True)
    else:
        await safe_answer(query, "تو صف انتظار نبودی.", show_alert=True)


async def group_game_text_trigger(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """وقتی کسی تو گروه کلمهٔ «بازی» رو بفرسته، پنل انتخاب بازی رایگان باز می‌شه."""
    rows = rows_clean([
        [
            kb_button("game_xo", "group_new:xo") if is_game_enabled("xo") else None,
            kb_button("game_rps", "group_new:rps") if is_game_enabled("rps") else None,
        ],
        [kb_button("game_dice", "group_new:dice") if is_game_enabled("dice") else None],
        [kb_button("game_hokm", "hokm_group_start") if is_game_enabled("hokm") else None],
    ])
    if not rows:
        return
    text, entities = render_text("game_group_pick_prompt")
    await update.message.reply_text(text, entities=entities, reply_markup=InlineKeyboardMarkup(rows))


async def group_gamble_text_trigger(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """وقتی کسی تو گروه کلمهٔ «قمار» رو بفرسته، همون بازی‌ها ولی با ورودی/شرط‌بندی شروع می‌شن."""
    rows = rows_clean([
        [
            kb_button("game_xo", "group_gamble_new:xo") if is_game_enabled("xo") else None,
            kb_button("game_rps", "group_gamble_new:rps") if is_game_enabled("rps") else None,
        ],
        [kb_button("game_dice", "group_gamble_new:dice") if is_game_enabled("dice") else None],
    ])
    if not rows:
        return
    await update.message.reply_text("🎰 قمار! یه بازی انتخاب کن (بعدش ازت مبلغ ورودی رو می‌پرسم):", reply_markup=InlineKeyboardMarkup(rows))


async def group_gamble_new_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    game_type = query.data.split(":", 1)[1]
    if not is_game_enabled(game_type):
        await safe_answer(query, "این بازی الان توسط ادمین غیرفعاله.", show_alert=True)
        return ConversationHandler.END
    data = load_data()
    host_id = update.effective_user.id
    host = data["users"].get(str(host_id))
    if not host or host.get("character_id") is None:
        await safe_answer(query, "اول باید تو پیوی ربات /start بزنی.", show_alert=True)
        return ConversationHandler.END
    if host.get("current_game"):
        text, _ = render_text("game_already_playing")
        await safe_answer(query, text, show_alert=True)
        return ConversationHandler.END
    context.user_data["gamble_type"] = game_type
    context.user_data["gamble_chat_id"] = update.effective_chat.id
    await safe_answer(query, )
    text, entities = render_text("gamble_ask_stake", currency_name=CURRENCY_NAME)
    await query.message.reply_text(text, entities=entities)
    return GAMBLE_STAKE


async def group_gamble_stake_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    game_type = context.user_data.get("gamble_type")
    chat_id = context.user_data.get("gamble_chat_id")
    if not game_type:
        return ConversationHandler.END
    try:
        stake = int(update.message.text.strip())
        if stake <= 0:
            raise ValueError
    except ValueError:
        text, entities = render_text("gamble_invalid_stake")
        await update.message.reply_text(text, entities=entities)
        return GAMBLE_STAKE

    data = load_data()
    host_id = update.effective_user.id
    host = data["users"].get(str(host_id))
    if not host or host["points"] < stake:
        text, entities = render_text("gamble_insufficient_funds", amount=stake)
        await update.message.reply_text(text, entities=entities)
        return ConversationHandler.END

    host["points"] -= stake
    gid = new_game_id()
    game = {
        "id": gid, "type": game_type, "scope": "group", "chat_id": chat_id,
        "players": [host_id], "status": "waiting", "message_ids": {}, "last_activity": now_ts(),
        "stake": stake,
    }
    data["games"][gid] = game
    save_data()

    btn = kb_button("game_join", f"game_join:{gid}")
    kb = InlineKeyboardMarkup([[btn]] if btn else [])
    text, entities = render_text(
        "gamble_waiting", host_name=user_display_name(host), game_name=GAME_NAMES_FA[game_type],
        stake=stake, currency_name=CURRENCY_NAME,
    )
    msg = await update.message.reply_text(text, entities=entities, reply_markup=kb)
    data["games"][gid]["message_ids"]["group"] = msg.message_id
    save_data()
    return ConversationHandler.END


async def group_new_game_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    game_type = query.data.split(":", 1)[1]
    if not is_game_enabled(game_type):
        await safe_answer(query, "این بازی الان توسط ادمین غیرفعاله.", show_alert=True)
        return
    data = load_data()
    host_id = update.effective_user.id
    host = data["users"].get(str(host_id))
    if not host or host["character_id"] is None:
        await safe_answer(query, "اول باید تو پیوی ربات /start بزنی.", show_alert=True)
        return
    if host.get("current_game"):
        text, _ = render_text("game_already_playing")
        await safe_answer(query, text, show_alert=True)
        return

    gid = new_game_id()
    game = {
        "id": gid,
        "type": game_type,
        "scope": "group",
        "chat_id": update.effective_chat.id,
        "players": [host_id],
        "status": "waiting",
        "message_ids": {},
        "last_activity": now_ts(),
        "stake": 0,
    }
    data["games"][gid] = game
    save_data()

    await safe_answer(query, )
    btn = kb_button("game_join", f"game_join:{gid}")
    kb = InlineKeyboardMarkup([[btn]] if btn else [])
    text, entities = render_text("game_group_new", host_name=get_uname(host_id), game_name=GAME_NAMES_FA[game_type])
    msg = await safe_edit_text(query.message, text, entities=entities, reply_markup=kb)
    game["message_ids"]["group"] = msg.message_id
    save_data()


async def group_game_join_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    gid = query.data.split(":", 1)[1]
    data = load_data()
    game = data["games"].get(gid)
    if not game or game.get("status") != "waiting":
        await safe_answer(query, "این بازی دیگه در انتظار نیست.", show_alert=True)
        return
    joiner_id = update.effective_user.id
    if joiner_id == game["players"][0]:
        await safe_answer(query, "نمی‌تونی با خودت بازی کنی!", show_alert=True)
        return
    joiner = data["users"].get(str(joiner_id))
    if not joiner or joiner["character_id"] is None:
        await safe_answer(query, "اول باید تو پیوی ربات /start بزنی.", show_alert=True)
        return
    if joiner.get("current_game"):
        text, _ = render_text("game_already_playing")
        await safe_answer(query, text, show_alert=True)
        return

    stake = game.get("stake", 0)
    if stake:
        if joiner["points"] < stake:
            text, _ = render_text("gamble_insufficient_funds", amount=stake)
            await safe_answer(query, text, show_alert=True)
            return
        joiner["points"] -= stake

    host_id = game["players"][0]
    game_type = game["type"]
    chat_id = game["chat_id"]
    data["games"].pop(gid, None)
    save_data()

    text, _ = render_text("game_group_joined")
    await safe_answer(query, text, show_alert=True)
    await start_game_between(context, host_id, joiner_id, game_type, "group", chat_id=chat_id, stake=stake)


async def check_idle_games(context: ContextTypes.DEFAULT_TYPE):
    """جاب دوره‌ای: بازی‌ها (فعال یا در انتظار) و صف‌های انتظار پیوی که بیش از
    GAME_IDLE_TIMEOUT ثانیه بی‌فعالیت بودن رو خودکار می‌بنده/پاک می‌کنه.
    کل بدنه تو یه try/except عمومیه: یه خطای غیرمنتظره تو یه آبجکت بازی خراب
    نباید کل جاب رو (و به‌تبعش هر جاب دیگه‌ای که تو همون تیک اجرا می‌شد) بندازه."""
    try:
        await _check_idle_games_inner(context)
    except Exception:
        logger.exception("خطا تو جاب check_idle_games")


async def _check_idle_games_inner(context: ContextTypes.DEFAULT_TYPE):
    data = load_data()
    now = now_ts()
    changed = False

    for gid, game in list(data["games"].items()):
        last = game.get("last_activity", now)
        if now - last <= GAME_IDLE_TIMEOUT:
            continue
        data["games"].pop(gid, None)
        changed = True

        # اگه بازی «در انتظار» با ورودی/شرط‌بندی بود و کسی نپیوست، پول میزبان رو برگردون
        stake = game.get("stake", 0)
        if stake and game.get("status") == "waiting":
            host_id = game["players"][0]
            host = data["users"].get(str(host_id))
            if host:
                host["points"] += stake
                try:
                    await context.bot.send_message(
                        chat_id=host_id,
                        text=f"⌛️ کسی وارد بازی قمارت نشد؛ {stake} {CURRENCY_NAME} بهت برگشت.",
                    )
                except (BadRequest, Forbidden):
                    pass

        for uid in game.get("players", []):
            u = data["users"].get(str(uid))
            if u:
                u["current_game"] = None
        text, entities = render_text("game_timeout")
        try:
            if game["scope"] == "group":
                mid = game["message_ids"].get("group")
                if mid:
                    await context.bot.edit_message_text(
                        chat_id=game["chat_id"], message_id=mid, text=text, entities=entities
                    )
            else:
                for uid in game.get("players", []):
                    mid = game["message_ids"].get(str(uid))
                    if mid:
                        await context.bot.edit_message_text(chat_id=uid, message_id=mid, text=text, entities=entities)
        except BadRequest:
            pass

    for gt in ("xo", "rps", "dice"):
        queue = data["game_queue"].get(gt, [])
        fresh = []
        for entry in queue:
            if now - entry.get("ts", now) > GAME_IDLE_TIMEOUT:
                changed = True
                try:
                    await context.bot.send_message(chat_id=entry["uid"], text="⌛️ زمان انتظار برای حریف تموم شد و از صف خارج شدی.")
                except (BadRequest, Forbidden):
                    pass
            else:
                fresh.append(entry)
        data["game_queue"][gt] = fresh

    # میزهای حکمِ رهاشده (چه در انتظار نفر چهارم، چه وسط بازی) که مدتیه فعالیتی نداشتن
    for tid, table in list(data["hokm_tables"].items()):
        last = table.get("last_activity", now)
        if now - last <= GAME_IDLE_TIMEOUT:
            continue
        data["hokm_tables"].pop(tid, None)
        changed = True
        for pid in table.get("players", []):
            u = data["users"].get(str(pid))
            if u:
                u["current_game"] = None
        timeout_text = "⌛️ میز حکم به‌خاطر عدم فعالیت بسته شد."
        try:
            if table.get("scope") == "group" and table.get("chat_id"):
                await context.bot.send_message(chat_id=table["chat_id"], text=timeout_text)
            else:
                for pid in table.get("players", []):
                    try:
                        await context.bot.send_message(chat_id=pid, text=timeout_text)
                    except (BadRequest, Forbidden):
                        pass
        except (BadRequest, Forbidden):
            pass

    # صف انتظار پیوی حکم که بیش از حد منتظر نفر چهارم مونده
    stale_hokm_queue = []
    fresh_hokm_queue = []
    for uid in data["hokm_queue"]:
        fresh_hokm_queue.append(uid)  # صف حکم timestamp نداره؛ فقط اندازه‌ش رو کنترل می‌کنیم که خیلی بزرگ نشه
    if len(fresh_hokm_queue) > 40:
        stale_hokm_queue = fresh_hokm_queue[:-40]
        fresh_hokm_queue = fresh_hokm_queue[-40:]
        for uid in stale_hokm_queue:
            try:
                await context.bot.send_message(chat_id=uid, text="⌛️ صف حکم شلوغ بود و از صف قدیمی خارج شدی؛ دوباره امتحان کن.")
            except (BadRequest, Forbidden):
                pass
        data["hokm_queue"] = fresh_hokm_queue
        changed = True

    if changed:
        save_data()


# ============================================================================
# فعالیت‌های گروهی: ردیابی گروه‌ها، رویداد کلیک سریع، «فمبوی کوچولو»
# ============================================================================
_speedclick_lock = asyncio.Lock()
FAMBO_KID_COOLDOWN = 5 * 60


async def track_group_chat(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """این هندلر تو گروه دیسپچ جدا (group=1) ثبت می‌شه تا با بقیه هندلرها تداخل نکنه؛
    فقط چت‌آیدی گروه‌هایی که ربات توشونه رو یادداشت می‌کنه، برای رویداد کلیک سریع."""
    chat = update.effective_chat
    if not chat or chat.type not in (ChatType.GROUP, ChatType.SUPERGROUP):
        return
    data = load_data()
    if chat.id not in data["known_groups"]:
        data["known_groups"].append(chat.id)
        save_data()


async def speedclick_job(context: ContextTypes.DEFAULT_TYPE):
    """هر ۱۰ دقیقه، تو هر گروه شناخته‌شده یه پیام با دکمهٔ جایزه می‌فرسته؛
    اولین کسی که کلیک کنه ۵ تا ۲۵ فمبوی پوینت رندوم می‌گیره."""
    try:
        await _speedclick_job_inner(context)
    except Exception:
        logger.exception("خطا تو جاب speedclick_job")


async def _speedclick_job_inner(context: ContextTypes.DEFAULT_TYPE):
    data = load_data()

    # پاک‌سازی رویدادهای قدیمی (چه گرفته‌شده چه نگرفته) تا این دیکشنری برای همیشه
    # بزرگ‌وبزرگ‌تر نشه و حافظه رو نخوره — دقیقاً همون چیزی که می‌تونه بعد چند
    # ساعت باعث مصرف حافظهٔ زیاد و کشته‌شدن پروسه بشه.
    now = now_ts()
    stale_ids = [eid for eid, ev in data["speedclick_events"].items() if now - ev.get("created_at", now) > 1200]
    for eid in stale_ids:
        data["speedclick_events"].pop(eid, None)

    for chat_id in list(data.get("known_groups", [])):
        event_id = str(data["next_speedclick_id"])
        data["next_speedclick_id"] += 1
        text, entities = render_text("speedclick_prompt")
        btn = kb_button("speedclick", f"speedclick:{event_id}")
        if not btn:
            continue
        kb = InlineKeyboardMarkup([[btn]])
        try:
            msg = await context.bot.send_message(chat_id=chat_id, text=text, entities=entities, reply_markup=kb)
            data["speedclick_events"][event_id] = {
                "chat_id": chat_id, "message_id": msg.message_id, "claimed_by": None, "created_at": now_ts(),
            }
        except (BadRequest, Forbidden):
            data["known_groups"].remove(chat_id)
    save_data()


async def speedclick_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    event_id = query.data.split(":", 1)[1]
    async with _speedclick_lock:
        data = load_data()
        event = data["speedclick_events"].get(event_id)
        if not event:
            await safe_answer(query, "این جایزه دیگه معتبر نیست.", show_alert=True)
            return
        if event.get("claimed_by") is not None:
            text, _ = render_text("speedclick_too_late")
            await safe_answer(query, text, show_alert=True)
            return
        user = ensure_user(update)
        amount = random.randint(5, 25)
        user["points"] += amount
        event["claimed_by"] = update.effective_user.id
        data["speedclick_events"].pop(event_id, None)
        save_data()

    text, entities = render_text("speedclick_won", name=user_display_name(user), amount=amount)
    try:
        await safe_edit_text(query.message, text, entities=entities)
    except BadRequest:
        pass
    await safe_answer(query, f"🎉 +{amount} {CURRENCY_NAME}", show_alert=True)


async def fambo_kid_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """وقتی کسی تو گروه بگه «فمبوی کوچولو»، هر ۵ دقیقه یه‌بار (شخصی) جایزهٔ رندوم می‌گیره."""
    data = load_data()
    user = ensure_user(update)
    uid = str(update.effective_user.id)
    last = data["fambo_kid_last"].get(uid, 0)
    elapsed = now_ts() - last
    if elapsed < FAMBO_KID_COOLDOWN:
        minutes = (FAMBO_KID_COOLDOWN - elapsed) // 60 + 1
        text, entities = render_text("fambo_kid_cooldown", minutes=minutes)
        await update.message.reply_text(text, entities=entities)
        return
    amount = random.randint(5, 25)
    user["points"] += amount
    data["fambo_kid_last"][uid] = now_ts()
    save_data()
    text, entities = render_text("fambo_kid_reward", amount=amount)
    await update.message.reply_text(text, entities=entities)


async def fambo_panel_trigger(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """پنل شخصی کاربر تو گروه — وقتی بگه «فمبوی»، فقط خودش می‌تونه دکمه‌هاشو بزنه."""
    user = ensure_user(update)
    if user.get("character_id") is None:
        await update.message.reply_text("اول باید تو پیوی ربات /start بزنی تا کاراکتر بگیری، بعد اینجا «فمبوی» بگو.")
        return
    data = load_data()
    rows = rows_clean([
        [kb_button("profile", "menu_profile"), kb_button("feed", "menu_feed")],
        [kb_button("market", "menu_market"), kb_button("inventory", "menu_inventory")],
        [kb_button("games", "menu_games"), kb_button("partner", "menu_partner")],
        [kb_button("my_characters", "menu_mychars")],
        [kb_button("help", "menu_help"), kb_button("donate", url=DONATE_URL)],
    ])
    next_xp = xp_for_level(user["level"])
    bar, percent = xp_progress_bar(user["xp"], next_xp)
    partner_line = "دارم 💞" if user.get("partner_id") else "ندارم"
    text = (
        f"👤 پنل {user_display_name(user)}\n\n"
        f"سطح: {user['level']} — {bar} {percent}%\n"
        f"تا سطح بعد: {next_xp - user['xp']} تجربه مونده\n"
        f"پارتنر: {partner_line}\n"
        f"{CURRENCY_NAME}: {user['points']}\n\n"
        f"⏳ این پنل تا ۳ دقیقهٔ دیگه خودکار بسته می‌شه."
    )
    msg = await update.message.reply_text(text, reply_markup=InlineKeyboardMarkup(rows))
    register_group_panel(update.effective_chat.id, msg.message_id, update.effective_user.id)


async def group_help_trigger(update: Update, context: ContextTypes.DEFAULT_TYPE):
    text, entities = render_text("help_group_text")
    await update.message.reply_text(text, entities=entities)


# ---------- انتقال فمبوی پوینت (تو گروه، با ریپلای) ----------
async def transfer_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    reply = update.message.reply_to_message
    if not reply or not reply.from_user:
        text, entities = render_text("transfer_no_reply")
        await update.message.reply_text(text, entities=entities)
        return ConversationHandler.END

    target_tg_user = reply.from_user
    sender_id = update.effective_user.id
    if target_tg_user.id == sender_id:
        text, entities = render_text("transfer_self")
        await update.message.reply_text(text, entities=entities)
        return ConversationHandler.END

    data = load_data()
    target = data["users"].get(str(target_tg_user.id))
    if not target:
        text, entities = render_text("transfer_target_not_registered")
        await update.message.reply_text(text, entities=entities)
        return ConversationHandler.END

    ensure_user(update)  # مطمئن می‌شیم فرستنده تو دیتابیس هست
    context.user_data["transfer_target_id"] = target_tg_user.id
    text, entities = render_text("transfer_ask_amount", target_name=user_display_name(target))
    await update.message.reply_text(text, entities=entities)
    return TRANSFER_AMOUNT


async def transfer_amount_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    target_id = context.user_data.get("transfer_target_id")
    if not target_id:
        return ConversationHandler.END
    try:
        amount = int(update.message.text.strip())
        if amount <= 0:
            raise ValueError
    except ValueError:
        text, entities = render_text("transfer_invalid_amount")
        await update.message.reply_text(text, entities=entities)
        return TRANSFER_AMOUNT

    data = load_data()
    sender_id = update.effective_user.id
    sender = data["users"].get(str(sender_id))
    target = data["users"].get(str(target_id))
    if not sender or not target:
        return ConversationHandler.END
    if sender["points"] < amount:
        text, entities = render_text("transfer_insufficient_funds", amount=amount)
        await update.message.reply_text(text, entities=entities)
        return ConversationHandler.END

    sender["points"] -= amount
    target["points"] += amount
    save_data()

    text, entities = render_text("transfer_success_sender", amount=amount, target_name=user_display_name(target))
    await update.message.reply_text(text, entities=entities)
    text2, entities2 = render_text("transfer_success_receiver", amount=amount, sender_name=user_display_name(sender))
    try:
        await context.bot.send_message(chat_id=target_id, text=text2, entities=entities2)
    except (BadRequest, Forbidden):
        pass
    return ConversationHandler.END


# ============================================================================
# تغییر اسم کاراکتر و تعویض کاراکتر (رندوم / درخواست از ادمین)
# ============================================================================
async def rename_char_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    data = load_data()
    user = data["users"].get(str(update.effective_user.id))
    if not user or user["character_id"] is None:
        await safe_answer(query, "اول /start رو بزن.", show_alert=True)
        return ConversationHandler.END
    await safe_answer(query, )
    text, entities = render_text("rename_char_prompt")
    await query.message.reply_text(text, entities=entities)
    return RENAME_CHAR


async def rename_char_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    data = load_data()
    uid = str(update.effective_user.id)
    name = update.message.text.strip()[:32]
    if uid not in data["users"] or data["users"][uid]["character_id"] is None:
        await update.message.reply_text("اول باید کاراکتر بگیری، دستور /start رو بزن.")
        return ConversationHandler.END
    if not name or name.lower().startswith(("http://", "https://", "t.me/", "@")):
        await update.message.reply_text("این اسم معتبر نیست (نباید لینک یا یوزرنیم باشه). یه اسم دیگه بفرست:")
        return RENAME_CHAR
    data["users"][uid]["custom_name"] = name
    save_data()
    text, entities = render_text("rename_char_success", name=name)
    await update.message.reply_text(text, entities=entities, reply_markup=main_menu_keyboard(data["users"][uid]))
    return ConversationHandler.END


def swap_char_menu_keyboard(is_vip):
    rows = [[kb_button("swap_random", "swap_random"), kb_button("swap_admin", "swap_admin_request")]]
    if is_vip:
        rows.append([kb_button("swap_vip", "swap_vip_list")])
    rows.append([kb_button("back", "menu_back")])
    return InlineKeyboardMarkup(rows_clean(rows))


async def show_swap_char_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    data = load_data()
    user = data["users"].get(str(update.effective_user.id))
    if not user or user["character_id"] is None:
        await safe_answer(query, "اول /start رو بزن.", show_alert=True)
        return
    await safe_answer(query, )
    text, entities = render_text("swap_char_menu")
    await safe_edit_text(query.message, text, entities=entities, reply_markup=swap_char_menu_keyboard(user.get("vip", False)))


async def swap_random_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    data = load_data()
    uid = str(update.effective_user.id)
    user = data["users"].get(uid)
    if not user or user["character_id"] is None:
        await safe_answer(query, "اول /start رو بزن.", show_alert=True)
        return
    candidates = eligible_characters(user.get("vip", False), exclude_id=user["character_id"])
    if not candidates:
        candidates = eligible_characters(user.get("vip", False))
    if not candidates:
        text, entities = render_text("no_characters_available")
        await safe_answer(query, )
        await query.message.reply_text(text, entities=entities)
        return
    new_char = random.choice(candidates)
    grant_character(user, new_char["id"])
    save_data()
    await safe_answer(query, )
    tb = TextBuilder()
    tb.add_custom_emoji(new_char["placeholder"], new_char["custom_emoji_id"])
    tb.add_text("\n")
    tb.add_rendered(*render_text("swap_char_random_success"))
    text, entities = tb.build()
    await query.message.reply_text(text, entities=entities, reply_markup=main_menu_keyboard(user))


async def swap_vip_list_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """فقط برای کاربرهای VIP: لیست کاراکترهای ویژهٔ VIP رو نشون می‌ده تا خودشون یکی رو انتخاب کنن."""
    query = update.callback_query
    data = load_data()
    uid = str(update.effective_user.id)
    user = data["users"].get(uid)
    if not user or not user.get("vip"):
        await safe_answer(query, "این بخش فقط برای کاربرهای VIP فعاله.", show_alert=True)
        return
    vip_chars = [c for c in data["characters"] if c.get("vip_only")]
    if not vip_chars:
        await safe_answer(query, "فعلاً هیچ کاراکتر VIP‌ای تعریف نشده.", show_alert=True)
        return
    await safe_answer(query, )
    rows = [[InlineKeyboardButton(c["base_name"], callback_data=f"swap_vip_pick:{c['id']}")] for c in vip_chars]
    back_btn = kb_button("back", "menu_swap_char")
    if back_btn:
        rows.append([back_btn])
    await safe_edit_text(query.message, "🌟 یه کاراکتر ویژهٔ VIP انتخاب کن:", reply_markup=InlineKeyboardMarkup(rows))


async def swap_vip_pick_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    data = load_data()
    uid = str(update.effective_user.id)
    user = data["users"].get(uid)
    if not user or not user.get("vip"):
        await safe_answer(query, "این بخش فقط برای کاربرهای VIP فعاله.", show_alert=True)
        return
    char_id = int(query.data.split(":", 1)[1])
    new_char = get_char(char_id)
    if not new_char or not new_char.get("vip_only"):
        await safe_answer(query, "این کاراکتر دیگه در دسترس نیست.", show_alert=True)
        return
    grant_character(user, new_char["id"])
    save_data()
    await safe_answer(query, )
    tb = TextBuilder()
    tb.add_custom_emoji(new_char["placeholder"], new_char["custom_emoji_id"])
    tb.add_text("\n")
    tb.add_rendered(*render_text("swap_char_random_success"))
    text, entities = tb.build()
    await query.message.reply_text(text, entities=entities, reply_markup=main_menu_keyboard(user))


# ---------- کاراکترهای من (سوییچ رایگان بین کاراکترهایی که قبلاً گرفته/خریده) ----------
async def show_my_characters(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if is_group_panel_blocked(update):
        await safe_answer(query, "این پنل مال خودت نیست! بگو «فمبوی» تا پنل خودتو بگیری.", show_alert=True)
        return
    touch_group_panel(update)
    await safe_answer(query, )
    data = load_data()
    uid = str(update.effective_user.id)
    user = data["users"].get(uid)
    if not user:
        await query.message.reply_text("اول /start رو بزن.")
        return
    owned_ids = user.get("owned_characters", [])
    rows = []
    for cid in owned_ids:
        c = get_char(cid)
        if not c:
            continue
        mark = "✅ " if c["id"] == user.get("character_id") else ""
        star = "🌟" if c.get("vip_only") else ""
        rows.append([InlineKeyboardButton(f"{mark}{star}{c['base_name']}", callback_data=f"mychars_pick:{c['id']}")])
    if not rows:
        text = "هنوز هیچ کاراکتری نداری."
    else:
        text = "🎭 کاراکترهای من — رو هرکدوم بزنی، همون می‌شه کاراکتر فعالت (رایگان):"
    back_btn = kb_button("back", "menu_back")
    if back_btn:
        rows.append([back_btn])
    if hasattr(query.message, "edit_text"):
        try:
            await safe_edit_text(query.message, text, reply_markup=InlineKeyboardMarkup(rows))
            return
        except BadRequest:
            pass
    await query.message.reply_text(text, reply_markup=InlineKeyboardMarkup(rows))


async def mychars_pick_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if is_group_panel_blocked(update):
        await safe_answer(query, "این پنل مال خودت نیست! بگو «فمبوی» تا پنل خودتو بگیری.", show_alert=True)
        return
    touch_group_panel(update)
    data = load_data()
    uid = str(update.effective_user.id)
    user = data["users"].get(uid)
    char_id = int(query.data.split(":", 1)[1])
    if not user or char_id not in user.get("owned_characters", []):
        await safe_answer(query, "این کاراکتر رو نداری.", show_alert=True)
        return
    user["character_id"] = char_id
    save_data()
    await safe_answer(query, "✅ کاراکتر فعالت عوض شد.", show_alert=True)
    await show_my_characters(update, context)


async def swap_admin_request_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    data = load_data()
    uid = update.effective_user.id
    user = data["users"].get(str(uid))
    if not user or user["character_id"] is None:
        await safe_answer(query, "اول /start رو بزن.", show_alert=True)
        return
    already_pending = any(r["user_id"] == uid for r in data["char_requests"].values())
    if already_pending:
        await safe_answer(query, "قبلاً یه درخواست فعال داری، صبر کن ادمین جواب بده.", show_alert=True)
        return

    req_id = str(data["next_charreq_id"])
    data["next_charreq_id"] += 1
    data["char_requests"][req_id] = {"user_id": uid, "created_at": now_ts()}
    save_data()

    await safe_answer(query, )
    text, entities = render_text("swap_char_admin_sent")
    await query.message.reply_text(text, entities=entities)

    admin_text, admin_entities = render_text(
        "swap_char_admin_notify", user_name=user_display_name(user), user_id=uid
    )
    rows = rows_clean([
        [kb_button("admin_answer_charreq", f"admin_answer_charreq:{req_id}")],
        [kb_button("admin_user_chat", f"admin_chat_open:{uid}")],
    ])
    try:
        await context.bot.send_message(chat_id=ADMIN_ID, text=admin_text, entities=admin_entities, reply_markup=InlineKeyboardMarkup(rows))
    except (BadRequest, Forbidden):
        logger.warning("نتونستم به ادمین اطلاع بدم؛ شاید ادمین هنوز /start نزده.")


async def admin_answer_charreq_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_admin(update):
        await safe_answer(query, "دسترسی نداری.", show_alert=True)
        return ConversationHandler.END
    req_id = query.data.split(":", 1)[1]
    data = load_data()
    req = data["char_requests"].get(req_id)
    if not req:
        await safe_answer(query, "این درخواست دیگه معتبر نیست (شاید قبلاً پاسخ داده شده).", show_alert=True)
        return ConversationHandler.END
    await safe_answer(query, )
    context.user_data["charreq_id"] = req_id
    await query.message.reply_text(
        "خود ایموجی پرمیوم کاراکتر جدید رو برام بفرست تا برای این کاربر ثبتش کنم.\nبرای لغو: /cancel"
    )
    return ADMIN_CHAR_REQUEST


async def admin_answer_charreq_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    req_id = context.user_data.get("charreq_id")
    data = load_data()
    req = data["char_requests"].get(req_id) if req_id else None
    if not req:
        await update.message.reply_text("این درخواست دیگه معتبر نیست.", reply_markup=admin_main_keyboard())
        return ConversationHandler.END

    entities_map = update.message.parse_entities(types=[MessageEntity.CUSTOM_EMOJI])
    if not entities_map:
        await update.message.reply_text(
            "ایموجی پرمیوم پیدا نشد. مطمئن شو که یه ایموجی پرمیوم واقعی فرستادی.\nدوباره امتحان کن یا /cancel بزن."
        )
        return ADMIN_CHAR_REQUEST

    entity = list(entities_map.keys())[0]
    placeholder = entities_map[entity]
    custom_emoji_id = entity.custom_emoji_id

    existing = next((c for c in data["characters"] if c["custom_emoji_id"] == custom_emoji_id), None)
    if existing:
        new_char = existing
    else:
        new_char = {
            "id": data["next_char_id"],
            "custom_emoji_id": custom_emoji_id,
            "placeholder": placeholder,
            "base_name": "کاراکتر ویژه",
        }
        data["characters"].append(new_char)
        data["next_char_id"] += 1

    target_uid = req["user_id"]
    target_user = data["users"].get(str(target_uid))
    if target_user:
        grant_character(target_user, new_char["id"])
    data["char_requests"].pop(req_id, None)
    save_data()

    tb = TextBuilder()
    text_done, entities_done = render_text("swap_char_admin_done", user_name=user_display_name(target_user) if target_user else str(target_uid))
    tb.add_rendered(text_done, entities_done)
    text, entities = tb.build()
    await update.message.reply_text(text, entities=entities, reply_markup=admin_main_keyboard())

    if target_user:
        tb2 = TextBuilder()
        tb2.add_custom_emoji(new_char["placeholder"], new_char["custom_emoji_id"])
        tb2.add_text("\n")
        tb2.add_rendered(*render_text("swap_char_user_notify"))
        user_text, user_entities = tb2.build()
        try:
            await context.bot.send_message(chat_id=target_uid, text=user_text, entities=user_entities, reply_markup=main_menu_keyboard(target_user))
        except (BadRequest, Forbidden):
            pass

    return ConversationHandler.END


# ============================================================================
# پارتنر، ازدواج، بچه‌دار شدن و رول‌پلی
# ============================================================================
def couple_key(uid1, uid2):
    a, b = sorted([int(uid1), int(uid2)])
    return f"{a}_{b}"


def get_couple(uid1, uid2):
    data = load_data()
    return data["couples"].get(couple_key(uid1, uid2))


async def process_referral(context, user_id, referrer_id):
    data = load_data()
    if referrer_id == user_id:
        text, entities = render_text("partner_self_link")
        try:
            await context.bot.send_message(chat_id=user_id, text=text, entities=entities)
        except (BadRequest, Forbidden):
            pass
        return

    referrer = data["users"].get(str(referrer_id))
    user = data["users"].get(str(user_id))
    if not referrer or not user or user.get("character_id") is None or referrer.get("character_id") is None:
        return

    if user.get("partner_id") == referrer_id:
        text, entities = render_text("partner_already_this")
        await context.bot.send_message(chat_id=user_id, text=text, entities=entities)
        return

    if user.get("partner_id"):
        cur_partner_id = user["partner_id"]
        text, entities = render_text("partner_you_have_one")
        await context.bot.send_message(chat_id=user_id, text=text, entities=entities)
        cur_partner = data["users"].get(str(cur_partner_id))
        if cur_partner:
            alert_text, alert_entities = render_text("partner_cheat_alert", name=user_display_name(user))
            try:
                await context.bot.send_message(chat_id=cur_partner_id, text=alert_text, entities=alert_entities)
            except (BadRequest, Forbidden):
                pass
        return

    if referrer.get("partner_id") and referrer["partner_id"] != user_id:
        other_id = referrer["partner_id"]
        other = data["users"].get(str(other_id))
        text, entities = render_text(
            "partner_third_party",
            p1_name=user_display_name(referrer),
            p2_name=user_display_name(other) if other else "—",
        )
        await context.bot.send_message(chat_id=user_id, text=text, entities=entities)
        return

    user["partner_id"] = referrer_id
    referrer["partner_id"] = user_id
    save_data()

    text1, ent1 = render_text("partner_registered_you", partner_name=user_display_name(referrer))
    await context.bot.send_message(chat_id=user_id, text=text1, entities=ent1)
    text2, ent2 = render_text("partner_registered_other", partner_name=user_display_name(user))
    try:
        await context.bot.send_message(chat_id=referrer_id, text=text2, entities=ent2)
    except (BadRequest, Forbidden):
        pass


def partner_menu_keyboard(has_partner, has_child):
    if not has_partner:
        return InlineKeyboardMarkup(rows_clean([[kb_button("back", "menu_back")]]))
    rows = [[kb_button("partner_game", "partner_game"), kb_button("partner_rp", "partner_rp")]]
    if has_child:
        rows.append([kb_button("partner_child_view", "menu_partner_child_view")])
    else:
        rows.append([kb_button("partner_child", "partner_child_new")])
    rows.append([kb_button("partner_divorce", "partner_divorce_start")])
    rows.append([kb_button("back", "menu_back")])
    return InlineKeyboardMarkup(rows_clean(rows))


async def show_partner_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await safe_answer(query, )
    data = load_data()
    uid = update.effective_user.id
    user = data["users"].get(str(uid))
    if not user or user.get("character_id") is None:
        await query.message.reply_text("اول /start رو بزن.")
        return

    partner_id = user.get("partner_id")
    if not partner_id:
        me = await context.bot.get_me()
        ref_link = f"https://t.me/{me.username}?start=ref_{uid}"
        text, entities = render_text("partner_no_partner", ref_link=ref_link)
        await safe_edit_text(query.message, text, entities=entities, reply_markup=partner_menu_keyboard(False, False))
        return

    partner = data["users"].get(str(partner_id))
    partner_char = get_char(partner["character_id"]) if partner else None
    couple = get_couple(uid, partner_id)
    has_child = bool(couple and couple.get("child"))

    tb = TextBuilder()
    if partner_char:
        tb.add_custom_emoji(partner_char["placeholder"], partner_char["custom_emoji_id"])
        tb.add_text(" ")
    tb.add_rendered(
        *render_text(
            "partner_has_partner",
            partner_name=user_display_name(partner) if partner else "—",
            partner_level=partner["level"] if partner else "—",
        )
    )
    text, entities = tb.build()
    await safe_edit_text(query.message, text, entities=entities, reply_markup=partner_menu_keyboard(True, has_child))


async def partner_game_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """شروع یه بازی (XO/سنگ‌کاغذقیچی) مستقیم با پارتنر (بدون صف انتظار)."""
    query = update.callback_query
    data = load_data()
    uid = update.effective_user.id
    user = data["users"].get(str(uid))
    partner_id = user.get("partner_id") if user else None
    if not partner_id:
        text, _ = render_text("rp_no_partner")
        await safe_answer(query, text, show_alert=True)
        return
    partner = data["users"].get(str(partner_id))
    if user.get("current_game") or (partner and partner.get("current_game")):
        text, _ = render_text("game_already_playing")
        await safe_answer(query, text, show_alert=True)
        return
    await safe_answer(query, )
    rows = rows_clean([[kb_button("game_xo", "partner_game_pick:xo"), kb_button("game_rps", "partner_game_pick:rps")]])
    await query.message.reply_text("یه بازی برای شروع با پارتنرت انتخاب کن:", reply_markup=InlineKeyboardMarkup(rows))


async def partner_game_pick_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    game_type = query.data.split(":", 1)[1]
    data = load_data()
    uid = update.effective_user.id
    user = data["users"].get(str(uid))
    partner_id = user.get("partner_id") if user else None
    if not partner_id:
        await safe_answer(query, "پارتنر نداری.", show_alert=True)
        return
    partner = data["users"].get(str(partner_id))
    if not partner or partner.get("character_id") is None:
        await safe_answer(query, "پارتنرت هنوز کاراکتر نداره.", show_alert=True)
        return
    if user.get("current_game") or partner.get("current_game"):
        text, _ = render_text("game_already_playing")
        await safe_answer(query, text, show_alert=True)
        return
    await safe_answer(query, )
    await start_game_between(context, uid, partner_id, game_type, "pm")


async def partner_rp_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    data = load_data()
    uid = update.effective_user.id
    user = data["users"].get(str(uid))
    partner_id = user.get("partner_id") if user else None
    if not partner_id:
        text, _ = render_text("rp_no_partner")
        await safe_answer(query, text, show_alert=True)
        return
    partner = data["users"].get(str(partner_id))
    await safe_answer(query, )
    if user.get("rp_active"):
        user["rp_active"] = False
        if partner:
            partner["rp_active"] = False
        save_data()
        text, entities = render_text("rp_stop")
        await query.message.reply_text(text, entities=entities)
        try:
            if partner:
                await context.bot.send_message(chat_id=partner_id, text=text, entities=entities)
        except (BadRequest, Forbidden):
            pass
    else:
        user["rp_active"] = True
        if partner:
            partner["rp_active"] = True
        save_data()
        text, entities = render_text("rp_start")
        await query.message.reply_text(text, entities=entities)
        try:
            if partner:
                await context.bot.send_message(chat_id=partner_id, text=text, entities=entities)
        except (BadRequest, Forbidden):
            pass


async def rp_relay_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """رله کردن پیام‌های متنی بین دو پارتنری که رول‌پلی رو فعال کردن.
    این هندلر آخرین هندلر متنیه؛ فقط وقتی هیچ مکالمهٔ فعال دیگه‌ای اون آپدیت رو نگرفته باشه اجرا می‌شه."""
    if not update.message or not update.message.text:
        return
    data = load_data()
    uid = str(update.effective_user.id)
    user = data["users"].get(uid)
    if not user or not user.get("rp_active") or not user.get("partner_id"):
        return
    partner_id = user["partner_id"]
    partner = data["users"].get(str(partner_id))
    if not partner or not partner.get("rp_active"):
        return

    char = get_char(user.get("character_id"))
    tb = TextBuilder()
    if char:
        tb.add_custom_emoji(char["placeholder"], char["custom_emoji_id"])
        tb.add_text(" ")
    tb.add_text(f"{user_display_name(user)}: ")
    tb.add_text(update.message.text)
    text, entities = tb.build()
    try:
        await context.bot.send_message(chat_id=partner_id, text=text, entities=entities)
    except (BadRequest, Forbidden):
        pass


# ============================================================================
# چت مستقیم ادمین با یه کاربر (بدون نیاز به باز کردن پیوی کاربر، از داخل خود ربات)
# ============================================================================
async def admin_chat_open_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_admin(update):
        await safe_answer(query, "دسترسی نداری.", show_alert=True)
        return
    target_uid = int(query.data.split(":", 1)[1])
    data = load_data()
    target = data["users"].get(str(target_uid))
    if not target:
        await safe_answer(query, "این کاربر پیدا نشد.", show_alert=True)
        return
    data["admin_active_chat"] = target_uid
    save_data()
    await safe_answer(query, )
    await query.message.reply_text(
        f"💬 چت با {user_display_name(target)} باز شد.\n"
        "از الان هر پیامی که بفرستی مستقیم براش می‌ره و پیام‌هایی که بفرسته هم برات میاد.\n"
        "برای پایان دادن، دستور /endchat رو بزن."
    )
    try:
        await context.bot.send_message(chat_id=target_uid, text="👑 ادمین یه گفتگو باهات باز کرد. هر پیامی بفرستی مستقیم براش می‌ره.")
    except (BadRequest, Forbidden):
        pass


async def endchat_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not is_admin(update):
        return
    data = load_data()
    if not data.get("admin_active_chat"):
        await update.message.reply_text("چت باز فعالی نداری.")
        return
    target_uid = data["admin_active_chat"]
    data["admin_active_chat"] = None
    save_data()
    await update.message.reply_text("💬 چت بسته شد.", reply_markup=admin_main_keyboard())
    try:
        await context.bot.send_message(chat_id=target_uid, text="💬 گفتگوت با ادمین به پایان رسید.")
    except (BadRequest, Forbidden):
        pass


async def game_opponent_relay_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """اگه فرستنده الان وسط یه بازی پیوی با «چت با رقیب» فعاله، پیامش رو مستقیم برای
    حریفش می‌فرسته. برمی‌گردونه True اگه پیام رو مصرف کرد (یعنی بقیهٔ هندلرها لازم نیست اجرا بشن)."""
    data = load_data()
    uid = update.effective_user.id
    user = data["users"].get(str(uid))
    if not user or not user.get("current_game"):
        return False
    game = data["games"].get(user["current_game"])
    if not game or game.get("scope") != "pm" or not game.get("chat_active"):
        return False
    other_id = next((p for p in game["players"] if p != uid), None)
    if not other_id:
        return False
    name = user_display_name(user)
    try:
        await context.bot.send_message(chat_id=other_id, text=f"💬 {name}: {update.message.text}")
    except (BadRequest, Forbidden):
        pass
    return True


async def final_text_relay_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """آخرین هندلر متنی تو کل ربات — اول چت مستقیم ادمین⟷کاربر، بعد چت با رقیب وسط بازی،
    بعد رول‌پلی بین پارتنرها. فقط وقتی هیچ مکالمه/هندلر دیگه‌ای آپدیت رو نگرفته باشه اجرا می‌شه."""
    if not update.message or not update.message.text:
        return
    data = load_data()
    uid = update.effective_user.id
    active_chat = data.get("admin_active_chat")

    if active_chat and uid == ADMIN_ID:
        try:
            await context.bot.send_message(chat_id=active_chat, text=f"👑 ادمین: {update.message.text}")
        except (BadRequest, Forbidden):
            await update.message.reply_text("نتونستم پیام رو براش بفرستم (شاید ربات رو بلاک کرده).")
        return

    if active_chat and uid == active_chat:
        sender = data["users"].get(str(uid))
        name = user_display_name(sender) if sender else str(uid)
        try:
            await context.bot.send_message(chat_id=ADMIN_ID, text=f"👤 {name}: {update.message.text}")
        except (BadRequest, Forbidden):
            pass
        return

    if await game_opponent_relay_handler(update, context):
        return

    await rp_relay_handler(update, context)


# ---------- طلاق ----------
async def partner_divorce_start_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    data = load_data()
    uid = update.effective_user.id
    user = data["users"].get(str(uid))
    partner_id = user.get("partner_id") if user else None
    if not partner_id:
        text, _ = render_text("divorce_no_partner")
        await safe_answer(query, text, show_alert=True)
        return
    if str(partner_id) in data["divorce_requests"] or str(uid) in data["divorce_requests"]:
        text, _ = render_text("divorce_already_pending")
        await safe_answer(query, text, show_alert=True)
        return
    data["divorce_requests"][str(partner_id)] = {"from": uid, "ts": now_ts()}
    save_data()
    await safe_answer(query, )
    text, entities = render_text("divorce_request_sent")
    await query.message.reply_text(text, entities=entities)

    notify_text, notify_entities = render_text("divorce_request_notify", name=user_display_name(user))
    rows = rows_clean([[kb_button("divorce_accept", f"divorce_accept:{uid}"), kb_button("divorce_reject", f"divorce_reject:{uid}")]])
    try:
        await context.bot.send_message(chat_id=partner_id, text=notify_text, entities=notify_entities, reply_markup=InlineKeyboardMarkup(rows))
    except (BadRequest, Forbidden):
        pass


async def divorce_decision_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    accepted = query.data.startswith("divorce_accept:")
    requester_id = int(query.data.split(":", 1)[1])
    data = load_data()
    responder_id = update.effective_user.id
    req = data["divorce_requests"].get(str(responder_id))
    if not req or req.get("from") != requester_id:
        await safe_answer(query, "این درخواست دیگه معتبر نیست.", show_alert=True)
        return
    data["divorce_requests"].pop(str(responder_id), None)

    responder = data["users"].get(str(responder_id))
    requester = data["users"].get(str(requester_id))

    if accepted:
        if responder:
            responder["partner_id"] = None
            responder["rp_active"] = False
        if requester:
            requester["partner_id"] = None
            requester["rp_active"] = False
        data["couples"].pop(couple_key(requester_id, responder_id), None)
        save_data()
        await safe_answer(query, )
        text1, ent1 = render_text("divorce_accepted_other")
        await safe_edit_text(query.message, text1, entities=ent1)
        text2, ent2 = render_text("divorce_accepted_requester")
        try:
            await context.bot.send_message(chat_id=requester_id, text=text2, entities=ent2)
        except (BadRequest, Forbidden):
            pass
    else:
        save_data()
        await safe_answer(query, )
        await safe_edit_text(query.message, "شما درخواست طلاق رو رد کردید. 💞")
        text, entities = render_text("divorce_rejected")
        try:
            await context.bot.send_message(chat_id=requester_id, text=text, entities=entities)
        except (BadRequest, Forbidden):
            pass


# ---------- بچه‌دار شدن ----------
async def partner_child_new_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    data = load_data()
    uid = update.effective_user.id
    user = data["users"].get(str(uid))
    partner_id = user.get("partner_id") if user else None
    if not partner_id:
        text, _ = render_text("child_no_partner")
        await safe_answer(query, text, show_alert=True)
        return
    ck = couple_key(uid, partner_id)
    couple = data["couples"].get(ck)
    if couple and couple.get("child"):
        text, _ = render_text("child_already_have")
        await safe_answer(query, text, show_alert=True)
        return
    if not data["characters"]:
        text, _ = render_text("child_no_characters")
        await safe_answer(query, text, show_alert=True)
        return

    child_char = random.choice(data["characters"])
    child = {
        "character_id": child_char["id"],
        "custom_name": None,
        "level": 1,
        "xp": 0,
        "power": power_for_level(1, False),
        "last_feed": 0,
    }
    if not couple:
        couple = {"partners": [uid, partner_id], "child": None, "created_at": now_ts()}
    couple["child"] = child
    data["couples"][ck] = couple
    save_data()

    await safe_answer(query, )
    tb = TextBuilder()
    tb.add_custom_emoji(child_char["placeholder"], child_char["custom_emoji_id"])
    tb.add_text("\n")
    tb.add_rendered(*render_text("child_born"))
    text, entities = tb.build()
    context.user_data["awaiting_child_name_couple"] = ck
    msg = await query.message.reply_text(text, entities=entities)
    partner = data["users"].get(str(partner_id))
    try:
        await context.bot.send_message(chat_id=partner_id, text=text, entities=entities)
    except (BadRequest, Forbidden):
        pass
    return CHILD_NAME


async def child_name_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    ck = context.user_data.get("awaiting_child_name_couple")
    data = load_data()
    couple = data["couples"].get(ck) if ck else None
    if not couple or not couple.get("child"):
        return ConversationHandler.END
    name = update.message.text.strip()[:32]
    couple["child"]["custom_name"] = name
    save_data()
    text, entities = render_text("child_named", name=name)
    uid = update.effective_user.id
    user = data["users"].get(str(uid))
    await update.message.reply_text(text, entities=entities, reply_markup=main_menu_keyboard(user))
    for pid in couple["partners"]:
        if pid != uid:
            try:
                await context.bot.send_message(chat_id=pid, text=text, entities=entities)
            except (BadRequest, Forbidden):
                pass
    return ConversationHandler.END


async def show_child_status(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await safe_answer(query, )
    data = load_data()
    uid = update.effective_user.id
    user = data["users"].get(str(uid))
    partner_id = user.get("partner_id") if user else None
    couple = get_couple(uid, partner_id) if partner_id else None
    if not couple or not couple.get("child"):
        await query.message.reply_text("هنوز بچه‌ای ندارید.")
        return
    child = couple["child"]
    child_char = get_char(child["character_id"])
    partner = data["users"].get(str(partner_id))
    next_xp = xp_for_level(child["level"])
    tb = TextBuilder()
    if child_char:
        tb.add_custom_emoji(child_char["placeholder"], child_char["custom_emoji_id"])
        tb.add_text(" ")
    tb.add_rendered(
        *render_text(
            "child_status",
            p1_name=user_display_name(user), p2_name=user_display_name(partner) if partner else "—",
            name=child.get("custom_name") or "بی‌نام",
            level=child["level"], xp=child["xp"], next_xp=next_xp, power=child["power"],
        )
    )
    text, entities = tb.build()
    rows = rows_clean([[kb_button("partner_child_feed", "child_feed")], [kb_button("back", "menu_partner")]])
    await safe_edit_text(query.message, text, entities=entities, reply_markup=InlineKeyboardMarkup(rows))


async def child_feed_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    data = load_data()
    uid = update.effective_user.id
    user = data["users"].get(str(uid))
    partner_id = user.get("partner_id") if user else None
    couple = get_couple(uid, partner_id) if partner_id else None
    if not couple or not couple.get("child"):
        await safe_answer(query, "بچه‌ای ندارید.", show_alert=True)
        return
    child = couple["child"]
    elapsed = now_ts() - child.get("last_feed", 0)
    if elapsed < FEED_COOLDOWN:
        minutes = (FEED_COOLDOWN - elapsed) // 60 + 1
        text, _ = render_text("child_feed_cooldown", minutes=minutes)
        await safe_answer(query, text, show_alert=True)
        return

    partner = data["users"].get(str(partner_id))
    chosen_key = None
    owner = None
    for candidate in (user, partner):
        if not candidate:
            continue
        fridge = candidate.get("fridge", {})
        k = next((kk for kk, vv in fridge.items() if vv > 0), None)
        if k:
            chosen_key = k
            owner = candidate
            break
    if not chosen_key:
        text, _ = render_text("child_feed_no_food")
        await safe_answer(query, text, show_alert=True)
        return

    item = find_item(get_shop_items("food"), chosen_key)
    owner["fridge"][chosen_key] -= 1
    if owner["fridge"][chosen_key] <= 0:
        del owner["fridge"][chosen_key]

    child["xp"] += item["xp"] if item else 15
    child["last_feed"] = now_ts()

    leveled_up = False
    while child["xp"] >= xp_for_level(child["level"]):
        child["xp"] -= xp_for_level(child["level"])
        child["level"] += 1
        child["power"] = power_for_level(child["level"], False)
        leveled_up = True
    save_data()

    await safe_answer(query, )
    text, entities = render_text("child_feed_success", xp=item["xp"] if item else 15)
    await query.message.reply_text(text, entities=entities)
    if leveled_up:
        text2, entities2 = render_text("child_level_up", level=child["level"], power=child["power"])
        await query.message.reply_text(text2, entities=entities2)


# ----------------------------------------------------------------------------
# پنل ادمین
# ----------------------------------------------------------------------------
def is_admin(update: Update) -> bool:
    return update.effective_user.id == ADMIN_ID


def admin_main_keyboard():
    rows = rows_clean([
        [kb_button("admin_channels", "admin_channels"), kb_button("admin_chars", "admin_chars")],
        [kb_button("admin_texts", "admin_texts"), kb_button("admin_buttons_menu", "admin_buttons")],
        [kb_button("admin_shop_icons", "admin_shopicons"), kb_button("admin_shop_manage", "admin_shopmanage")],
        [kb_button("admin_games_manage", "admin_gamesmanage")],
        [kb_button("admin_users", "admin_users_0"), kb_button("admin_stats", "admin_stats")],
        [kb_button("admin_broadcast", "admin_broadcast")],
    ])
    return InlineKeyboardMarkup(rows)


async def admin_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not is_admin(update):
        return
    text, entities = render_text("admin_welcome")
    await update.message.reply_text(text, entities=entities, reply_markup=admin_main_keyboard())


async def admin_router(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_admin(update):
        await safe_answer(query, "دسترسی نداری.", show_alert=True)
        return
    action = query.data
    await safe_answer(query, )
    data = load_data()

    if action == "admin_back":
        text, entities = render_text("admin_welcome")
        await safe_edit_text(query.message, text, entities=entities, reply_markup=admin_main_keyboard())

    # ---------------- کانال‌ها ----------------
    elif action == "admin_channels":
        await admin_router_channels_refresh(query)

    elif action.startswith("admin_delchannel:"):
        chat_id = action.split(":", 1)[1]
        data["channels"] = [c for c in data["channels"] if str(c["chat_id"]) != chat_id]
        save_data()
        await admin_router_channels_refresh(query)

    # ---------------- کاراکترها ----------------
    elif action == "admin_chars":
        await admin_router_chars_refresh(query, page=0)

    elif action.startswith("admin_charspage:"):
        page = int(action.split(":", 1)[1])
        await admin_router_chars_refresh(query, page=page)

    elif action.startswith("admin_char_detail:"):
        _, char_id, page = action.split(":")
        await admin_char_detail_menu(query, int(char_id), int(page))

    elif action.startswith("admin_chartogglevip:"):
        _, char_id, page = action.split(":")
        c = get_char(int(char_id))
        if c:
            c["vip_only"] = not c.get("vip_only", False)
            save_data()
        await admin_char_detail_menu(query, int(char_id), int(page))

    elif action.startswith("admin_delchar:"):
        parts = action.split(":")
        char_id = int(parts[1])
        page = int(parts[2]) if len(parts) > 2 else 0
        data["characters"] = [c for c in data["characters"] if c["id"] != char_id]
        save_data()
        total_pages = max(1, (len(data["characters"]) + CHARS_PER_PAGE - 1) // CHARS_PER_PAGE)
        page = min(page, total_pages - 1)
        await admin_router_chars_refresh(query, page=page)

    # ---------------- متن‌ها ----------------
    elif action == "admin_texts":
        rows = [
            [InlineKeyboardButton(k, callback_data=f"admin_edittext_pick:{k}")]
            for k in data["texts"].keys()
        ]
        back_btn = kb_button("back", "admin_back")
        if back_btn:
            rows.append([back_btn])
        await safe_edit_text(query.message, "✏️ یکی از متن‌ها رو برای ویرایش انتخاب کن:", reply_markup=InlineKeyboardMarkup(rows))

    # ---------------- دکمه‌ها ----------------
    elif action == "admin_buttons":
        rows = [
            [InlineKeyboardButton(
                f"{'✅' if v.get('enabled', True) else '🚫'} {v['text']}",
                callback_data=f"admin_editbutton_pick:{k}",
            )]
            for k, v in data["buttons"].items()
        ]
        back_btn = kb_button("back", "admin_back")
        if back_btn:
            rows.append([back_btn])
        await safe_edit_text(query.message, 
            "🔘 یکی از دکمه‌ها رو برای ویرایش (متن/رنگ/آیکون/فعال-غیرفعال) انتخاب کن:",
            reply_markup=InlineKeyboardMarkup(rows),
        )

    elif action.startswith("admin_setstyle:"):
        _, key, style = action.split(":")
        style = None if style == "none" else style
        data["buttons"][key]["style"] = style
        save_data()
        await safe_answer(query, "رنگ دکمه به‌روزرسانی شد ✅", show_alert=True)

    elif action.startswith("admin_clearicon:"):
        key = action.split(":", 1)[1]
        data["buttons"][key]["icon"] = None
        save_data()
        await safe_answer(query, "آیکون دستی این دکمه پاک شد ✅", show_alert=True)

    elif action.startswith("admin_toggleenabled:"):
        key = action.split(":", 1)[1]
        data["buttons"][key]["enabled"] = not data["buttons"][key].get("enabled", True)
        save_data()
        state = "فعال ✅" if data["buttons"][key]["enabled"] else "غیرفعال 🚫"
        await safe_answer(query, f"دکمه «{data['buttons'][key]['text']}» {state} شد.", show_alert=True)

    # ---------------- آیکون آیتم‌های فروشگاه ----------------
    elif action == "admin_shopicons":
        rows = []
        for it in get_shop_items("food") + get_shop_items("weapon") + get_shop_items("clothes"):
            rows.append([InlineKeyboardButton(it["name"], callback_data=f"admin_setitemicon_pick:{it['key']}")])
        back_btn = kb_button("back", "admin_back")
        if back_btn:
            rows.append([back_btn])
        await safe_edit_text(query.message, "🖼 یه آیتم رو انتخاب کن تا آیکون پرمیومش رو ست کنی:", reply_markup=InlineKeyboardMarkup(rows))

    # ---------------- کاربران ----------------
    elif action.startswith("admin_users_"):
        page = int(action.split("_")[-1])
        await render_users_page(query, page)

    elif action.startswith("admin_user_detail:"):
        target_uid = action.split(":", 1)[1]
        await render_user_detail(query, target_uid)

    elif action.startswith("admin_toggle_vip:"):
        target_uid = action.split(":", 1)[1]
        u = data["users"].get(target_uid)
        if u:
            u["vip"] = not u["vip"]
            u["power"] = power_for_level(u["level"], u["vip"])
            save_data()
        await render_user_detail(query, target_uid)

    # ---------------- آمار ----------------
    elif action == "admin_stats":
        total_users = len(data["users"])
        with_char = len([u for u in data["users"].values() if u["character_id"]])
        vip_count = len([u for u in data["users"].values() if u["vip"]])
        text = (
            f"📊 آمار ربات\n\n"
            f"👥 کل کاربران: {total_users}\n"
            f"🎭 کاربران دارای کاراکتر: {with_char}\n"
            f"💎 کاربران VIP: {vip_count}\n"
            f"🍖 تعداد غذا دادن‌ها: {data['stats']['total_feeds']}\n"
            f"⚔️ تعداد مبارزه‌ها: {data['stats']['total_battles']}\n"
            f"🎮 بازی‌های در حال انجام: {len(data['games'])}\n"
            f"🎭 تعداد کاراکترهای تعریف‌شده: {len(data['characters'])}\n"
            f"📢 تعداد کانال‌های جوین اجباری: {len(data['channels'])}"
        )
        back_btn = kb_button("back", "admin_back")
        rows = [[back_btn]] if back_btn else []
        await safe_edit_text(query.message, text, reply_markup=InlineKeyboardMarkup(rows))

    # ---------------- مدیریت سوپرمارکت ----------------
    elif action == "admin_shopmanage":
        rows = rows_clean([
            [kb_button("market_food", "admin_shopcat:food"), kb_button("market_weapon", "admin_shopcat:weapon")],
            [kb_button("market_clothes", "admin_shopcat:clothes")],
            [kb_button("back", "admin_back")],
        ])
        await safe_edit_text(query.message, "🛍 مدیریت سوپرمارکت — یه بخش رو انتخاب کن:", reply_markup=InlineKeyboardMarkup(rows))

    elif action == "admin_gamesmanage":
        await admin_games_manage_refresh(query)

    elif action.startswith("admin_gametoggle:"):
        gt = action.split(":", 1)[1]
        data["game_enabled"][gt] = not data["game_enabled"].get(gt, True)
        save_data()
        await admin_games_manage_refresh(query)

    elif action.startswith("admin_shopcat:"):
        category = action.split(":", 1)[1]
        await admin_shop_category_refresh(query, category)

    elif action.startswith("admin_shopitem_pick:"):
        _, category, key = action.split(":", 2)
        await admin_shop_item_menu(query, category, key)

    elif action.startswith("admin_shop_delete:"):
        _, category, key = action.split(":", 2)
        data["shop"][category] = [it for it in data["shop"][category] if it["key"] != key]
        save_data()
        text, _ = render_text("admin_shop_delete_done")
        await safe_answer(query, text, show_alert=True)
        await admin_shop_category_refresh(query, category)

    # ---------------- fallback ----------------
    else:
        await query.message.reply_text(
            "این دکمه دیگه معتبر نیست یا گفتگوی قبلی ناقص مونده. دستور /admin رو دوباره بزن.",
            reply_markup=admin_main_keyboard(),
        )


async def admin_games_manage_refresh(query):
    data = load_data()
    labels = {"xo": "❌⭕ دوز", "rps": "✊✋✌️ سنگ‌کاغذقیچی", "dice": "🎲 تاس دوئل", "hokm": "🃏 حکم"}
    rows = []
    for gt, label in labels.items():
        enabled = data["game_enabled"].get(gt, True)
        state = "✅ فعال" if enabled else "🚫 غیرفعال"
        rows.append([InlineKeyboardButton(f"{label} — {state}", callback_data=f"admin_gametoggle:{gt}")])
    back_btn = kb_button("back", "admin_back")
    if back_btn:
        rows.append([back_btn])
    await safe_edit_text(
        query.message, "🎮 مدیریت بازی‌ها — رو هرکدوم بزنی فعال/غیرفعال می‌شه:", reply_markup=InlineKeyboardMarkup(rows)
    )


async def admin_shop_category_refresh(query, category):
    data = load_data()
    items = data["shop"].get(category, [])
    rows = []
    for it in items:
        rows.append([InlineKeyboardButton(f"{it['name']} — {it['price']} 💰", callback_data=f"admin_shopitem_pick:{category}:{it['key']}")])
    add_btn = kb_button("admin_shop_add", f"admin_shopadd_start:{category}")
    if add_btn:
        rows.append([add_btn])
    back_btn = kb_button("back", "admin_shopmanage")
    if back_btn:
        rows.append([back_btn])
    title = {"food": "🍔 غذا", "weapon": "🗡 اسلحه", "clothes": "👕 لباس"}[category]
    await safe_edit_text(query.message, f"{title} — آیتم‌های فروشگاه:", reply_markup=InlineKeyboardMarkup(rows))


async def admin_shop_item_menu(query, category, key):
    data = load_data()
    item = find_item(data["shop"].get(category, []), key)
    if not item:
        await safe_answer(query, "این آیتم دیگه موجود نیست.", show_alert=True)
        return
    stat_field = {"food": None, "weapon": "atk", "clothes": "defense"}[category]
    lines = [f"اسم: {item['name']}", f"قیمت: {item['price']}"]
    if category == "food":
        lines.append(f"تجربه: {item.get('xp', 0)}")
        lines.append(f"{CURRENCY_NAME}: {item.get('points', 0)}")
    elif stat_field:
        lines.append(f"{stat_field}: {item.get(stat_field, 0)}")
    text = "\n".join(lines)
    rows = [
        [InlineKeyboardButton("✏️ اسم", callback_data=f"admin_shopedit_start:{category}:{key}:name")],
        [InlineKeyboardButton("✏️ قیمت", callback_data=f"admin_shopedit_start:{category}:{key}:price")],
    ]
    if category == "food":
        rows.append([InlineKeyboardButton("✏️ تجربه", callback_data=f"admin_shopedit_start:{category}:{key}:xp")])
        rows.append([InlineKeyboardButton(f"✏️ {CURRENCY_NAME}", callback_data=f"admin_shopedit_start:{category}:{key}:points")])
    elif stat_field:
        rows.append([InlineKeyboardButton(f"✏️ {stat_field}", callback_data=f"admin_shopedit_start:{category}:{key}:{stat_field}")])
    del_btn = kb_button("admin_shop_delete", f"admin_shop_delete:{category}:{key}")
    if del_btn:
        rows.append([del_btn])
    back_btn = kb_button("back", f"admin_shopcat:{category}")
    if back_btn:
        rows.append([back_btn])
    await safe_edit_text(query.message, text, reply_markup=InlineKeyboardMarkup(rows))


async def admin_router_channels_refresh(query):
    data = load_data()
    rows = []
    for ch in data["channels"]:
        rows.append([InlineKeyboardButton(f"🗑 {ch['title']}", callback_data=f"admin_delchannel:{ch['chat_id']}")])
    add_btn = kb_button("admin_channel_add", "admin_addchannel")
    if add_btn:
        rows.append([add_btn])
    back_btn = kb_button("back", "admin_back")
    if back_btn:
        rows.append([back_btn])
    await safe_edit_text(query.message, "📢 کانال‌های جوین اجباری:", reply_markup=InlineKeyboardMarkup(rows))


CHARS_PER_PAGE = 20


async def admin_router_chars_refresh(query, page: int = 0):
    data = load_data()
    chars = data["characters"]
    total = len(chars)
    start = page * CHARS_PER_PAGE
    chunk = chars[start:start + CHARS_PER_PAGE]

    rows = []
    for c in chunk:
        star = "🌟" if c.get("vip_only") else ""
        price_tag = f" 💰{c['price']}" if c.get("price") else ""
        rows.append([InlineKeyboardButton(f"{star}{c['base_name']}{price_tag}", callback_data=f"admin_char_detail:{c['id']}:{page}")])

    nav = []
    if start > 0:
        nav.append(InlineKeyboardButton("⬅️ قبلی", callback_data=f"admin_charspage:{page-1}"))
    if start + CHARS_PER_PAGE < total:
        nav.append(InlineKeyboardButton("بعدی ➡️", callback_data=f"admin_charspage:{page+1}"))
    if nav:
        rows.append(nav)

    add_btn = kb_button("admin_char_add", "admin_addchar")
    if add_btn:
        rows.append([add_btn])
    import_btn = kb_button("admin_char_import_pack", "admin_importpack")
    if import_btn:
        rows.append([import_btn])
    back_btn = kb_button("back", "admin_back")
    if back_btn:
        rows.append([back_btn])

    total_pages = max(1, (total + CHARS_PER_PAGE - 1) // CHARS_PER_PAGE)
    await safe_edit_text(query.message, 
        f"🎭 کاراکترها ({total} عدد) — صفحهٔ {page+1}/{total_pages} — 🌟=VIP، 💰=قیمت‌دار. برای ویرایش/حذف رو یکی بزن:",
        reply_markup=InlineKeyboardMarkup(rows),
    )


async def admin_char_detail_menu(query, char_id, page):
    c = get_char(char_id)
    if not c:
        await safe_answer(query, "این کاراکتر دیگه موجود نیست.", show_alert=True)
        return
    text = (
        f"اسم: {c['base_name']}\n"
        f"VIP: {'بله 🌟' if c.get('vip_only') else 'خیر'}\n"
        f"قیمت: {c.get('price', 0)} {CURRENCY_NAME}" + (" (رایگان/رندوم)" if not c.get("price") else "")
    )
    rows = [
        [InlineKeyboardButton("✏️ تغییر قیمت", callback_data=f"admin_charprice_start:{char_id}:{page}")],
        [InlineKeyboardButton(
            "🚫 غیر-VIP کن" if c.get("vip_only") else "🌟 VIP کن",
            callback_data=f"admin_chartogglevip:{char_id}:{page}",
        )],
        [InlineKeyboardButton("🗑 حذف این کاراکتر", callback_data=f"admin_delchar:{char_id}:{page}")],
        [InlineKeyboardButton("🔙 بازگشت به لیست", callback_data=f"admin_charspage:{page}")],
    ]
    await safe_edit_text(query.message, text, reply_markup=InlineKeyboardMarkup(rows))


async def render_users_page(query, page: int):
    data = load_data()
    users_list = list(data["users"].values())
    users_list.sort(key=lambda u: u["points"], reverse=True)
    start = page * USERS_PER_PAGE
    chunk = users_list[start : start + USERS_PER_PAGE]

    rows = []
    for u in chunk:
        label = f"{u['first_name'] or u['user_id']} | {u['points']} {CURRENCY_NAME}"
        rows.append([InlineKeyboardButton(label, callback_data=f"admin_user_detail:{u['user_id']}")])

    nav = []
    if start > 0:
        nav.append(InlineKeyboardButton("⬅️ قبلی", callback_data=f"admin_users_{page-1}"))
    if start + USERS_PER_PAGE < len(users_list):
        nav.append(InlineKeyboardButton("بعدی ➡️", callback_data=f"admin_users_{page+1}"))
    if nav:
        rows.append(nav)
    back_btn = kb_button("back", "admin_back")
    if back_btn:
        rows.append([back_btn])

    text = f"👥 لیست کاربران (صفحه {page+1}) — مجموع: {len(users_list)}"
    await safe_edit_text(query.message, text, reply_markup=InlineKeyboardMarkup(rows))


async def render_user_detail(query, target_uid: str):
    data = load_data()
    u = data["users"].get(target_uid)
    if not u:
        await safe_answer(query, "کاربر پیدا نشد.", show_alert=True)
        return
    char = get_char(u["character_id"]) if u["character_id"] else None
    char_name = char["base_name"] if char else "—"
    text = (
        f"👤 کاربر: {u['first_name'] or '—'} (@{u['username'] or '---'})\n"
        f"آیدی عددی: {u['user_id']}\n"
        f"نام کاراکتر: {u['custom_name'] or '—'} ({char_name})\n"
        f"سطح: {u['level']} | تجربه: {u['xp']}\n"
        f"قدرت: {u['power']} | {CURRENCY_NAME}: {u['points']}\n"
        f"اسلحه: {weapon_label(u)} | لباس: {clothes_label(u)}\n"
        f"برد/باخت: {u['wins']}/{u['losses']}\n"
        f"VIP: {'✅ بله' if u['vip'] else '❌ خیر'}"
    )
    rows = [
        [InlineKeyboardButton(
            "❌ حذف VIP" if u["vip"] else "💎 اعطای VIP",
            callback_data=f"admin_toggle_vip:{target_uid}",
        )],
        [InlineKeyboardButton("➕ افزودن امتیاز", callback_data=f"admin_givepoints_start:{target_uid}")],
    ]
    chat_btn = kb_button("admin_user_chat", f"admin_chat_open:{target_uid}")
    if chat_btn:
        rows.append([chat_btn])
    back_btn = kb_button("back", "admin_users_0")
    if back_btn:
        rows.append([back_btn])
    await safe_edit_text(query.message, text, reply_markup=InlineKeyboardMarkup(rows))


# ------------- Conversation: ویرایش قیمت یه کاراکتر موجود -------------
async def admin_charprice_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_admin(update):
        await safe_answer(query, "دسترسی نداری.", show_alert=True)
        return ConversationHandler.END
    _, char_id, page = query.data.split(":")
    context.user_data["charprice_target"] = {"char_id": int(char_id), "page": int(page)}
    await safe_answer(query, )
    await query.message.reply_text("قیمت جدید رو به " + CURRENCY_NAME + " بفرست (0 یعنی رایگان/رندوم):")
    return ADD_CHAR_PRICE


async def admin_charprice_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    target = context.user_data.get("charprice_target")
    if not target:
        return ConversationHandler.END
    try:
        price = int(update.message.text.strip())
        if price < 0:
            raise ValueError
    except ValueError:
        await update.message.reply_text("لطفاً فقط یه عدد صفر یا مثبت بفرست.")
        return ADD_CHAR_PRICE
    c = get_char(target["char_id"])
    if not c:
        await update.message.reply_text("این کاراکتر دیگه موجود نیست.", reply_markup=admin_main_keyboard())
        return ConversationHandler.END
    c["price"] = price
    save_data()
    await update.message.reply_text(f"✅ قیمت «{c['base_name']}» شد {price} {CURRENCY_NAME}.", reply_markup=admin_main_keyboard())
    return ConversationHandler.END


# ------------- Conversation: افزودن آیتم فروشگاه -------------
SHOP_STAT_FIELDS = {"food": ["xp", "points"], "weapon": ["atk"], "clothes": ["defense"]}
SHOP_STAT_LABELS = {"xp": "تجربه", "points": CURRENCY_NAME, "atk": "قدرت حمله (atk)", "defense": "قدرت دفاع (defense)"}


async def admin_shop_add_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_admin(update):
        await safe_answer(query, "دسترسی نداری.", show_alert=True)
        return ConversationHandler.END
    await safe_answer(query, )
    category = query.data.split(":", 1)[1]
    context.user_data["shop_new_item"] = {"category": category}
    text, entities = render_text("admin_shop_add_name")
    await query.message.reply_text(text, entities=entities)
    return SHOP_ADD_NAME


async def admin_shop_add_name_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    item = context.user_data.get("shop_new_item")
    if not item:
        return ConversationHandler.END
    entities_map = update.message.parse_entities(types=[MessageEntity.CUSTOM_EMOJI])
    item["name"] = update.message.text.strip()[:40]
    if entities_map:
        entity = list(entities_map.keys())[0]
        item["_icon"] = entity.custom_emoji_id
    text, entities = render_text("admin_shop_add_price", currency_name=CURRENCY_NAME)
    await update.message.reply_text(text, entities=entities)
    return SHOP_ADD_PRICE


async def admin_shop_add_price_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    item = context.user_data.get("shop_new_item")
    if not item:
        return ConversationHandler.END
    try:
        item["price"] = int(update.message.text.strip())
    except ValueError:
        await update.message.reply_text("لطفاً فقط عدد بفرست.")
        return SHOP_ADD_PRICE
    fields_queue = list(SHOP_STAT_FIELDS[item["category"]])
    context.user_data["shop_add_fields_queue"] = fields_queue
    next_field = fields_queue.pop(0)
    context.user_data["shop_add_current_field"] = next_field
    text, entities = render_text("admin_shop_add_stat", stat_name=SHOP_STAT_LABELS[next_field])
    await update.message.reply_text(text, entities=entities)
    return SHOP_ADD_STAT


async def admin_shop_add_stat_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    item = context.user_data.get("shop_new_item")
    field = context.user_data.get("shop_add_current_field")
    if not item or not field:
        return ConversationHandler.END
    try:
        item[field] = int(update.message.text.strip())
    except ValueError:
        await update.message.reply_text("لطفاً فقط عدد بفرست.")
        return SHOP_ADD_STAT

    fields_queue = context.user_data.get("shop_add_fields_queue", [])
    if fields_queue:
        next_field = fields_queue.pop(0)
        context.user_data["shop_add_current_field"] = next_field
        text, entities = render_text("admin_shop_add_stat", stat_name=SHOP_STAT_LABELS[next_field])
        await update.message.reply_text(text, entities=entities)
        return SHOP_ADD_STAT

    data = load_data()
    category = item.pop("category")
    icon = item.pop("_icon", None)
    key = f"{category}_custom_{data['next_item_id']}"
    data["next_item_id"] += 1
    item["key"] = key
    if category != "food":
        item["level"] = len(data["shop"][category]) + 1
    data["shop"][category].append(item)
    if icon:
        data["item_icons"][key] = icon
    save_data()

    text, entities = render_text("admin_shop_add_done", name=item["name"])
    await update.message.reply_text(text, entities=entities, reply_markup=admin_main_keyboard())
    return ConversationHandler.END


# ------------- Conversation: ویرایش یک فیلد آیتم فروشگاه -------------
async def admin_shop_edit_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_admin(update):
        await safe_answer(query, "دسترسی نداری.", show_alert=True)
        return ConversationHandler.END
    await safe_answer(query, )
    _, category, key, field = query.data.split(":", 3)
    context.user_data["shop_edit_target"] = {"category": category, "key": key, "field": field}
    label = SHOP_STAT_LABELS.get(field, field if field != "name" and field != "price" else ("اسم" if field == "name" else f"قیمت ({CURRENCY_NAME})"))
    await query.message.reply_text(f"مقدار جدید برای «{label}» رو بفرست:")
    return SHOP_EDIT_FIELD


async def admin_shop_edit_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    target = context.user_data.get("shop_edit_target")
    if not target:
        return ConversationHandler.END
    data = load_data()
    item = find_item(data["shop"].get(target["category"], []), target["key"])
    if not item:
        await update.message.reply_text("این آیتم دیگه موجود نیست.", reply_markup=admin_main_keyboard())
        return ConversationHandler.END

    field = target["field"]
    if field == "name":
        entities_map = update.message.parse_entities(types=[MessageEntity.CUSTOM_EMOJI])
        item["name"] = update.message.text.strip()[:40]
        if entities_map:
            entity = list(entities_map.keys())[0]
            data["item_icons"][target["key"]] = entity.custom_emoji_id
    else:
        try:
            item[field] = int(update.message.text.strip())
        except ValueError:
            await update.message.reply_text("لطفاً فقط عدد بفرست.")
            return SHOP_EDIT_FIELD

    save_data()
    text, entities = render_text("admin_shop_edit_done")
    await update.message.reply_text(text, entities=entities, reply_markup=admin_main_keyboard())
    return ConversationHandler.END


# ------------- Conversation: افزودن کانال -------------
async def add_channel_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_admin(update):
        await safe_answer(query, "دسترسی نداری.", show_alert=True)
        return ConversationHandler.END
    await safe_answer(query, )
    await query.message.reply_text(
        "آیدی عددی، یوزرنیم (مثل @mychannel) یا لینک کانال رو بفرست.\nربات باید ادمین اون کانال باشه.\nبرای لغو: /cancel"
    )
    return ADD_CHANNEL


async def add_channel_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    text = update.message.text.strip()
    chat_id = text
    if text.startswith("https://t.me/"):
        chat_id = "@" + text.split("https://t.me/")[1].strip("/")
    try:
        chat = await context.bot.get_chat(chat_id)
    except (BadRequest, Forbidden) as e:
        await update.message.reply_text(f"نشد اطلاعات این کانال رو بگیرم: {e}\nدوباره امتحان کن یا /cancel بزن.")
        return ADD_CHANNEL

    link = None
    if chat.username:
        link = f"https://t.me/{chat.username}"
    else:
        try:
            link = await context.bot.export_chat_invite_link(chat.id)
        except (BadRequest, Forbidden) as e:
            await update.message.reply_text(
                "این کانال یوزرنیم عمومی نداره و ربات هم نتونست خودکار لینک دعوت بسازه "
                f"(خطا: {e}).\nربات باید تو این کانال ادمین باشه و دسترسی «دعوت کاربران» رو داشته باشه.\n"
                "بعد از ادمین کردن ربات، دوباره همین آیدی/یوزرنیم رو بفرست، یا /cancel بزن."
            )
            return ADD_CHANNEL

    data = load_data()
    data["channels"].append(
        {"chat_id": chat.id, "title": chat.title or chat.username or str(chat.id), "link": link}
    )
    save_data()
    await update.message.reply_text(f"✅ کانال «{chat.title}» اضافه شد.", reply_markup=admin_main_keyboard())
    return ConversationHandler.END


# ------------- Conversation: افزودن کاراکتر -------------
async def add_char_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_admin(update):
        await safe_answer(query, "دسترسی نداری.", show_alert=True)
        return ConversationHandler.END
    await safe_answer(query, )
    await query.message.reply_text(
        "خود ایموجی پرمیوم مورد نظر رو (فقط همون ایموجی) برام بفرست.\nبرای لغو: /cancel"
    )
    return ADD_CHAR_EMOJI


async def add_char_receive_emoji(update: Update, context: ContextTypes.DEFAULT_TYPE):
    message = update.message
    entities_map = message.parse_entities(types=[MessageEntity.CUSTOM_EMOJI])
    if not entities_map:
        await update.message.reply_text(
            "ایموجی پرمیوم پیدا نشد. مطمئن شو که یه ایموجی پرمیوم واقعی (نه ایموجی معمولی) فرستادی.\nدوباره امتحان کن یا /cancel بزن."
        )
        return ADD_CHAR_EMOJI

    entity = list(entities_map.keys())[0]
    placeholder = entities_map[entity]
    context.user_data["new_char_emoji_id"] = entity.custom_emoji_id
    context.user_data["new_char_placeholder"] = placeholder
    await update.message.reply_text("عالی! حالا یه اسم پیش‌فرض برای این کاراکتر بفرست (مثلاً: اژدهای آتشین).")
    return ADD_CHAR_NAME


async def add_char_receive_name(update: Update, context: ContextTypes.DEFAULT_TYPE):
    name = update.message.text.strip()[:32]
    context.user_data["new_char_name"] = name
    rows = rows_clean([[kb_button("admin_char_vip_yes", "admin_char_vip:yes"), kb_button("admin_char_vip_no", "admin_char_vip:no")]])
    await update.message.reply_text("این کاراکتر مخصوص کاربرهای VIP باشه یا برای همه؟", reply_markup=InlineKeyboardMarkup(rows))
    return ADD_CHAR_VIP


async def add_char_vip_choice(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_admin(update):
        await safe_answer(query, "دسترسی نداری.", show_alert=True)
        return ConversationHandler.END
    vip_only = query.data.split(":", 1)[1] == "yes"
    context.user_data["new_char_vip"] = vip_only
    await safe_answer(query, )
    await query.message.reply_text(
        "این کاراکتر پولیه؟ اگه آره، قیمتش رو به " + CURRENCY_NAME + " بفرست (فقط عدد).\n"
        "اگه رایگانه و از طریق تاس رندوم/VIP به کاربرها داده بشه، عدد 0 بفرست."
    )
    return ADD_CHAR_PRICE


async def add_char_price_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    try:
        price = int(update.message.text.strip())
        if price < 0:
            raise ValueError
    except ValueError:
        await update.message.reply_text("لطفاً فقط یه عدد صفر یا مثبت بفرست.")
        return ADD_CHAR_PRICE

    data = load_data()
    name = context.user_data.get("new_char_name", "کاراکتر")
    vip_only = context.user_data.get("new_char_vip", False)
    new_char = {
        "id": data["next_char_id"],
        "custom_emoji_id": context.user_data["new_char_emoji_id"],
        "placeholder": context.user_data["new_char_placeholder"],
        "base_name": name,
        "vip_only": vip_only,
        "price": price,
    }
    data["characters"].append(new_char)
    data["next_char_id"] += 1
    save_data()

    tb = TextBuilder()
    tb.add_custom_emoji(new_char["placeholder"], new_char["custom_emoji_id"])
    star = " 🌟(VIP)" if vip_only else ""
    price_note = f" — قیمت: {price} {CURRENCY_NAME}" if price else " (رایگان/رندوم)"
    tb.add_text(f" کاراکتر «{name}»{star}{price_note} با موفقیت اضافه شد ✅")
    text, entities = tb.build()
    await update.message.reply_text(text, entities=entities, reply_markup=admin_main_keyboard())
    return ConversationHandler.END


# ------------- Conversation: وارد کردن کل یک پک ایموجی -------------
async def import_pack_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_admin(update):
        await safe_answer(query, "دسترسی نداری.", show_alert=True)
        return ConversationHandler.END
    await safe_answer(query, )
    await query.message.reply_text(
        "نام کوتاه پک رو بفرست (یا لینکش، مثل https://t.me/addemoji/AnimeEmojiPack).\n"
        "ربات همهٔ ایموجی‌های پک رو خودکار به‌عنوان کاراکتر اضافه می‌کنه.\nبرای لغو: /cancel"
    )
    return IMPORT_PACK


async def import_pack_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    raw = update.message.text.strip()
    if "t.me/addemoji/" in raw:
        pack_name = raw.split("t.me/addemoji/")[1].strip("/")
    elif raw.startswith("@"):
        pack_name = raw[1:]
    else:
        pack_name = raw

    try:
        sticker_set = await context.bot.get_sticker_set(name=pack_name)
    except (BadRequest, Forbidden) as e:
        await update.message.reply_text(
            f"نتونستم پک «{pack_name}» رو پیدا کنم: {e}\nدوباره امتحان کن یا /cancel بزن."
        )
        return IMPORT_PACK
    except Exception as e:
        logger.exception("خطای غیرمنتظره در import_pack_receive")
        await update.message.reply_text(
            f"یه خطای غیرمنتظره پیش اومد: {e}\nدوباره امتحان کن یا /cancel بزن."
        )
        return IMPORT_PACK

    context.user_data["import_pack_stickers"] = sticker_set.stickers
    context.user_data["import_pack_name"] = pack_name
    rows = rows_clean([[kb_button("admin_char_vip_yes", "admin_pack_vip:yes"), kb_button("admin_char_vip_no", "admin_pack_vip:no")]])
    await update.message.reply_text(
        "همهٔ کاراکترهای این پک مخصوص کاربرهای VIP باشن یا برای همه؟",
        reply_markup=InlineKeyboardMarkup(rows),
    )
    return IMPORT_PACK_VIP


async def import_pack_vip_choice(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_admin(update):
        await safe_answer(query, "دسترسی نداری.", show_alert=True)
        return ConversationHandler.END
    vip_only = query.data.split(":", 1)[1] == "yes"
    stickers = context.user_data.get("import_pack_stickers", [])
    pack_name = context.user_data.get("import_pack_name", "")
    await safe_answer(query, )

    data = load_data()
    try:
        existing_ids = {c["custom_emoji_id"] for c in data["characters"]}
        added = 0
        skipped_no_id = 0
        for sticker in stickers:
            cid = getattr(sticker, "custom_emoji_id", None)
            if not cid:
                skipped_no_id += 1
                continue
            if cid in existing_ids:
                continue
            placeholder = sticker.emoji or "🔥"
            data["characters"].append(
                {
                    "id": data["next_char_id"],
                    "custom_emoji_id": cid,
                    "placeholder": placeholder,
                    "base_name": f"کاراکتر {data['next_char_id']}",
                    "vip_only": vip_only,
                }
            )
            data["next_char_id"] += 1
            added += 1
        save_data()
    except Exception as e:
        logger.exception("خطا هنگام ذخیرهٔ کاراکترهای وارد شده از پک")
        await query.message.reply_text(
            f"پک پیدا شد ولی هنگام ذخیره‌سازی خطا خوردیم: {e}\nدوباره امتحان کن.",
            reply_markup=admin_main_keyboard(),
        )
        return ConversationHandler.END

    star = " 🌟(VIP)" if vip_only else ""
    msg = f"✅ از پک «{pack_name}»، {added} کاراکتر جدید{star} اضافه شد."
    if skipped_no_id:
        msg += f"\n({skipped_no_id} آیتم این پک ایموجی سفارشی نبودن و رد شدن.)"
    msg += "\nاسم پیش‌فرضشون «کاراکتر ۱، ۲، ...» هست؛ هر کدوم رو بعداً می‌تونی حذف و با اسم دلخواه دوباره اضافه کنی."
    await query.message.reply_text(msg, reply_markup=admin_main_keyboard())
    return ConversationHandler.END


# ------------- Conversation: ویرایش متن -------------
async def edit_text_pick(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_admin(update):
        await safe_answer(query, "دسترسی نداری.", show_alert=True)
        return ConversationHandler.END
    await safe_answer(query, )
    key = query.data.split(":", 1)[1]
    context.user_data["edit_text_key"] = key
    data = load_data()
    current = data["texts"].get(key, "")
    await query.message.reply_text(
        f"متن فعلی «{key}»:\n\n{current}\n\n—————\nمتن جدید رو بفرست (برای لغو: /cancel):"
    )
    return EDIT_TEXT


async def edit_text_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    data = load_data()
    key = context.user_data.get("edit_text_key")
    if not key:
        return ConversationHandler.END
    data["texts"][key] = update.message.text

    entities_map = update.message.parse_entities(types=[MessageEntity.CUSTOM_EMOJI])
    if entities_map:
        data["texts_entities"][key] = [
            {"offset": ent.offset, "length": ent.length, "custom_emoji_id": ent.custom_emoji_id}
            for ent in entities_map.keys()
        ]
    else:
        data["texts_entities"].pop(key, None)

    save_data()
    note = ""
    if entities_map:
        note = "\n(ایموجی پرمیومی که گذاشتی هم ذخیره شد و همیشه پرمیوم نمایش داده می‌شه.)"
    await update.message.reply_text(f"✅ متن به‌روزرسانی شد.{note}", reply_markup=admin_main_keyboard())
    return ConversationHandler.END


# ------------- Conversation: ویرایش دکمه -------------
async def edit_button_pick(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_admin(update):
        await safe_answer(query, "دسترسی نداری.", show_alert=True)
        return ConversationHandler.END
    await safe_answer(query, )
    key = query.data.split(":", 1)[1]
    context.user_data["edit_button_key"] = key
    data = load_data()
    enabled = data["buttons"][key].get("enabled", True)
    rows = [
        [
            InlineKeyboardButton("🔵 آبی", callback_data=f"admin_setstyle:{key}:primary"),
            InlineKeyboardButton("🟢 سبز", callback_data=f"admin_setstyle:{key}:success"),
            InlineKeyboardButton("🔴 قرمز", callback_data=f"admin_setstyle:{key}:danger"),
            InlineKeyboardButton("⚪️ پیش‌فرض", callback_data=f"admin_setstyle:{key}:none"),
        ],
        [InlineKeyboardButton("🚫 حذف آیکون دستی این دکمه", callback_data=f"admin_clearicon:{key}")],
        [InlineKeyboardButton(
            "🚫 غیرفعال کردن این دکمه" if enabled else "✅ فعال کردن این دکمه",
            callback_data=f"admin_toggleenabled:{key}",
        )],
    ]
    await query.message.reply_text(
        "برای تغییر متن دکمه، متن جدید رو بفرست.\n"
        "برای گذاشتن ایموجی پرمیوم کنار دکمه (چون داخل متن دکمه اصلاً امکانش نیست)، خودِ ایموجی پرمیوم رو بفرست.\n"
        "رنگ و فعال/غیرفعال بودن رو هم می‌تونی از دکمه‌های پایین انتخاب کنی. برای لغو: /cancel",
        reply_markup=InlineKeyboardMarkup(rows),
    )
    return EDIT_BUTTON_TEXT


async def edit_button_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    data = load_data()
    key = context.user_data.get("edit_button_key")
    if not key:
        return ConversationHandler.END

    entities_map = update.message.parse_entities(types=[MessageEntity.CUSTOM_EMOJI])
    if entities_map:
        entity = list(entities_map.keys())[0]
        data["buttons"][key]["icon"] = entity.custom_emoji_id
        save_data()
        await update.message.reply_text(
            "✅ ایموجی پرمیوم به‌عنوان آیکون کنار این دکمه ست شد.", reply_markup=admin_main_keyboard()
        )
        return ConversationHandler.END

    data["buttons"][key]["text"] = update.message.text.strip()[:40]
    save_data()
    await update.message.reply_text("✅ متن دکمه به‌روزرسانی شد.", reply_markup=admin_main_keyboard())
    return ConversationHandler.END


# ------------- Conversation: آیکون آیتم‌های فروشگاه -------------
async def set_item_icon_pick(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_admin(update):
        await safe_answer(query, "دسترسی نداری.", show_alert=True)
        return ConversationHandler.END
    await safe_answer(query, )
    key = query.data.split(":", 1)[1]
    context.user_data["item_icon_key"] = key
    await query.message.reply_text("خود ایموجی پرمیوم رو برای این آیتم بفرست. برای لغو: /cancel")
    return SET_ITEM_ICON


async def set_item_icon_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    data = load_data()
    key = context.user_data.get("item_icon_key")
    if not key:
        return ConversationHandler.END
    entities_map = update.message.parse_entities(types=[MessageEntity.CUSTOM_EMOJI])
    if not entities_map:
        await update.message.reply_text("ایموجی پرمیوم پیدا نشد. دوباره امتحان کن یا /cancel بزن.")
        return SET_ITEM_ICON
    entity = list(entities_map.keys())[0]
    data["item_icons"][key] = entity.custom_emoji_id
    save_data()
    await update.message.reply_text("✅ آیکون این آیتم ست شد.", reply_markup=admin_main_keyboard())
    return ConversationHandler.END


# ------------- Conversation: دادن امتیاز -------------
async def give_points_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_admin(update):
        await safe_answer(query, "دسترسی نداری.", show_alert=True)
        return ConversationHandler.END
    await safe_answer(query, )
    if query.data.startswith("admin_givepoints_start:"):
        target_uid = query.data.split(":", 1)[1]
        context.user_data["give_points_uid"] = target_uid
        await query.message.reply_text(f"چند {CURRENCY_NAME} اضافه بشه؟ یه عدد بفرست (منفی هم مجازه). لغو: /cancel")
        return GIVE_POINTS_AMOUNT
    await query.message.reply_text("آیدی عددی کاربر رو بفرست:")
    return GIVE_POINTS_UID


async def give_points_receive_uid(update: Update, context: ContextTypes.DEFAULT_TYPE):
    uid = update.message.text.strip()
    data = load_data()
    if uid not in data["users"]:
        await update.message.reply_text("همچین کاربری تو دیتابیس ربات نیست. دوباره امتحان کن یا /cancel.")
        return GIVE_POINTS_UID
    context.user_data["give_points_uid"] = uid
    await update.message.reply_text(f"چند {CURRENCY_NAME} اضافه بشه؟ یه عدد بفرست (منفی هم مجازه).")
    return GIVE_POINTS_AMOUNT


async def give_points_receive_amount(update: Update, context: ContextTypes.DEFAULT_TYPE):
    data = load_data()
    uid = context.user_data.get("give_points_uid")
    try:
        amount = int(update.message.text.strip())
    except ValueError:
        await update.message.reply_text("لطفاً فقط عدد بفرست.")
        return GIVE_POINTS_AMOUNT
    data["users"][uid]["points"] = max(0, data["users"][uid]["points"] + amount)
    save_data()
    await update.message.reply_text(f"✅ {CURRENCY_NAME} اعمال شد.", reply_markup=admin_main_keyboard())
    return ConversationHandler.END


# ------------- Conversation: پیام همگانی -------------
async def broadcast_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if not is_admin(update):
        await safe_answer(query, "دسترسی نداری.", show_alert=True)
        return ConversationHandler.END
    await safe_answer(query, )
    await query.message.reply_text("متن پیام همگانی رو بفرست (برای لغو: /cancel):")
    return BROADCAST_MSG


async def broadcast_receive(update: Update, context: ContextTypes.DEFAULT_TYPE):
    data = load_data()
    text = update.message.text
    sent, failed = 0, 0
    for uid in data["users"].keys():
        try:
            await context.bot.send_message(chat_id=int(uid), text=text)
            sent += 1
        except (BadRequest, Forbidden):
            failed += 1
    await update.message.reply_text(f"✅ ارسال شد.\nموفق: {sent} | ناموفق: {failed}", reply_markup=admin_main_keyboard())
    return ConversationHandler.END


async def global_error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):
    """هر خطای پیش‌بینی‌نشده رو لاگ می‌کنه و در صورت امکان یه پیام واضح به کاربر می‌ده
    تا هیچ‌وقت یه دکمه بدون هیچ واکنشی نمونه."""
    logger.exception("خطای مدیریت‌نشده در پردازش یک آپدیت", exc_info=context.error)
    try:
        if isinstance(update, Update) and update.effective_message:
            await update.effective_message.reply_text(
                "یه خطای غیرمنتظره پیش اومد. دوباره امتحان کن یا /admin (یا /start) رو بزن."
            )
    except Exception:
        pass


# ----------------------------------------------------------------------------
# ثبت هندلرها و اجرا
# ----------------------------------------------------------------------------
async def _on_shutdown(app):
    force_flush_data_sync()


def main():
    load_data()
    # concurrent_updates=True خیلی مهمه: بدونش، تا وقتی یه هندلر کند (مثلاً انیمیشن ۱ دقیقه‌ای مبارزه)
    # در حال اجراست، کل ربات برای همهٔ کاربرهای دیگه هم «فریز» می‌مونه چون آپدیت‌ها پشت سر هم
    # (سری) پردازش می‌شن. با این تنظیم، آپدیت‌های مختلف هم‌زمان و مستقل از هم پردازش می‌شن.
    app = Application.builder().token(BOT_TOKEN).concurrent_updates(True).post_shutdown(_on_shutdown).build()
    app.add_error_handler(global_error_handler)

    # /start و نام‌گذاری کاراکتر
    start_conv = ConversationHandler(
        entry_points=[CommandHandler("start", start_cmd)],
        states={NAME_CHAR: [MessageHandler(filters.TEXT & ~filters.COMMAND, receive_char_name)]},
        fallbacks=[CommandHandler("cancel", cancel_conv)],
        name="start_conv",
    )
    app.add_handler(start_conv)

    app.add_handler(CallbackQueryHandler(check_join_callback, pattern="^check_join$"))
    app.add_handler(CommandHandler("admin", admin_cmd))
    app.add_handler(CommandHandler("endchat", endchat_cmd))
    app.add_handler(CallbackQueryHandler(admin_chat_open_callback, pattern=r"^admin_chat_open:"))

    app.add_handler(CallbackQueryHandler(noop_callback, pattern="^noop$"))

    app.add_handler(
        CallbackQueryHandler(
            menu_router,
            pattern="^menu_(feed|battle|profile|leaderboard|vip|help|back|admin_panel|market|market_food|market_weapon|market_clothes|market_character|inventory|inv_weapon|inv_clothes|games|rematch|swap_char|partner|partner_child_view|mychars)$",
        )
    )
    app.add_handler(CallbackQueryHandler(feeduse_callback, pattern=r"^feeduse:"))
    app.add_handler(CallbackQueryHandler(buy_item_callback, pattern=r"^buy:"))
    app.add_handler(CallbackQueryHandler(sell_item_callback, pattern=r"^sell:"))
    app.add_handler(CallbackQueryHandler(equip_callback, pattern=r"^equip:"))
    app.add_handler(CallbackQueryHandler(unequip_callback, pattern=r"^unequip:"))

    # بازی‌ها
    app.add_handler(CallbackQueryHandler(game_pick_callback, pattern=r"^game_pick:"))
    app.add_handler(CallbackQueryHandler(game_cancel_queue_callback, pattern=r"^game_cancelq:"))
    app.add_handler(CallbackQueryHandler(group_new_game_callback, pattern=r"^group_new:"))
    app.add_handler(CallbackQueryHandler(group_game_join_callback, pattern=r"^game_join:"))
    app.add_handler(CallbackQueryHandler(xo_move_callback, pattern=r"^xo_move:"))
    app.add_handler(CallbackQueryHandler(rps_choice_callback, pattern=r"^rps_choice:"))
    app.add_handler(CallbackQueryHandler(dice_roll_callback, pattern=r"^dice_roll:"))
    app.add_handler(CallbackQueryHandler(game_cancel_callback, pattern=r"^game_cancel:"))
    app.add_handler(CallbackQueryHandler(game_chat_toggle_callback, pattern=r"^game_chat:"))
    app.add_handler(CallbackQueryHandler(rematch_pick_callback, pattern=r"^rematch_pick:"))
    app.add_handler(CallbackQueryHandler(rematch_game_callback, pattern=r"^rematch_game:"))
    app.add_handler(CallbackQueryHandler(invite_decision_callback, pattern=r"^invite_(accept|reject):"))
    app.add_handler(CallbackQueryHandler(hokm_pick_callback, pattern="^hokm_pick$"))
    app.add_handler(CallbackQueryHandler(hokm_group_start_callback, pattern="^hokm_group_start$"))
    app.add_handler(CallbackQueryHandler(hokm_join_callback, pattern=r"^hokm_join:"))
    app.add_handler(CallbackQueryHandler(hokm_trump_callback, pattern=r"^hokm_trump:"))
    app.add_handler(CallbackQueryHandler(hokm_play_callback, pattern=r"^hokm_play:"))
    app.add_handler(
        MessageHandler(
            filters.TEXT & filters.ChatType.GROUPS & filters.Regex(r"^بازی$") & ~filters.COMMAND,
            group_game_text_trigger,
        )
    )
    app.add_handler(
        MessageHandler(
            filters.TEXT & filters.ChatType.GROUPS & filters.Regex(r"^قمار$") & ~filters.COMMAND,
            group_gamble_text_trigger,
        )
    )
    app.add_handler(
        ConversationHandler(
            entry_points=[CallbackQueryHandler(group_gamble_new_start, pattern=r"^group_gamble_new:")],
            states={GAMBLE_STAKE: [MessageHandler(filters.TEXT & ~filters.COMMAND, group_gamble_stake_receive)]},
            fallbacks=[CommandHandler("cancel", cancel_conv)],
            name="group_gamble_conv",
        )
    )
    app.add_handler(
        MessageHandler(
            filters.TEXT & filters.ChatType.GROUPS & filters.Regex(r"^فمبوی کوچولو$") & ~filters.COMMAND,
            fambo_kid_callback,
        )
    )
    app.add_handler(
        MessageHandler(
            filters.TEXT & filters.ChatType.GROUPS & filters.Regex(r"^فمبوی$") & ~filters.COMMAND,
            fambo_panel_trigger,
        )
    )
    app.add_handler(
        MessageHandler(
            filters.TEXT & filters.ChatType.GROUPS & filters.Regex(r"^راهنما$") & ~filters.COMMAND,
            group_help_trigger,
        )
    )
    app.add_handler(
        ConversationHandler(
            entry_points=[
                MessageHandler(
                    filters.TEXT & filters.ChatType.GROUPS & filters.Regex(r"^انتقال فمبوی پوینت$") & ~filters.COMMAND,
                    transfer_start,
                )
            ],
            states={TRANSFER_AMOUNT: [MessageHandler(filters.TEXT & ~filters.COMMAND, transfer_amount_receive)]},
            fallbacks=[CommandHandler("cancel", cancel_conv)],
            name="transfer_conv",
        )
    )
    app.add_handler(CallbackQueryHandler(speedclick_callback, pattern=r"^speedclick:"))
    # ردیابی گروه‌ها تو یه دیسپچ‌گروه جدا (group=1) تا با بقیهٔ هندلرها تداخل نکنه
    app.add_handler(MessageHandler(filters.ChatType.GROUPS, track_group_chat), group=1)

    # جاب دوره‌ای برای بستن خودکار بازی‌های بی‌فعالیت (بیش از ۵ دقیقه)
    if app.job_queue is not None:
        app.job_queue.run_repeating(check_idle_games, interval=60, first=30)
        app.job_queue.run_repeating(speedclick_job, interval=600, first=600)
        app.job_queue.run_repeating(flush_data_job, interval=2, first=2)
        app.job_queue.run_repeating(close_group_panels_job, interval=30, first=30)
    else:
        global _autoflush_enabled
        _autoflush_enabled = False
        logger.warning(
            "job_queue در دسترس نیست (احتمالاً pip install \"python-telegram-bot[job-queue]\" نصب نشده)؛ "
            "بستن خودکار بازی‌های بی‌فعالیت و رویداد کلیک سریع غیرفعالن، و ذخیره‌سازی دیتا هم "
            "برمی‌گرده به حالت فوری (کندتر ولی مطمئن)."
        )

    # تعویض کاراکتر (رندوم / درخواست از ادمین)
    app.add_handler(CallbackQueryHandler(swap_random_callback, pattern="^swap_random$"))
    app.add_handler(CallbackQueryHandler(swap_admin_request_callback, pattern="^swap_admin_request$"))
    app.add_handler(CallbackQueryHandler(swap_vip_list_callback, pattern="^swap_vip_list$"))
    app.add_handler(CallbackQueryHandler(swap_vip_pick_callback, pattern=r"^swap_vip_pick:"))
    app.add_handler(CallbackQueryHandler(mychars_pick_callback, pattern=r"^mychars_pick:"))

    # پارتنر / خانواده
    app.add_handler(CallbackQueryHandler(partner_game_callback, pattern="^partner_game$"))
    app.add_handler(CallbackQueryHandler(partner_game_pick_callback, pattern=r"^partner_game_pick:"))
    app.add_handler(CallbackQueryHandler(partner_rp_callback, pattern="^partner_rp$"))
    app.add_handler(CallbackQueryHandler(partner_divorce_start_callback, pattern="^partner_divorce_start$"))
    app.add_handler(CallbackQueryHandler(divorce_decision_callback, pattern=r"^divorce_(accept|reject):"))
    app.add_handler(CallbackQueryHandler(child_feed_callback, pattern="^child_feed$"))
    app.add_handler(
        ConversationHandler(
            entry_points=[CallbackQueryHandler(partner_child_new_callback, pattern="^partner_child_new$")],
            states={CHILD_NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, child_name_receive)]},
            fallbacks=[CommandHandler("cancel", cancel_conv)],
            name="child_name_conv",
        )
    )

    # افزودن کانال
    app.add_handler(
        ConversationHandler(
            entry_points=[CallbackQueryHandler(add_channel_start, pattern="^admin_addchannel$")],
            states={ADD_CHANNEL: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_channel_receive)]},
            fallbacks=[CommandHandler("cancel", cancel_conv)],
            name="add_channel_conv",
        )
    )

    # افزودن کاراکتر
    app.add_handler(
        ConversationHandler(
            entry_points=[CallbackQueryHandler(add_char_start, pattern="^admin_addchar$")],
            states={
                ADD_CHAR_EMOJI: [MessageHandler(filters.ALL & ~filters.COMMAND, add_char_receive_emoji)],
                ADD_CHAR_NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_char_receive_name)],
                ADD_CHAR_VIP: [CallbackQueryHandler(add_char_vip_choice, pattern=r"^admin_char_vip:")],
                ADD_CHAR_PRICE: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_char_price_receive)],
            },
            fallbacks=[CommandHandler("cancel", cancel_conv)],
            name="add_char_conv",
        )
    )

    # ویرایش قیمت یه کاراکتر موجود
    app.add_handler(
        ConversationHandler(
            entry_points=[CallbackQueryHandler(admin_charprice_start, pattern=r"^admin_charprice_start:")],
            states={ADD_CHAR_PRICE: [MessageHandler(filters.TEXT & ~filters.COMMAND, admin_charprice_receive)]},
            fallbacks=[CommandHandler("cancel", cancel_conv)],
            name="admin_charprice_conv",
        )
    )

    # وارد کردن کل یک پک ایموجی
    app.add_handler(
        ConversationHandler(
            entry_points=[CallbackQueryHandler(import_pack_start, pattern="^admin_importpack$")],
            states={
                IMPORT_PACK: [MessageHandler(filters.TEXT & ~filters.COMMAND, import_pack_receive)],
                IMPORT_PACK_VIP: [CallbackQueryHandler(import_pack_vip_choice, pattern=r"^admin_pack_vip:")],
            },
            fallbacks=[CommandHandler("cancel", cancel_conv)],
            name="import_pack_conv",
        )
    )

    # ویرایش متن
    app.add_handler(
        ConversationHandler(
            entry_points=[CallbackQueryHandler(edit_text_pick, pattern=r"^admin_edittext_pick:")],
            states={EDIT_TEXT: [MessageHandler(filters.TEXT & ~filters.COMMAND, edit_text_receive)]},
            fallbacks=[CommandHandler("cancel", cancel_conv)],
            name="edit_text_conv",
        )
    )

    # ویرایش دکمه
    app.add_handler(
        ConversationHandler(
            entry_points=[CallbackQueryHandler(edit_button_pick, pattern=r"^admin_editbutton_pick:")],
            states={EDIT_BUTTON_TEXT: [MessageHandler(filters.TEXT & ~filters.COMMAND, edit_button_receive)]},
            fallbacks=[CommandHandler("cancel", cancel_conv)],
            name="edit_button_conv",
        )
    )

    # آیکون آیتم‌های فروشگاه
    app.add_handler(
        ConversationHandler(
            entry_points=[CallbackQueryHandler(set_item_icon_pick, pattern=r"^admin_setitemicon_pick:")],
            states={SET_ITEM_ICON: [MessageHandler(filters.ALL & ~filters.COMMAND, set_item_icon_receive)]},
            fallbacks=[CommandHandler("cancel", cancel_conv)],
            name="set_item_icon_conv",
        )
    )

    # تغییر اسم کاراکتر
    app.add_handler(
        ConversationHandler(
            entry_points=[CallbackQueryHandler(rename_char_start, pattern="^menu_rename_char$")],
            states={RENAME_CHAR: [MessageHandler(filters.TEXT & ~filters.COMMAND, rename_char_receive)]},
            fallbacks=[CommandHandler("cancel", cancel_conv)],
            name="rename_char_conv",
        )
    )

    # پاسخ ادمین به درخواست تعویض کاراکتر
    app.add_handler(
        ConversationHandler(
            entry_points=[CallbackQueryHandler(admin_answer_charreq_start, pattern=r"^admin_answer_charreq:")],
            states={ADMIN_CHAR_REQUEST: [MessageHandler(filters.ALL & ~filters.COMMAND, admin_answer_charreq_receive)]},
            fallbacks=[CommandHandler("cancel", cancel_conv)],
            name="admin_answer_charreq_conv",
        )
    )

    # دادن امتیاز
    app.add_handler(
        ConversationHandler(
            entry_points=[
                CallbackQueryHandler(give_points_start, pattern=r"^admin_givepoints_start:"),
            ],
            states={
                GIVE_POINTS_UID: [MessageHandler(filters.TEXT & ~filters.COMMAND, give_points_receive_uid)],
                GIVE_POINTS_AMOUNT: [MessageHandler(filters.TEXT & ~filters.COMMAND, give_points_receive_amount)],
            },
            fallbacks=[CommandHandler("cancel", cancel_conv)],
            name="give_points_conv",
        )
    )

    # پیام همگانی
    app.add_handler(
        ConversationHandler(
            entry_points=[CallbackQueryHandler(broadcast_start, pattern="^admin_broadcast$")],
            states={BROADCAST_MSG: [MessageHandler(filters.TEXT & ~filters.COMMAND, broadcast_receive)]},
            fallbacks=[CommandHandler("cancel", cancel_conv)],
            name="broadcast_conv",
        )
    )

    # افزودن آیتم فروشگاه
    app.add_handler(
        ConversationHandler(
            entry_points=[CallbackQueryHandler(admin_shop_add_start, pattern=r"^admin_shopadd_start:")],
            states={
                SHOP_ADD_NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, admin_shop_add_name_receive)],
                SHOP_ADD_PRICE: [MessageHandler(filters.TEXT & ~filters.COMMAND, admin_shop_add_price_receive)],
                SHOP_ADD_STAT: [MessageHandler(filters.TEXT & ~filters.COMMAND, admin_shop_add_stat_receive)],
            },
            fallbacks=[CommandHandler("cancel", cancel_conv)],
            name="shop_add_conv",
        )
    )

    # ویرایش یک فیلد آیتم فروشگاه
    app.add_handler(
        ConversationHandler(
            entry_points=[CallbackQueryHandler(admin_shop_edit_start, pattern=r"^admin_shopedit_start:")],
            states={SHOP_EDIT_FIELD: [MessageHandler(filters.TEXT & ~filters.COMMAND, admin_shop_edit_receive)]},
            fallbacks=[CommandHandler("cancel", cancel_conv)],
            name="shop_edit_conv",
        )
    )

    # بقیهٔ کال‌بک‌های ادمین (کانال‌ها، کاراکترها، متن‌ها، دکمه‌ها، کاربران، آمار، VIP toggle، استایل)
    app.add_handler(CallbackQueryHandler(admin_router, pattern="^admin_"))

    # رلهٔ چت رول‌پلی بین پارتنرها — همیشه آخرین هندلر متنیه تا مزاحم مکالمه‌های دیگه نشه
    app.add_handler(
        MessageHandler(filters.TEXT & filters.ChatType.PRIVATE & ~filters.COMMAND, final_text_relay_handler)
    )

    logger.info("ربات در حال اجراست...")
    app.run_polling(allowed_updates=Update.ALL_TYPES)


if __name__ == "__main__":
    main()