import telebot
from telebot import types
import sqlite3
from datetime import datetime, timedelta
import re
from collections import defaultdict, deque
import threading
import time
from contextlib import contextmanager
import uuid
import random
import logging
from logging.handlers import RotatingFileHandler
import functools
import os
from types import SimpleNamespace
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout

# ==========================================
# 🌟 پیکربندی سیستم لاگ‌گیری حرفه‌ای (Logging)
# ==========================================
logger = logging.getLogger('AnonBot')
logger.setLevel(logging.INFO)

# فرمت نمایش لاگ‌ها (تاریخ - نام ماژول - سطح ارور - متن پیام)
log_formatter = logging.Formatter('%(asctime)s - %(levelname)s - [Line: %(lineno)d] - %(message)s')

# ۱. هندلر فایل: ثبت همه‌چیز (ارورها و اطلاعات) در فایل با محدودیت حجم ۵ مگابایت
file_handler = RotatingFileHandler('bot_errors.log', maxBytes=5*1024*1024, backupCount=2, encoding='utf-8')
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(log_formatter)

# ۲. هندلر ترمینال: فقط نمایش ارورهای مهم (ERROR) برای خلوت ماندن ترمینال
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.ERROR) 
console_handler.setFormatter(log_formatter)

logger.addHandler(file_handler)
logger.addHandler(console_handler)

TOKEN = os.getenv('BOT_TOKEN', '').strip()
if not TOKEN:
    raise RuntimeError('BOT_TOKEN تنظیم نشده است؛ توکن جدید را در متغیر محیطی BOT_TOKEN قرار دهید.')
ADMINS = [2002131598, 897651192]  # عددی

# ======================================================================
# --- ثابت‌ها، حافظه‌ها و متغیرهای سراسری
# ======================================================================
# --- مچ‌میکینگ ---
REMATCH_COOLDOWN_DAYS = None

# --- جوین اجباری ---
FAIL_OPEN_ON_API_ERROR = False  # خطای بررسی عضویت نباید دسترسی را باز کند      
MEMBERSHIP_TTL_OK = 300            
MEMBERSHIP_TTL_NO = 10             
MEMBERSHIP_TTL_UNKNOWN = 30        
CHANNELS_CACHE_TTL = 60            

# --- دایرکت ---
BLOCK_LINKS_IN_DIRECT = True       

# --- گزارش‌ها ---
REPORTS_FOR_ADMIN_REVIEW = 3       
MAX_REPORTS_PER_DAY = 10

# --- مچ دوباره ---
REMATCH_RELAX_AFTER_SECONDS = 180

membership_cache = {}              
channels_cache = {'t': 0.0, 'v': []}
cache_lock = threading.Lock()
_notice_last = {}                  
pending_start_links = {}           
pending_start_links_lock = threading.Lock()

# --- گیت (thread دریافت آپدیت) ---
MEMBERSHIP_API_TIMEOUT = 5     
GATE_SHARDS = 8                
GATE_ALLOW, GATE_BANNED, GATE_NEED_JOIN, GATE_UNKNOWN, GATE_ERROR = 0, 1, 2, 3, 4
gate_shards = [ThreadPoolExecutor(max_workers=1, thread_name_prefix=f'gate{i}') for i in range(GATE_SHARDS)]
api_pool = ThreadPoolExecutor(max_workers=16, thread_name_prefix='tgapi')
# تعداد ثابت قفل‌ها؛ برای جلوگیری از حذف/ساخت هم‌زمان قفل یک کاربر و رشد بی‌نهایت دیکشنری
user_check_locks = [threading.Lock() for _ in range(128)]      
# قفل‌های ثابت و شاردشده؛ هم ارسال پیام و هم پایان همان چت از یک قفل استفاده می‌کنند.
chat_lifecycle_locks = [threading.RLock() for _ in range(256)]
# جلوگیری از شروع هم‌زمان دو عملیات refund در همین Process
stars_refund_lock = threading.Lock()
LINK_PATTERN = re.compile(r"(?i)(https?://|www\.|t\.me/|telegram\.me/|\.com\b|\.ir\b|\.net\b|\.org\b|\.co\b)")

waiting_pool = deque() # صف مرکزی واحد برای تمام جستجوها

# ======================================================================
# --- توابع دیتابیس و Migration (آپدیت امنیتی)
# ======================================================================
def configure_sqlite():
    """WAL را یک‌بار تنظیم می‌کند؛ روی هر connection دوباره journal_mode عوض نمی‌شود."""
    conn = sqlite3.connect('anonchat.db', timeout=10, check_same_thread=False)
    try:
        conn.execute('PRAGMA journal_mode=WAL;')
        conn.execute('PRAGMA synchronous=NORMAL;')
        conn.execute('PRAGMA foreign_keys=ON;')
        conn.execute('PRAGMA busy_timeout=10000;')
        conn.commit()
    finally:
        conn.close()

@contextmanager
def get_db_connection():
    """Connection کوتاه‌عمر؛ WAL فقط در startup تنظیم می‌شود."""
    conn = sqlite3.connect('anonchat.db', timeout=10, check_same_thread=False)
    try:
        conn.execute('PRAGMA foreign_keys=ON;')
        conn.execute('PRAGMA busy_timeout=10000;')
        yield conn
    finally:
        conn.close()

def _generate_private_link_token(cur):
    """ساخت توکن لینک خصوصی ۱۲۸ بیتی و بررسی برخورد با دیتابیس."""
    for _ in range(20):
        token = f"chat_{uuid.uuid4().hex}"  # ۳۲ رقم hex = ۱۲۸ بیت
        cur.execute('SELECT 1 FROM users WHERE unique_link = ? LIMIT 1', (token,))
        if cur.fetchone() is None:
            return token
    raise RuntimeError('Could not generate a unique private-link token')


def _store_pending_start_link(user_id, message_text, ttl=3600, max_items=5000):
    """ذخیره امن لینک معلق؛ فقط ورودی منقضی یا قدیمی‌ترین ورودی حذف می‌شود."""
    if not message_text or not isinstance(message_text, str):
        return False

    now = time.time()
    with pending_start_links_lock:
        for uid, data in list(pending_start_links.items()):
            try:
                created_at = float(data[1])
            except (TypeError, ValueError, IndexError, KeyError):
                pending_start_links.pop(uid, None)
                continue
            if now - created_at >= ttl:
                pending_start_links.pop(uid, None)

        if user_id not in pending_start_links and len(pending_start_links) >= max_items:
            try:
                oldest_uid = min(
                    pending_start_links,
                    key=lambda uid: float(pending_start_links[uid][1])
                )
                pending_start_links.pop(oldest_uid, None)
            except (ValueError, TypeError, IndexError, KeyError):
                logger.warning('pending_start_links contained invalid entries; pruning them')
                for uid, data in list(pending_start_links.items()):
                    try:
                        float(data[1])
                    except (TypeError, ValueError, IndexError, KeyError):
                        pending_start_links.pop(uid, None)

        pending_start_links[user_id] = (message_text, now)
        return True


def _pop_pending_start_link(user_id, ttl=3600):
    """برداشت یک‌باره لینک معلق؛ لینک منقضی‌شده برگردانده نمی‌شود."""
    with pending_start_links_lock:
        data = pending_start_links.pop(user_id, None)

    if not data:
        return None
    try:
        message_text, created_at = data[0], float(data[1])
    except (TypeError, ValueError, IndexError, KeyError):
        return None

    if time.time() - created_at >= ttl:
        return None
    return message_text


def ensure_identity_token_indexes():
    """یک‌بار در Startup توکن‌های کوتاه قدیمی را ارتقا و یکتایی را در SQLite enforce می‌کند."""
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')

        # توکن‌های قدیمی chat_<8hex> فقط ۳۲ بیت‌اند؛ با توکن ۱۲۸ بیتی جایگزین می‌شوند.
        rows = cur.execute(
            "SELECT user_id, unique_link FROM users "
            "WHERE unique_link IS NOT NULL AND unique_link <> '' ORDER BY user_id"
        ).fetchall()
        seen_links = set()
        for uid, old_token in rows:
            token = str(old_token)
            is_current_format = bool(re.fullmatch(r'chat_[0-9a-fA-F]{32}', token))
            duplicate = token in seen_links
            if not is_current_format or duplicate:
                token = _generate_private_link_token(cur)
                cur.execute('UPDATE users SET unique_link = ? WHERE user_id = ?', (token, uid))
            seen_links.add(token)

        # profile_id عمومی است، اما باید یکتا باشد تا جست‌وجو به کاربر اشتباه نرسد.
        rows = cur.execute(
            "SELECT user_id, profile_id FROM users "
            "WHERE profile_id IS NOT NULL AND profile_id <> '' ORDER BY user_id"
        ).fetchall()
        seen_profiles = set()
        for uid, old_id in rows:
            profile_id = str(old_id)
            # شناسه‌های قدیمی ۶ رقمی هم برای جلوگیری از enumeration ساده به ۱۶ رقم ارتقا می‌یابند.
            is_short_profile_id = len(profile_id) < 12
            if profile_id in seen_profiles or is_short_profile_id:
                for _ in range(20):
                    candidate = uuid.uuid4().hex[:16]
                    if candidate not in seen_profiles:
                        cur.execute('SELECT 1 FROM users WHERE profile_id = ? LIMIT 1', (candidate,))
                        if cur.fetchone() is None:
                            profile_id = candidate
                            cur.execute('UPDATE users SET profile_id = ? WHERE user_id = ?', (profile_id, uid))
                            break
                else:
                    raise RuntimeError('Could not generate a unique profile id')
            seen_profiles.add(profile_id)

        cur.execute(
            "CREATE UNIQUE INDEX IF NOT EXISTS idx_users_unique_link_unique "
            "ON users(unique_link) WHERE unique_link IS NOT NULL AND unique_link <> ''"
        )
        cur.execute(
            "CREATE UNIQUE INDEX IF NOT EXISTS idx_users_profile_id_unique "
            "ON users(profile_id) WHERE profile_id IS NOT NULL AND profile_id <> ''"
        )
        conn.commit()

def _add_column_if_missing(cur, table, column, definition):
    try:
        cur.execute(f'ALTER TABLE {table} ADD COLUMN {column} {definition}')
    except sqlite3.OperationalError as exc:
        if 'duplicate column name' not in str(exc).lower():
            raise

def apply_hardening_migrations():
    """ساخت و ارتقای جداول امنیت، سکه، بازخورد و صف‌های حذف پایدار."""
    with get_db_connection() as conn:
        cur = conn.cursor()

        # ستون‌های افزوده‌شده به جداول قدیمی
        _add_column_if_missing(
            cur, 'users', 'extra_chat_credits', 'INTEGER DEFAULT 0'
        )
        _add_column_if_missing(
            cur, 'chat_requests', 'fee_paid', 'INTEGER DEFAULT 0'
        )

        # بازخورد چت؛ هر کاربر برای هر هدف در هر چت فقط یک رأی
        cur.execute('''
            CREATE TABLE IF NOT EXISTS chat_feedback (
                chat_id INTEGER NOT NULL,
                rater_id INTEGER NOT NULL,
                target_id INTEGER NOT NULL,
                score INTEGER NOT NULL CHECK(score BETWEEN 1 AND 5),
                created_at TEXT NOT NULL,
                UNIQUE(chat_id, rater_id, target_id)
            )
        ''')
        cur.execute('''
            CREATE INDEX IF NOT EXISTS idx_feedback_target
            ON chat_feedback(target_id)
        ''')

        # تاریخچه انتقال سکه
        cur.execute('''
            CREATE TABLE IF NOT EXISTS coin_transfers (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                sender_id INTEGER NOT NULL,
                receiver_id INTEGER NOT NULL,
                amount INTEGER NOT NULL CHECK(amount > 0),
                created_at TEXT NOT NULL
            )
        ''')
        cur.execute('''
            CREATE INDEX IF NOT EXISTS idx_coin_transfer_sender
            ON coin_transfers(sender_id)
        ''')
        cur.execute('''
            CREATE INDEX IF NOT EXISTS idx_coin_transfer_receiver
            ON coin_transfers(receiver_id)
        ''')

        # صف حذف مدیای زمان‌دار
        cur.execute('''
            CREATE TABLE IF NOT EXISTS secure_media_deletions (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                chat_id INTEGER NOT NULL,
                sender_id INTEGER NOT NULL,
                receiver_id INTEGER NOT NULL,
                original_message_id INTEGER NOT NULL,
                sent_message_id INTEGER NOT NULL,
                status_chat_id INTEGER,
                status_message_id INTEGER,
                delete_at REAL NOT NULL,
                attempts INTEGER NOT NULL DEFAULT 0,
                retry_at REAL NOT NULL DEFAULT 0
            )
        ''')

        # ارتقای نصب‌هایی که جدول secure_media_deletions را از قبل داشته‌اند
        _add_column_if_missing(
            cur, 'secure_media_deletions',
            'attempts', 'INTEGER NOT NULL DEFAULT 0'
        )
        _add_column_if_missing(
            cur, 'secure_media_deletions',
            'retry_at', 'REAL NOT NULL DEFAULT 0'
        )
        cur.execute('''
            CREATE INDEX IF NOT EXISTS idx_secure_media_delete_at
            ON secure_media_deletions(delete_at)
        ''')
        cur.execute('''
            CREATE INDEX IF NOT EXISTS idx_secure_media_retry
            ON secure_media_deletions(retry_at, delete_at)
        ''')

        # شناسه پیام‌های چت امن؛ ثبت پایدار برای حذف بعد از پایان چت یا Restart
        cur.execute('''
            CREATE TABLE IF NOT EXISTS secure_chat_messages (
                chat_id INTEGER NOT NULL,
                user_id INTEGER NOT NULL,
                message_id INTEGER NOT NULL,
                created_at REAL NOT NULL,
                delete_requested INTEGER NOT NULL DEFAULT 0,
                retry_at REAL NOT NULL DEFAULT 0,
                attempts INTEGER NOT NULL DEFAULT 0,
                PRIMARY KEY (chat_id, user_id, message_id)
            )
        ''')
        cur.execute('''
            CREATE INDEX IF NOT EXISTS idx_secure_chat_messages_pending
            ON secure_chat_messages(delete_requested, retry_at, created_at)
        ''')
        cur.execute('''
            CREATE INDEX IF NOT EXISTS idx_secure_chat_messages_chat
            ON secure_chat_messages(chat_id, user_id)
        ''')

        # پس از Restart هیچ Pair قبلی در RAM فعال نیست؛ پس تمام پیام‌های قبلی وارد صف حذف شوند.
        cur.execute('''
            UPDATE secure_chat_messages
            SET delete_requested = 1, retry_at = 0
            WHERE delete_requested = 0
        ''')

        # جدول دوستی‌های دوطرفه ناشناس (بدون افشای آیدی تلگرام)
        cur.execute('''
            CREATE TABLE IF NOT EXISTS friendships (
                user1_id INTEGER NOT NULL,
                user2_id INTEGER NOT NULL,
                created_at TEXT NOT NULL,
                origin_chat_id INTEGER,
                alias_by_user1 TEXT,
                alias_by_user2 TEXT,
                PRIMARY KEY (user1_id, user2_id),
                CHECK (user1_id < user2_id)
            )
        ''')
        cur.execute('''
            CREATE INDEX IF NOT EXISTS idx_friendships_user1
            ON friendships(user1_id)
        ''')
        cur.execute('''
            CREATE INDEX IF NOT EXISTS idx_friendships_user2
            ON friendships(user2_id)
        ''')

        # جدول درخواست‌های دوستی در جریان یا پایان چت
        cur.execute('''
            CREATE TABLE IF NOT EXISTS friend_requests (
                chat_id INTEGER NOT NULL,
                sender_id INTEGER NOT NULL,
                receiver_id INTEGER NOT NULL,
                status TEXT NOT NULL DEFAULT 'pending',
                created_at REAL NOT NULL,
                updated_at REAL NOT NULL,
                PRIMARY KEY (chat_id, sender_id, receiver_id)
            )
        ''')
        cur.execute('''
            CREATE INDEX IF NOT EXISTS idx_friend_requests_receiver_status
            ON friend_requests(receiver_id, status)
        ''')

        conn.commit()

# ======================================================================
# --- تعریف کلاس ربات و توابع گیت (باید قبل از تمام @bot هندلرها باشد)
# ======================================================================

def _gate_parts(update):
    """(msg, cb, user, chat, is_cb) یا None اگر آپدیت مشمول گیت نیست"""
    msg = update.message
    cb = update.callback_query
    if msg is not None:
        return msg, None, msg.from_user, msg.chat, False
    if cb is not None:
        return None, cb, cb.from_user, (cb.message.chat if cb.message else None), True
    return None

def _gate_user_id(update):
    parts = _gate_parts(update)
    if parts and parts[2] is not None:
        return parts[2].id
    return 0

def gate_classify(update):
    # طبقه‌بندی سریع آپدیت با استفاده از کش و بدون تماس شبکه.
    parts = _gate_parts(update)
    if parts is None:
        return GATE_ALLOW

    msg, cb, user, chat, is_cb = parts
    if msg is not None and getattr(msg, 'successful_payment', None) is not None:
        return GATE_ALLOW
    if user is None or user.is_bot or user.id in ADMINS:
        return GATE_ALLOW

    uid = user.id
    if is_blocked(uid):
        return GATE_BANNED

    is_private = chat is not None and chat.type == 'private'
    if msg is not None and not is_private:
        return GATE_ALLOW
    if is_cb and cb.data == 'check_membership':
        return GATE_ALLOW
    if msg is not None and msg.text and re.match(r'^/start(@\w+)?(\s|$)', msg.text):
        return GATE_ALLOW

    state = _cached_membership(uid)
    if state is True:
        return GATE_ALLOW
    if state is False:
        return GATE_NEED_JOIN
    if state == GATE_ERROR:
        return GATE_ERROR
    return GATE_UNKNOWN


def gate_resolve_membership(update):
    # بررسی عضویت در thread کمکی؛ خطای API اجازه عبور نمی‌دهد.
    uid = _gate_user_id(update)
    try:
        state = check_channels_membership(uid)
        if state is None:
            return GATE_ERROR
        return GATE_ALLOW if state is True else GATE_NEED_JOIN
    except Exception:
        logger.error('membership check failed; update denied', exc_info=True)
        return GATE_ERROR


def gate_enforce(update, verdict):
    """اثرات جانبی رد شدن آپدیت؛ خطای موقت را از مسدودی/عدم عضویت جدا می‌کند."""
    parts = _gate_parts(update)
    if parts is None:
        return

    msg, cb, user, chat, is_cb = parts
    if user is None:
        return
    uid = user.id
    is_private = chat is not None and chat.type == 'private'
    chat_id = chat.id if chat is not None else uid

    if verdict == GATE_ERROR:
        text = "⚠️ در حال حاضر امکان بررسی دسترسی شما وجود ندارد. لطفاً چند لحظه بعد دوباره تلاش کنید."
        if is_cb:
            try:
                bot.answer_callback_query(cb.id, text, show_alert=True)
            except Exception:
                pass
        elif is_private and _notice_allowed('gate_error', uid, 30):
            try:
                bot.send_message(chat_id, text)
            except Exception:
                pass
        return

    banned = (verdict == GATE_BANNED)

    # در صورت محرومیت یا عدم عضویت، state چت/صف جمع می‌شود.
    try:
        if uid in active_pairs or is_user_waiting(uid):
            force_end_chat(uid, "⚠️ چت به دلیل محدودیت دسترسی پایان یافت.")
    except Exception:
        logger.error("force_end_chat failed in gate_enforce", exc_info=True)

    if is_private:
        try:
            bot.clear_step_handler_by_chat_id(chat_id)
        except Exception:
            pass

    if is_cb:
        alert = "🚫 حساب شما مسدود شده است." if banned else "❌ برای ادامه باید در کانال‌های ربات عضو شوید."
        try:
            bot.answer_callback_query(cb.id, alert, show_alert=True)
        except Exception:
            pass

    if banned:
        if is_private and not is_cb and _notice_allowed('ban', uid, 60):
            try:
                bot.send_message(chat_id, "🚫 متاسفانه حساب شما مسدود شده است.")
            except Exception:
                pass
    elif is_private and _notice_allowed('join', uid, 10):
        try:
            show_channel_join_message(chat_id)
        except Exception:
            logger.error("show_channel_join_message failed", exc_info=True)

class GatedTeleBot(telebot.TeleBot):
    """
    thread دریافت آپدیت‌ها فقط کارهای سبک می‌کند:
      - مسدودی: یک SELECT ساده
      - عضویت: فقط خواندن از کش
    هر چیزی که کند است (تماس شبکه، قفل صف، پیام هشدار) به thread های کمکی سپرده می‌شود.
    """
    def process_new_updates(self, updates):
        ready = []
        for update in updates:
            try:
                verdict = gate_classify(update)
            except Exception:
                logger.error("gate_classify crashed; denying update", exc_info=True)
                verdict = GATE_ERROR

            if verdict == GATE_ALLOW:
                ready.append(update)
            else:
                uid = _gate_user_id(update)
                shard = gate_shards[uid % GATE_SHARDS]
                try:
                    shard.submit(self._gate_slow_path, update, verdict)
                except Exception:
                    logger.error("could not submit gate task; update denied", exc_info=True)
                    try:
                        gate_enforce(update, GATE_ERROR)
                    except Exception:
                        logger.error("gate fallback failed", exc_info=True)

        if ready:
            super(GatedTeleBot, self).process_new_updates(ready)

    def _gate_slow_path(self, update, verdict):
        try:
            if verdict == GATE_UNKNOWN:
                verdict = gate_resolve_membership(update)
        except Exception:
            logger.error("gate_resolve_membership crashed; denying update", exc_info=True)
            verdict = GATE_ERROR

        if verdict == GATE_ALLOW:
            try:
                super(GatedTeleBot, self).process_new_updates([update])
            except Exception:
                logger.error("forwarding gated update failed", exc_info=True)
            return

        try:
            gate_enforce(update, verdict)
        except Exception:
            logger.error("gate_enforce crashed", exc_info=True)

# 🌟 نمونه‌سازی ربات دقیقاً در این نقطه انجام می‌شود تا هندلرهای پایین‌تر آن را بشناسند
bot = GatedTeleBot(TOKEN)

# ======================================================================
# --- بقیه متغیرها و صف‌های انتظار
# ======================================================================

iran_cities = {
    'آذربایجان شرقی': ['تبریز', 'مراغه', 'مرند', 'میانه', 'اهر', 'بناب', 'سایر'],
    'آذربایجان غربی': ['ارومیه', 'خوی', 'میاندوآب', 'مهاباد', 'بوکان', 'سلماس', 'سایر'],
    'اردبیل': ['اردبیل', 'پارس‌آباد', 'مشگین‌شهر', 'خلخال', 'سایر'],
    'اصفهان': ['اصفهان', 'کاشان', 'خمینی‌شهر', 'نجف‌آباد', 'شاهین‌شهر', 'شهرضا', 'سایر'],
    'البرز': ['کرج', 'هشتگرد', 'نظرآباد', 'فردیس', 'سایر'],
    'ایلام': ['ایلام', 'ایوان', 'دهلران', 'مهران', 'سایر'],
    'بوشهر': ['بوشهر', 'برازجان', 'گناوه', 'کنگان', 'سایر'],
    'تهران': ['تهران', 'شهریار', 'اسلامشهر', 'بهارستان', 'ملارد', 'پاکدشت', 'ری', 'ورامین', 'سایر'],
    'چهارمحال و بختیاری': ['شهرکرد', 'بروجن', 'لردگان', 'فارسان', 'سایر'],
    'خراسان جنوبی': ['بیرجند', 'قائن', 'طبس', 'فردوس', 'سایر'],
    'خراسان رضوی': ['مشهد', 'نیشابور', 'سبزوار', 'تربت حیدریه', 'کاشمر', 'قوچان', 'سایر'],
    'خراسان شمالی': ['بجنورد', 'شیروان', 'اسفراین', 'آشخانه', 'سایر'],
    'خوزستان': ['اهواز', 'دزفول', 'آبادان', 'خرمشهر', 'اندیمشک', 'ماهشهر', 'ایذه', 'سایر'],
    'زنجان': ['زنجان', 'ابهر', 'خرمدره', 'قیدار', 'سایر'],
    'سمنان': ['سمنان', 'شاهرود', 'دامغان', 'گرمسار', 'سایر'],
    'سیستان و بلوچستان': ['زاهدان', 'زابل', 'ایرانشهر', 'چابهار', 'سراوان', 'سایر'],
    'فارس': ['شیراز', 'مرودشت', 'کازرون', 'جهرم', 'فسا', 'داراب', 'سایر'],
    'قزوین': ['قزوین', 'الوند', 'تاکستان', 'آبیک', 'سایر'],
    'قم': ['قم', 'قنوات', 'جعفریه', 'سایر'],
    'کردستان': ['سنندج', 'سقز', 'مریوان', 'بانه', 'قروه', 'سایر'],
    'کرمان': ['کرمان', 'سیرجان', 'رفسنجان', 'جیرفت', 'بم', 'سایر'],
    'کرمانشاه': ['کرمانشاه', 'اسلام‌آباد غرب', 'جوانرود', 'کنگاور', 'سایر'],
    'کهگیلویه و بویراحمد': ['یاسوج', 'دوگنبدان', 'دهدشت', 'سایر'],
    'گلستان': ['گرگان', 'گنبد کاووس', 'علی‌آباد کتول', 'بندر ترکمن', 'سایر'],
    'گیلان': ['رشت', 'بندر انزلی', 'لاهیجان', 'لنگرود', 'تالش', 'فومن', 'سایر'],
    'لرستان': ['خرم‌آباد', 'بروجرد', 'دورود', 'کوهدشت', 'الیگودرز', 'سایر'],
    'مازندران': ['ساری', 'بابل', 'آمل', 'قائم‌شهر', 'بهشهر', 'چالوس', 'سایر'],
    'مرکزی': ['اراک', 'ساوه', 'خمین', 'محلات', 'سایر'],
    'هرمزگان': ['بندرعباس', 'میناب', 'قشم', 'کیش', 'بندر لنگه', 'سایر'],
    'همدان': ['همدان', 'ملایر', 'نهاوند', 'تویسرکان', 'اسدآباد', 'سایر'],
    'یزد': ['یزد', 'میبد', 'اردکان', 'بافق', 'مهریز', 'سایر']
}

search_message_ids = {} 
active_pairs = {}
profile_viewed_tracker = set()
user_next_presses = defaultdict(list)
user_timeouts = {}
message_mapping = {}
secure_chats = set()
secure_tracker_lock = threading.RLock()
SECURE_TRACKER_MEMORY_LIMIT = 50
SECURE_MESSAGE_DELETE_WINDOW = 48 * 60 * 60
pending_secure_media = {}
pending_secure_media_lock = threading.Lock() 
active_group_games = {}
queue_lock = threading.RLock()
game_reveal_choices = defaultdict(dict)
game_sessions = set()
advanced_search_filters = {}
pending_broadcasts = {}
user_queue_times = {}
chat_last_activity = {}
chat_start_times = {} 
admin_receipt_messages = defaultdict(dict)
provinces = list(iran_cities.keys())

_secure_tracker_shutdown_lock = threading.Lock()
_secure_tracker_shutdown_chats = set()

class SecureTrackerPersistenceError(RuntimeError):
    """ثبت پایدار شناسه پیام امن در SQLite با شکست مواجه شده است."""


def _schedule_secure_tracker_shutdown(user_id, chat_id):
    """چت امن خراب را در Thread جدا می‌بندد تا قفل‌های ارسال/پایان چت deadlock نکنند."""
    try:
        user_id = int(user_id)
        chat_id = int(chat_id)
    except (TypeError, ValueError):
        logger.critical(
            'invalid secure tracker shutdown identifiers user=%r chat=%r',
            user_id, chat_id
        )
        return

    with _secure_tracker_shutdown_lock:
        if chat_id in _secure_tracker_shutdown_chats:
            return
        _secure_tracker_shutdown_chats.add(chat_id)

    def _shutdown_worker():
        try:
            with queue_lock:
                pair = active_pairs.get(user_id)
            if pair and int(pair[1]) == chat_id:
                force_end_chat(
                    user_id,
                    '⚠️ چت امن به دلیل خطای ذخیره‌سازی پیام‌ها برای حفظ حریم خصوصی پایان یافت.'
                )
            else:
                # اگر چت همین الان توسط مسیر دیگری بسته شده، فقط صف حذف را دوباره علامت بزن؛
                # cleanup و ارسال Feedback را دوباره اجرا نکن.
                with secure_tracker_lock:
                    secure_chats.discard(chat_id)
                    with get_db_connection() as conn:
                        cur = conn.cursor()
                        cur.execute(
                            '''UPDATE secure_chat_messages
                               SET delete_requested = 1, retry_at = 0
                               WHERE chat_id = ?''',
                            (chat_id,)
                        )
                        conn.commit()
                _process_secure_chat_message_deletions(chat_id=chat_id)
        except Exception:
            logger.critical(
                'secure tracker fail-safe shutdown failed user=%s chat=%s',
                user_id, chat_id,
                exc_info=True
            )
        finally:
            with _secure_tracker_shutdown_lock:
                _secure_tracker_shutdown_chats.discard(chat_id)

    try:
        threading.Thread(
            target=_shutdown_worker,
            name=f'secure-tracker-stop-{chat_id}',
            daemon=True
        ).start()
    except Exception:
        with _secure_tracker_shutdown_lock:
            _secure_tracker_shutdown_chats.discard(chat_id)
        logger.critical(
            'could not start secure tracker fail-safe thread user=%s chat=%s',
            user_id, chat_id,
            exc_info=True
        )
        # آخرین تلاش هم‌زمان؛ در صورت شکست Thread، از بازماندن چت امن جلوگیری کن.
        try:
            force_end_chat(
                user_id,
                '⚠️ چت امن به دلیل خطای ذخیره‌سازی پیام‌ها برای حفظ حریم خصوصی پایان یافت.'
            )
        except Exception:
            logger.critical('synchronous secure tracker shutdown failed', exc_info=True)

class _PersistentSecureMessageList(list):
    """در RAM فقط ۵۰ شناسه آخر را نگه می‌دارد؛ همه شناسه‌ها را در SQLite ثبت می‌کند."""

    def __init__(self, chat_id, user_id):
        super().__init__()
        self.chat_id = int(chat_id)
        self.user_id = int(user_id)

    def append(self, message_id):
        if message_id is None:
            return

        try:
            message_id = int(message_id)
        except (TypeError, ValueError):
            logger.warning(
                "invalid secure message id chat=%s user=%s value=%r",
                self.chat_id, self.user_id, message_id
            )
            return

        # حلقه حافظه: فقط آخرین ۵۰ شناسه در RAM باقی بماند (برای fallback در صورت خطای SQLite).
        with secure_tracker_lock:
            super().append(message_id)
            overflow = len(self) - SECURE_TRACKER_MEMORY_LIMIT
            if overflow > 0:
                del self[:overflow]

        # ثبت پایدار در SQLite؛ در صورت شکست، حذف فوری و پایان Fail-Safe چت امن زمان‌بندی می‌شود.
        try:
            with secure_tracker_lock:
                # اگر چت در حال پایان/پایان‌یافته باشد، رکورد مستقیماً وارد صف حذف شود.
                delete_requested = 0 if self.chat_id in secure_chats else 1

                with get_db_connection() as conn:
                    cur = conn.cursor()
                    cur.execute(
                        '''INSERT OR IGNORE INTO secure_chat_messages
                           (chat_id, user_id, message_id, created_at, delete_requested,
                            retry_at, attempts)
                           VALUES (?, ?, ?, ?, ?, 0, 0)''',
                        (
                            self.chat_id,
                            self.user_id,
                            message_id,
                            time.time(),
                            delete_requested
                        )
                    )
                    conn.commit()
        except Exception as exc:
            logger.critical(
                "secure message tracker DB insert failed chat=%s user=%s message=%s",
                self.chat_id, self.user_id, message_id,
                exc_info=True
            )
            try:
                bot.delete_message(self.user_id, message_id)
            except Exception:
                logger.warning(
                    "immediate fail-safe delete failed chat=%s user=%s message=%s",
                    self.chat_id, self.user_id, message_id,
                    exc_info=True
                )
            _schedule_secure_tracker_shutdown(self.user_id, self.chat_id)
            raise SecureTrackerPersistenceError(
                f"secure message persistence failed for chat={self.chat_id} user={self.user_id} message={message_id}"
            ) from exc

    def extend(self, message_ids):
        for message_id in message_ids:
            self.append(message_id)

class _SecureChatUserBucket(dict):
    def __init__(self, chat_id):
        super().__init__()
        self.chat_id = int(chat_id)

    def __missing__(self, user_id):
        value = _PersistentSecureMessageList(self.chat_id, int(user_id))
        self[user_id] = value
        return value


class _PersistentSecureChatTracker(dict):
    def __missing__(self, chat_id):
        value = _SecureChatUserBucket(int(chat_id))
        self[chat_id] = value
        return value

chat_msg_tracker = _PersistentSecureChatTracker()

# ==========================================
# 🌟 متغیرهای امنیتی، قفل‌های Thread-Safe و کانفیگ‌ها
# ==========================================
LAST_SEEN_WRITE_TTL = 60
CHAT_REQUEST_TTL_SECONDS = 600
CHAT_REQUEST_COST = 5
MAX_GROUP_PLAYERS = 40
ANON_MESSAGE_LIMIT = 40
ANON_MESSAGE_WINDOW = 10

search_reservations = {}
_last_seen_write = {}
_anon_message_times = defaultdict(deque)

# قفل‌های اختصاصی برای جلوگیری از Race Condition در دیکشنری‌های اشتراکی
last_seen_lock = threading.Lock()
rate_limit_lock = threading.Lock()
spam_lock = threading.Lock()
mapping_lock = threading.RLock()

PACKAGES_RIAL = {
    350: "269,000",
    580: "448,000",
    1700: "579,000",
    3000: "799,000",
    7000: "1,339,000"
}

PACKAGES_STARS = {
    350: 70,
    580: 115,
    1700: 150,
    3000: 210,
    7000: 340
}

GENDER_MAP = {
    'male': ('مرد ♂', 'M'),
    'female': ('زن ♀', 'F'),
    'transmale': ('ترنس مرد 🏳️‍⚧️', 'M'),
    'transfemale': ('ترنس زن 🏳‍⚧️', 'F'),
    'shemale': ('شیمیل ⚧', 'O'),
    'femboy': ('فمبوی (Femboy) 🎀', 'O'),  
    'tomboy': ('تامبوی (Tomboy) 🧢', 'O'),  
    'nonbinary': ('غیرباینری (Non-binary) ⚧', 'O'),
    'genderfluid': ('جندرفلوید (Fluid) 🌊', 'O'),
    'agender': ('ای‌جندر (Agender) ⚪', 'O'),
    'bigender': ('بایجندر (Bigender) ☯️', 'O'),
    'bisexual': ('بایسکشوال (Bisexual) 💖', 'O'),
    'pansexual': ('پنسکشوال (Pansexual) 💛', 'O'),
    'lesbian': ('لزبین (Lesbian) 🧡', 'O'),
    'gay': ('گی (Gay) 🏳️‍🌈', 'O'),
    'asexual': ('ای‌سکشوال (Asexual) 🖤', 'O'),
    'other': ('سایر / ترجیح می‌دهم نگویم 👤', 'O')
}

POS_MAP = {
    'top': 'تاپ 🔝',
    'vers': 'ورس ↔️',
    'bottom': 'بات 🔽',
    'unknown': 'ترجیح می‌دهم نگویم 👤'
}

ICEBREAKER_NORMAL = [
    "اگر می‌توانستی بدون پیامد یک قانون در دنیا وضع یا حذف کنی، چه بود؟",
    "عجیب‌ترین ترسی که داری و بقیه مسخره‌اش می‌کنند چیه؟",
    "اگر زندگی‌ات فیلم بود، در چه ژانری ساخته می‌شد و اسمش چی بود؟",
    "بدترین یا خنده‌دارترین سوتی که جلوی جمع دادی چی بوده؟",
    "یک حقیقت تلخ که خیلی دیر در زندگی فهمیدی؟",
    "حاضری ۱۰ سال کمتر زندگی کنی ولی ذهن همه رو بخونی؟",
    "بزرگ‌ترین «خط قرمز» اخلاقی‌ات توی رابطه چیه که اگر شکسته بشه فوری کات می‌کنی؟",
    "اگر می‌توانستی همین الان جای یک سلبریتی باشی، کی رو انتخاب می‌کردی؟",
    "چه عادتی داری که خودت هم می‌دونی رو اعصابه ولی ترکش نمی‌کنی؟",
    "عجیب‌ترین خریدی که تا حالا کردی و پشیمون شدی چی بود؟",
    "بیشتر با منطقت تصمیم می‌گیری یا با احساست به دیوار می‌خوری؟",
    "اگر فقط اجازه داشتی تا آخر عمر به ۳ تا آهنگ گوش بدی، کدوما بودن؟",
    "اولین چیزی که در برخورد اول با یک غریبه نظرت رو جلب می‌کنه چیه؟",
    "بزرگ‌ترین توهمی که در دوران کودکی داشتی چی بود؟",
    "ترجیح می‌دی به گذشته سفر کنی و یک اشتباه رو جبران کنی یا آینده‌ت رو ببینی؟",
    "چه رفتاری از سمت یک آدم باعث می‌شه درجا از چشم بیفته؟",
    "تا حالا شده از روی لجبازی تصمیمی بگیری که کلاً مسیرت رو عوض کنه؟",
    "اگر می‌تونستی یک استعداد هنری یا فنی رو توی یک ثانیه یاد بگیری، چی بود؟",
    "وقتی خیلی حالت بده، ترجیح می‌دی با یکی حرف بزنی یا توی تنهایی غرق بشی؟",
    "اگر قرار بود تا آخر عمرت فقط یک غذا بخوری، انتخابت چی بود؟",
    "شجاعانه‌ترین کاری که تا حالا کردی و قلبت اومده بود توی دهنت چی بود؟",
    "فکر می‌کنی بعد از مرگ قراره چه اتفاقی بیفته؟",
    "به نظرت آدما واقعاً عوض می‌شن یا فقط نقاب‌های جدید می‌زنن؟",
    "اگر بهت ۵ میلیارد تومن بدن به شرطی که ۱ سال گوشیت رو خاموش کنی، قبول می‌کنی؟",
    "خنده‌دارترین نظریه توطئه‌ای که شنیدی و حس کردی ممکنه راست باشه چیه؟",
    "قشنگ‌ترین دروغی که تا حالا به کسی گفتی چی بوده؟",
    "در حال حاضر از چی بیشتر از همه توی زندگی خسته‌ای؟",
    "فکر می‌کنی از دید بقیه آدم مغروری به نظر میای یا خونگرم؟",
    "اگر می‌تونستی ذهن نفر روبه‌روت رو بخونی، دوست داشتی اول چی بدونی؟",
    "بدترین قراری (Date) که تا حالا رفتی چطوری گذشت؟",
    "بیشتر از طرد شدن می‌ترسی یا از اینکه فراموش بشی؟",
    "نظرت درباره عشق در نگاه اول چیه؛ توهم یا واقعیت؟",
    "اگر می‌توانستی نامرئی بشی، اولین جایی که سرک می‌کشیدی کجا بود؟",
    "تا حالا شده به یک ناشناس رازی رو بگی که صمیمی‌ترین دوستت هم نمی‌دونه؟",
    "چه کتاب، فیلم یا بازی بیشترین تأثیر فکری رو روت گذاشته؟",
    "ترجیح می‌دی با کسی باشی که خیلی باهوش‌تر از توئه یا خیلی مهربون‌تر؟",
    "اگر می‌توانستی زمان رو متوقف کنی، چه استفاده‌ای ازش می‌کردی؟",
    "اگر بفهمی امروز آخرین روز دنیاست، آخرین پیامت رو به کی می‌دی؟",
    "بچگی‌هات فکر می‌کردی در این سن الان کجای زندگی باشی؟",
    "چه رفتاری در شبکه‌های اجتماعی برای تو نشانه «رد فلگ» (Red Flag) حساب می‌شه؟",
    "حاضری هیچ‌وقت نتونی دروغ بگی یا اینکه هیچ‌وقت نفهمی کی داره بهت دروغ می‌گه؟",
    "عجیب‌ترین آدمی که تا حالا توی زندگیت از نزدیک دیدی چه شکلی بود؟",
    "اهل ریسک کردنی یا محتاط قدم برمی‌داری؟",
    "چه صفت یا رفتاری از خودت هست که دوست داری بچه‌ت به ارث نبره؟",
    "اگر قرار باشه هویتت رو کاملاً عوض کنی و بری یک کشور دیگه، چه اسمی روی خودت می‌ذاری؟",
    "تا حالا شده از خوشحالی یا موفقیت کسی حسادت شدید کنی؟",
    "قشنگ‌ترین تعریفی که کسی تا حالا ازت کرده چی بوده؟",
    "نظرت درباره دوستی دختر و پسر بدون هیچ حسی چیه؛ شدنیه؟",
    "ترجیح می‌دی همه ترسی که ازت دارن از روی احترام باشه یا علاقه خالص؟",
    "فکر می‌کنی بزرگ‌ترین ضعفت چیه که اگر کسی بفهمه راحت می‌تونه بهت ضربه بزنه؟",
    "تاریک‌ترین جنبه شخصیتیت که سعی می‌کنی جلوی بقیه پنهانش کنی چیه؟",
    "در چه موقعیت‌هایی حس می‌کنی داری کاملاً نقش بازی می‌کنی و خود واقعیت نیستی؟",
    "اگر قرار بود صمیمی‌ترین دوستت یک نقد بی‌رحمانه بهت بکنه، احتمالاً چی می‌گفت؟",
    "از چه ویژگی رفتاری خودت به شدت خسته‌ای ولی راه حلی براش پیدا نکردی؟",
    "آخرین باری که از ته دل و بدون خجالت جلوی کسی گریه کردی کی بود؟",
    "تا حالا شده از بدبختی یا شکست یک آدم نزدیک، ته دلت یک حس رضایت مخفی پیدا کنی؟",
    "بزرگ‌ترین ناامنی ذهنی (Insecurity) که نسبت به بدنت یا رفتارت داری چیه؟",
    "فکر می‌کنی چه تصوری از تو توی ذهن بقیه کاملاً غلطه؟",
    "اگر قرار باشه فقط ۳ کلمه برای توصیف هسته درونی خودت بگی، اون کلمات چیه؟",
    "تا حالا احساس کردی هیچ‌کس در دنیا واقعاً تو رو اون‌طور که هستی نمی‌شناسه؟",
    "بیشتر تمایل داری کنترل همه‌چیز دست خودت باشه یا دوست داری تسلیم شرایط بشی؟",
    "نظرت درباره مفهوم «شانس» چیه؛ واقعاً وجود داره یا توجیه آدم‌ها برای بی‌عرضگیه؟",
    "چه کاری رو صرفاً برای تایید گرفتن از جامعه یا خانواده انجام می‌دی در حالی که خودت ازش متنفری؟",
    "وقتی عصبانی می‌شی سبک واکنشت چیه؛ سکوت سنگین یا فوران کلمات؟",
    "بزرگ‌ترین ایثاری که برای کسی کردی و بعدش حس حماقت بهت دست داد چی بود؟",
    "چقدر بلدی نه بگی بدون اینکه تا سه روز بعدش عذاب وجدان داشته باشی؟",
    "ترجیح می‌دی با واقعیت تلخ بمیرانندت یا با دروغ شیرین آرومت کنن؟",
    "به نظرت وقتی رابطه تمام می‌شه، دو طرف می‌تونن دوست صمیمی بمونن یا این فقط به تعویق انداختن فاجعه‌ست؟",
    "تا حالا شده با کسی قطع ارتباط کنی فقط به این دلیل که انرژی منفیش زیاد بوده؟",
    "نشانه‌ای که در اولین پیام‌های یک نفر باعث می‌شه حس کنی باهاش مکالمه خوبی نخواهی داشت چیه؟",
    "نظرت درباره خیانت احساسی چیه؛ به اندازه خیانت فیزیکی سنگین و نابخشودنیه؟",
    "تا حالا شده به رابطه دو نفر از اطرافیانت اون‌قدر حسودی کنی که آرزو کنی تموم بشه؟",
    "حاضر بودی با کسی ازدواج کنی که عاشقشی ولی خانواده‌هاتون در جنگ ابدی باشن؟",
    "اگر بفهمی بهترین دوستت پشت سرت حرف زده، مستقیم باهاش حرف می‌زنی یا بی‌صدا حذفش می‌کنی؟",
    "به نظرت فاصله سنی ایده‌آل توی رابطه چقدره و اختلاف زیاد چه پیامدی داره؟",
    "چه رفتاری در قرار اول باعث می‌شه سریع بهانه بیاری و فرار کنی؟",
    "آخرین پیام تلخی که به یک نفر دادی و بعدش بلاکش کردی چی بود؟",
    "آیا تا حالا وانمود کردی کسی رو دوست داری در حالی که حس خاصی نداشتی؟",
    "ارزش پول در روابط برات چقدر تعیین‌کننده‌ست؛ شعار یا واقعیت؟",
    "تا حالا شده دلت برای آدمی تنگ بشه که ضربه روحی بدی بهت زده بود؟",
    "اگر پارتنرت بدون اجازه‌ات چت‌هات رو بخونه، این یعنی مراقبت یا نقض کامل حریم؟",
    "ترجیح می‌دی طرفت خیلی وابسته باشه یا کاملاً مستقل و بی‌نیاز از تو؟",
    "چه رفتاری از سمت جنس مخالف بلافاصله احترامت رو نسبت بهش از بین می‌بره؟",
    "اگر بدونی نجات دادن جون ۵ غریبه به قیمت لو رفتن تمام رازهای زندگیت تموم می‌شه، دکمه رو می‌زنی؟",
    "ترجیح می‌دی ۱۰۰ سال با هوش معمولی زندگی کنی یا ۴۰ سال در اوج نبوغ؟",
    "اگر مجبور باشی بین نداشتن اینترنت تا آخر عمر یا دیدن نکردن هیچ آدمی به مدت ۲ سال یکی رو انتخاب کنی، کدوماست؟",
    "اگر می‌تونستی یک مکالمه ۱۰ دقیقه‌ای با خودِ ۵ سال پیشت داشته باشی، چه هشداری بهش می‌دادی؟",
    "حاضری برای همیشه فراموش بشی ولی دنیا بعد تو گلستان باشه، یا اسمت در تاریخ ثبت بشه به عنوان یک آدم معمولی؟",
    "اگر یک قرص بهت بدن که تمام خاطرات بد زندگیت رو پاک کنه، می‌خوریش؟",
    "حاضری بدونی کی می‌میری یا ترجیح می‌دی در بی‌خبری مطلق بمونی؟",
    "اگر بفهمی کل زندگیت تا این لحظه یک برنامه شبیه‌سازی بوده، کارت چیه؟",
    "ترجیح می‌دی توانایی سفر در زمان داشته باشی یا سفر به هر نقطه‌ای از فضا در یک ثانیه؟",
    "اگر بهت بگن می‌تونی یک نفر رو از صفحه روزگار محو کنی بدون اینکه کسی بفهمه، چه کسی کاندیده؟",
    "حاضری فقیر باشی ولی با عشق واقعیت زندگی کنی یا میلیاردر تنها؟",
    "ترجیح می‌دی به کسی خیانت کنی یا بهت خیانت بشه؟",
    "اگر بتونی ذهن حیوانات رو بخونی ولی دیگه با هیچ انسانی نتونی حرف بزنی، قبول می‌کنی؟",
    "تا حالا سر دوراهی بین کمک به دوستت و موفقیت کاری خودت موندی؟ چه تصمیمی گرفتی؟",
    "احمقانه‌ترین کاری که کردی فقط برای اینکه جلوی یک نفر خفن به نظر برسی چی بوده؟",
    "اگر گوشی تلفنت رو الان بردارم و صفحه اکسپلور یا سرچ‌هات رو ببینم، چقدر آبروت می‌ره؟",
    "عجیب‌ترین ترکیبی که برای غذا خوردی و برات خوشمزه بوده چیه؟",
    "تا حالا شده لباس کثیف از توی سبد رخت‌چرک‌ها برداری، بو کنی و بپوشی بری بیرون؟",
    "خنده‌دارترین تصادف یا زمین خوردنی که جلوی ملت برات اتفاق افتاد چطور بود؟",
    "اگر مجبور بشی به یک ژانر موزیک تا ابد گوش بدی، کدوم رو انتخاب می‌کنی؟",
    "بدترین خریدی که تو جوگیری تخفیف‌های آنلاین کردی چی بود؟",
    "تا حالا شده خودت رو به مریضی بزنی تا از یک قرار یا مهمونی فرار کنی؟",
    "مسخره‌ترین دروغی که توی رزومه یا مصاحبه کاری گفتی چی بوده؟",
    "ترجیح می‌دی همیشه پاهات خیس باشه یا همیشه احساس کنی پشت گردنت خارش داره؟",
    "چند بار در روز گوشیت رو بی‌دلیل باز و بسته می‌کنی فقط از سر بی‌حوصلگی؟",
    "اگر یک مستند از روتین آخر هفته‌هات بسازن، ریتمش هیجان‌انگیزه یا افسرده‌کننده؟",
    "بدترین سوتی که توی یک تماس تلفنی یا ویس فرستادن دادی چی بود؟",
    "تا حالا شده پشت سر کسی حرف بزنی و متوجه بشی دقیقاً پشت سرت وایساده بوده؟",
    "عجیب‌ترین خرافاتی که با اینکه می‌دونی چرنده ولی ناخودآگاه رعایتش می‌کنی چیه؟",
    "اگر جهان فقط یک پیام مشترک نیاز داشت تا از جنگ خلاص بشه، چی بود؟",
    "به تناسخ اعتقاد داری؟ اگر آره، فکر می‌کنی در زندگی قبلیت کی یا چی بودی؟",
    "کدوم کاراکتر تخیلی (فیلم، سریال، انیمه) رو شبیه‌ترین نسخه به خودت می‌دونی؟",
    "اگر یک روز از خواب بیدار بشی و ببینی جنسیتت عوض شده، اولین کاری که می‌کنی چیه؟",
    "نظرت درباره عدالت چیه؛ یک توهم بشری یا یک حقیقت کیهانی؟",
    "ترجیح می‌دی در یک جامعه پیشرفته بدون آزادی زندگی کنی یا یک جامعه بدوی با آزادی مطلق؟",
    "اگر می‌تونستی حقیقت یک معمای بزرگ تاریخی رو بفهمی، کدوم رو انتخاب می‌کردی؟",
    "زیباترین منظره‌ای که با چشم‌های خودت دیدی و عکسش هرگز حق مطلب رو ادا نکرد چی بود؟",
    "به نظرت تکنولوژی داره انسان‌ها رو تنهاتر می‌کنه یا داره به هم نزدیک‌ترشون می‌کنه؟",
    "آخرین باری که یک اثر هنری (فیلم، کتاب، نقاشی) تکونت داد کی بود؟",
    "اگر قرار باشه فقط یک یادگاری از خودت روی کره زمین به جا بذاری، اون چیه؟",
    "الان که پشت این چت نشستی، حس غالب درونت چیه: بی‌حوصلگی، کنجکاوی، تنهایی یا هیجان؟",
    "اگر الان بهت بگم تا ۵ دقیقه دیگه باید یک چمدون ببندی و برای همیشه بری، چه چیزهایی برمی‌داری؟",
    "فکر می‌کنی لحن پیام دادن من در حال حاضر چطور به نظر میاد؛ دوستانه، سرسخت یا عجیب؟",
    "تا حالا شده از روی عکس پروفایل یا بیوگرافی کسی به صورت قاطع درباره‌اش قضاوت کنی؟",
    "اگر بخوای یک اعتراف کوچک به من بکنی که به غریبه‌ها راحت‌تر می‌شه گفت، چی می‌گی؟",
    "شبیه‌ترین میوه یا سبزیجات به حال و هوای امروزت چیه؟",
    "فکر می‌کنی بعد از پایان این چت، چیزی از این مکالمه توی ذهنت می‌مونه؟",
    "خسته‌کننده‌ترین سوالی که ملت معمولاً توی چت ناشناس می‌پرسن از نظرت چیه؟",
    "اگر الان درِ اتاقت باز بشه و بتونی یک شخصیت معروف رو روبه‌روت ببینی، کیه؟",
    "سرعت پاسخ دادن یک نفر به پیام‌هات چقدر روی میزان ارزشی که براش قائلی تاثیر داره؟",
    "اگر مجبور بودی الان برای من یک نصیحت دوستانه بنویسی، چی می‌نوشتی؟",
    "آیا اهل بازی روانی و سیاست‌های رفتاری توی صحبت هستی یا کاملاً رک و ساده‌ای؟",
    "بیشترین هزینه‌ای که برای اشتباهات خودت دادی چی بوده؛ زمان، پول یا روان؟",
    "اگر بهت یک میکروفون بدن که صدات برای ۸ میلیارد آدم پخش بشه، در ۳۰ ثانیه چی می‌گی؟",
    "به نظرت حسادت یک نشانه از علاقه‌ست یا نشانه عدم اعتمادبه‌نفس؟",
    "حاضری با کسی باشی که خیلی خوب دروغ می‌گه یا کسی که بدون ملاحظه با حقیقت تحقیرت می‌کنه؟",
    "تا حالا شده از دست خودت اون‌قدر عصبانی بشی که به در و دیوار بکوبی؟",
    "اگر یک نقاشی از کل مسیر زندگیت تا امروز بکشن، چه رنگی توش بیشتر غالبه؟",
    "آخرین باری که احساس کردی برای یک نفر اضافه هستی کی بود؟",
    "به نظرت بخشیدن دیگران سخت‌تره یا بخشیدن اشتباهات خودت؟",
    "اگر فقط حق داشتی یک اپلیکیشن رو روی گوشیت نگه داری، کدوم می‌موند؟",
    "نظرت درباره ازدواج سنتی در مقابل دوستی‌های طولانی‌مدت چیه؟",
    "تا حالا شده به یک مکالمه بعد از ۳ سال فکر کنی و حرص بخوری که چرا فلان جواب رو ندادی؟",
    "فکر می‌کنی ۱۰ سال دیگه از موقعیت الانت راضی خواهی بود یا حسرت می‌خوری؟",
    "آیا می‌تونی تنهایی بری کافه یا سینما و واقعاً ازش لذت ببری؟",
    "خفن‌ترین استعداد غیرکاربردی که داری چیه؟",
    "اگر همین الان قرار باشه بدون هیچ حرفی چت رو ببندی، دلیلت چی می‌تونه باشه؟"
]

ICEBREAKER_18PLUS = [
    "جذاب‌ترین ویژگی ظاهری جنس مخالف از نظر تو چیه که بلافاصله می‌گیرتت؟",
    "تا حالا شده نسبت به کسی فانتزی داشته باشی که اصلاً نباید بهش فکر می‌کردی؟",
    "ترجیح می‌دی در رابطه عاطفی فرد کنترل‌کننده (Dominant) باشی یا مطیع (Submissive)؟",
    "عجیب‌ترین یا پرخطرترین مکانی که تا حالا به شیطنت یا رابطه فکر کردی کجا بوده؟",
    "بوی عطر یا فرم لباس پوشیدن چقدر توی برانگیختگی حست تأثیر داره؟",
    "تا حالا شده فقط به خاطر ظاهر یا جذابیت فیزیکی یک نفر باهاش وارد ارتباط بشی؟",
    "اولین بار که کشش فیزیکی شدید به یک نفر حس کردی چند سالت بود؟",
    "رابطه برات بیشتر ذهنی شروع می‌شه یا اولویت اولت جاذبه بدنیه؟",
    "چه حرکت ناگهانی از پارتنرت می‌تونه درجا قفلت کنه؟",
    "نظرت درباره چت متنی هیجانی (Sexting) چیه؛ اهلشی یا سردت می‌کنه؟",
    "ترجیح می‌دی یک رابطه خیلی داغ و کوتاه داشته باشی یا آروم و طولانی؟",
    "نظرت درباره ارسال یا دریافت عکس‌های شخصی و خصوصی (Nudes) چیه؟",
    "تا حالا شده روی کراش دوستت یا اکسِ یک آشنا حس متفاوتی پیدا کنی؟",
    "چه رفتاری در تخت‌خواب یا شیطنت‌های دونفره برات بزرگ‌ترین «ترن‌آف» (Turn-off) به حساب میاد؟",
    "حاضر بودی با کسی که از لحاظ اخلاقی باهاش سازگار نیستی ولی جذابیت جنسی فوق‌العاده داره بمونی؟",
    "بیشتر جذب صدای طرف مقابل می‌شی، فرم لب‌هاش، یا نگاهش؟",
    "تا حالا با کسی فقط برای اینکه تنها نباشی وقت گذروندی؟",
    "تا چه حد اهل فانتزی‌های غیرمعمول و Role-play هستی؟",
    "بدترین یا معذب‌کننده‌ترین تجربه‌ای که حین خلوت دونفره برات پیش اومده چی بوده؟",
    "ترجیح می‌دی رابطه پر از هیجان و شیطنتِ پیش‌بینی‌نشده باشه یا ملایم و رمانتیک؟",
    "نظرت درباره روابط بدون تعهد و صرفاً حسی-فیزیکی (Friends with Benefits) چیه؟",
    "تا حالا مچ کسی رو موقع کارای خصوصی گرفتی یا خودت لو رفتی؟",
    "چه مدل لباسی روی پارتنرت می‌تونه بیشترین کشش رو برات ایجاد کنه؟",
    "تا حالا شده حین رابطه به یک نفر دیگه فکر کنی؟",
    "خط قرمز فانتزی‌هات کجاست که حس می‌کنی حاضر نیستی هرگز امتحانش کنی؟",
    "به نظرت امکان داره رابطه زناشویی بدون صمیمیت جنسی دوام بیاره؟",
    "ترجیح می‌دی طرفت خیلی خجالتی و معصوم باشه یا فوق‌العاده بی‌پروا و جسور؟",
    "بوسه اولت چطور بود؛ افتضاح یا مثل فیلما؟",
    "تا حالا از روی کنجکاوی فیلم‌هایی دیدی که خودت هم بعدش تعجب کنی؟",
    "جذاب‌ترین قسمت بدن جنس مخالف برای تو کجاست؟",
    "اهل شیطنت‌های فیزیکی توی جاهای نیمه‌عمومی (مثل سینما، ماشین یا کوچه خلوت) هستی؟",
    "وقتی از یکی خوشت میاد چطور با بدنت بهش سیگنال می‌دی؟",
    "تا حالا شده احساس کنی یکی با نگاهش داره لباسات رو از تنت درمیاره؟ واکنشت چی بود؟",
    "در یک رابطه، لمس فیزیکی چقدر برات در رتبه اول اهمیت قرار داره؟",
    "فکر می‌کنی توی چت جذاب‌تری یا در ملاقات حضوری و از نزدیک؟",
    "تا حالا خواب شفاف یا عمیق دونفره دیدی که صبحش دلت نخواد بیدار شی؟",
    "اگر پارتنرت پیشنهاد بازی جرئت یا حقیقت خیلی بی‌پروا بده، پایه‌ای؟",
    "آیا رفتارهای تهاجمی ملایم (مثل گاز گرفتن یا کشیدن مو) رو دوست داری یا ترجیحت نوازش مطلقه؟",
    "سریع‌ترین زمانی که بعد از آشنایی با یکی وارد فاز شیطنت شدی چقدر بوده؟",
    "فکر می‌کنی از نظر پارتنر سابقت آدم هاتی بودی یا خسته‌کننده؟",
    "چه کلماتی حین صحبت دونفره می‌تونه بیشترین ضربان قلب رو برات بیاره؟",
    "تا حالا شده کسی با یک زمزمه در گوشَت بتونه تمام کنترلت رو به هم بریزه؟",
    "نظرت درباره ماساژهای طولانی دونفره چیه؛ اهلشی؟",
    "چقدر اهل شوخی‌های دوپهلو و کنایه‌های با معنای دوم هستی؟",
    "اگر فقط یک شب با کراش رویاییت باشی و فردا هیچ‌کس یادش نیاد، چطور می‌گذرونیش؟",
    "تا حالا حسودی کردی به اینکه اکسِ کسی که دوستش داری چه تجربه‌هایی باهاش داشته؟",
    "تا چه اندازه حاضری توی رابطه فانتزی‌های طرفت رو برآورده کنی، حتی اگه سلیقه خودت نباشه؟",
    "تاریکی مطلق رو در خلوت دونفره ترجیح می‌دی یا نور ملایم و شمع؟",
    "بی‌شرمانه‌ترین درخواستی که تا حالا کسی توی چت ازت کرده چی بوده؟",
    "اگر طرف مقابلت همین الان بگه «جرئت داری هرچی بگم انجام بدی؟» جوابت چیه؟",
    "اولین برخورد نگاهت با جنس مخالف روی کدوم اندام یا ویژگی فیزیکی قفل می‌شه؟",
    "صدای آروم و خش‌دار برات تحریک‌کننده‌تره یا لحن جسور و بااعتمادبه‌نفس؟",
    "بوی عطر گرم و تلخ بیشتر روت تاثیر می‌ذاره یا خنک و شیرین؟",
    "چه مدل استایلی روی پارتنرت باعث می‌شه نتونی چشم ازش برداری؟",
    "لمس اتفاقی دست یا کمر از طرف کسی که بهش حس داری چه حسی در بدنت ایجاد می‌کنه؟",
    "تماس چشمی طولانی و بدون کلام بیشتر معذبت می‌کنه یا برات تحریک‌کننده و بازی‌گونه‌ست؟",
    "لبخند شیطنت‌آمیز یا گاز گرفتن گوشه لب؛ کدوم زودتر تسلیمت می‌کنه؟",
    "وقتی از کسی خوشت میاد، ناخودآگاه چطور با بدنت بهش نزدیک می‌شی؟",
    "تا حالا شده خط فک یا فرم رگ‌های دست یک نفر کشش ناگهانی شدیدی برات ایجاد کنه؟",
    "نظرت درباره خیس بودن موها بعد از حمام در پارتنرت چیه؛ جذابیتی برات داره؟",
    "چه مدل کفش یا لباس پوشیدنی در جنس مخالف برات دافعه کامل حسی (Turn-off) داره؟",
    "تماس فیزیکی زیر میز یا در جمع مخفیانه چقدر سطح آدرنالینت رو بالا می‌بره؟",
    "آیا لحن صدای پارتنرت در مکالمه تلفنی شبانه می‌تونه حالت فیزیکیت رو تغییر بده؟",
    "بوی طبیعی پوست طرف مقابل چقدر توی برقراری ارتباط نزدیک برات حیاتیه؟",
    "ترجیح می‌دی موهای بلند و پرپشت داشته باشه یا مدل‌های خاص و کوتاه؟",
    "عجیب‌ترین یا بی‌شرمانه‌ترین سناریوی ذهنی که تنهایی بهش فکر کردی چی بوده؟",
    "تا حالا به سناریوی گیر افتادن در آسانسور با یک آدم به شدت جذاب فکر کردی؟",
    "چقدر به ایده‌های نقش‌آفرینی (Role-play) مثل غریبه‌ها در یک بار علاقه داری؟",
    "ترجیح می‌دی فضایی با نور کم، شمع و موسیقی آروم باشه یا یک برخورد ناگهانی بدون هیچ برنامه‌ریزی؟",
    "آیا تا حالا فانتزی داشتن رابطه در سفر و با یک مسافر ناشناس رو داشتی؟",
    "بستن چشم‌ها با چشم‌بند و محروم شدن از حس بینایی برات ترسناکه یا به شدت جذاب؟",
    "استفاده از لباس‌های خاص با طراحی تحریک‌کننده چقدر در برانگیختگی ذهنت نقش داره؟",
    "تا حالا به انجام کارهای ممنوعه با کسی که نباید (مثل کراش ممنوعه) فکر کردی؟",
    "کثیف حرف زدن (Dirty Talk) در حین شیطنت؛ باعث اشتیاق بیشترت می‌شه یا حست رو می‌پرونه؟",
    "نظرت درباره استفاده از یخ یا پر برای تماس با پوست چیه؟",
    "آیا بازی‌های کارتی یا تاس‌های شیطنت دونفره رو جذاب می‌دونی یا چیپ و لوس؟",
    "تمایل داری صحنه رابطه پر از کلمات عاشقانه باشه یا جملات خشن و آمرانه؟",
    "تا چه اندازه حاضری برای تجربه یک فانتزی جدید ریسک کنی؟",
    "ایده‌آل‌ترین مکانی که توی ذهنت برای یک خلوت طولانی دو نفره داری کجاست؟",
    "اگر قرار باشه پارتنرت یک شب تمام قوانین رابطه رو تعیین کنه، پایه‌ای؟",
    "ترجیح می‌دی کنترل همه‌چیز در دست تو باشه یا تمام اختیارت رو به دیگری بسپاری؟",
    "اینکه بهت دستور بدن در رابطه چه کار کنی برات جذابه یا حس تحقیر بهت می‌ده؟",
    "فشردن گلو به آرامی، کشیدن موها یا زدن ضربه ملایم؛ خط قرمزته یا هیجان‌بخش؟",
    "چقدر اهل کل‌کل‌های حسی و لجبازی قبل از شروع نزدیکی هستی؟",
    "تا حالا حس مقاومت مصنوعی کردی برای اینکه طرف مقابل حریص‌تر بشه؟",
    "ترجیح می‌دی طرفت آدم بی‌رحم و با اعتماد‌به‌نفس در تخت‌خواب باشه یا مهربان و ملاحظه‌کار؟",
    "بسته شدن دست‌ها با طناب یا دستبند چقدر توی ذهنت تحریک‌کننده به حساب میاد؟",
    "آیا قدرت و نفوذ اجتماعی یا مالی جنس مخالف، کشش جنسی برات ایجاد می‌کنه؟",
    "واکنشت وقتی پارتنرت فرمانی می‌ده که کمی مرزت رو به چالش می‌کشه چیه؟",
    "آیا تا حالا از موضع ضعف قرار گرفتن در یک خلوت لذت بردی؟",
    "دوست داری از طرف مقابل اعتراف‌های حسی اجباری بگیری وقتی کاملاً تسلیم توئه؟",
    "تعقیب و گریز فیزیکی شوخی‌آمیز چقدر برات حکم پیش‌نوازی داره؟",
    "تفاوت ترجیحاتت در بیرون از اتاق خواب با داخل اون چقدر متناقضه؟ (مثلاً آدمی مستقل در بیرون ولی تسلیم در اتاق)",
    "وقتی کسی سعی می‌کنه روت مسلط بشه چه واکنشی نشون می‌دی؟",
    "جسورانه‌ترین مکانی که تا حالا شیطنت کردی کجا بوده؟",
    "تا حالا به سکس در مکان‌های نیمه‌عمومی مثل ماشین در جاده، ساحل تاریک یا پرو لباس فکر کردی؟",
    "ترس از لو رفتن یا گیر افتادن، حست رو قوی‌تر می‌کنه یا کلاً خاموشت می‌کنه؟",
    "نظرت درباره برقراری رابطه در خانه پدری در حالی که بقیه خوابن چیه؟",
    "آیا تا حالا شیطنتی انجام دادی که بعدش بگی «خدا رحم کرد کسی ندید»؟",
    "تا چه حد اجازه می‌دی کنجکاوی‌های جنسی، مرزهای اخلاقی عرفیت رو جابه‌جا کنن؟",
    "نظرت درباره کارهای پرریسک در سفر با قطار یا هواپیما چیه؟",
    "آیا تا حالا در استخر، جکوزی یا دریا خلوت دو نفره رو تجربه کردی؟",
    "اگر در یک مهمونی خصوصی پیشنهادی دریافت کنی که بری به یک اتاق خلوت، قبول می‌کنی؟",
    "بدترین یا خنده‌دارترین شرایطی که وسط شیطنت غافلگیر شدی چی بوده؟",
    "تا حالا شده از سر مستی یا ناآگاهی کاری انجام بدی که فرداش یادت نیاد یا پشیمون بشی؟",
    "آیا تماشای بدون اجازه خلوت دیگران برات حس کنجکاوی غریزی ایجاد می‌کنه؟",
    "تا حالا به امتحان کردن چیزهایی که جامعه اونا رو زشت یا ممنوعه می‌دونه فکر کردی؟",
    "ریسکی‌ترین اس‌ام‌اس یا عکسی که فرستادی و نزدیک بود به دست فرد اشتباهی برسه چی بود؟",
    "چت کردن با لحن هیجانی و جنسی (Sexting) چقدر روی برانگیختگی واقعی بدنت موثره؟",
    "آیا تا حالا پشیمون شدی از فرستادن عکسی که صورتت داخلش بوده؟",
    "ترجیح می‌دی اول یک عکس مبهم و وسوسه‌کننده بفرستی یا مستقیم و واضح؟",
    "ویس دادن با صدای زیرپوستی و لحن نفس‌زده رو ترجیح می‌دی یا متنی چت کردن؟",
    "تا حالا حین مکالمه متنی با کسی به ارگاسم رسیدی؟",
    "در چه صورتی حاضر می‌شی برای یک نفر در تلگرام یا اینستاگرام عکس شخصی بدون سانسور بفرستی؟",
    "آیا تا حالا اکانت فیک زدی فقط برای اینکه در فضای مجازی فانتزی‌هات رو بدون شناخته شدن پیاده کنی؟",
    "نظرت درباره تماس تصویری داغ (Video call) چیه؛ پایه‌ای یا ناامن حسش می‌کنی؟",
    "خواندن داستان‌های اروتیک و متنی برات جذاب‌تره یا دیدن ویدیو؟",
    "عجیب‌ترین درخواستی که توی دایرکت یا چت ناشناس ازت شده چی بود؟",
    "اگر یک نفر الان بهت پیام بده «عکس چیزی رو بفرست که دوست داری پارتنرت ببینه»، واکنشت چیه؟",
    "تا حالا شده بعد از فرستادن یک پیام خیلی داغ، از استرس خاموش کردن گوشی رو انتخاب کنی؟",
    "اگر کسی بدون اجازه از روی عکس‌های ارسالیت اسکرین‌شات بگیره، چیکار می‌کنی؟",
    "آیا رابطه صرفاً مجازی و از راه دور بدون تماس واقعی می‌تونه نیازهای جنسیت رو تامین کنه؟",
    "بزرگ‌ترین عاملی که در عرض ۵ ثانیه می‌تونه کل میل جنسیت رو خاموش کنه چیه؟",
    "بوی بد دهان، موهای زائد نامرتب، یا کثیفی ناخن؛ کدوم بدترین کابوسته؟",
    "رفتارهای مکانیکی و بدون احساس در تخت‌خواب چقدر آزارت می‌ده؟",
    "برات مهمه که پارتنرت دقیقاً بدونه با بدنت چیکار کنه یا دوست داری بهش یاد بدی؟",
    "آیا گاز گرفتن گردن یا مکیدن پوست (ایجاد Hicky) برات لذت‌بخشه یا مایه خجالت روز بعد؟",
    "بوسیدن لب‌ها؛ ترجیح می‌دی فرانسوی و عمیق باشه یا نرم، کوتاه و با آرامش؟",
    "ماساژ بدن با روغن قبل از رابطه چقدر روت جواب می‌ده؟",
    "دیدن بدن برهنه خودت در آینه چه حسی بهت می‌ده؛ اعتمادبه‌نفس یا حس نارضایتی؟",
    "آیا ترجیح می‌دی طرف مقابلت حین رابطه صدای ناله‌اش بلند باشه یا سکوت کنه؟",
    "نظرت درباره اصلاح کامل موهای بدن چیست؛ لازمه اصلیه یا سلیقه‌ای؟",
    "گاز گرفتن لب‌ها در حین بوسه چه حسی بهت می‌ده؟",
    "تا حالا شده حین رابطه خنده‌ت بگیره و نتونی جلوی خودت رو بگیری؟",
    "حرکات پرشتاب رو ترجیح می‌دی یا روندی بسیار کند، طاقت‌فرسا و پر از مکث؟",
    "بعد از رابطه چه رفتاری رو نیاز داری: بغل و حرف زدن یا اینکه هرکس بره پی کار خودش؟",
    "اگر پارتنرت مهارت کافی نداشته باشه ولی اشتیاقش زیاد باشه، چه رفتاری می‌کنی؟",
    "اولین باری که خودارضایی رو کشف کردی چند سالت بود و حس بعدش چی بود؟",
    "تا حالا چند بار رابطه‌ای رو صرفاً به خاطر نداشتن کیفیت در خلوت دونفره تموم کردی؟",
    "آیا تا حالا شیطنتی داشتی که حس کنی اگر خانواده‌ت بفهمن سکته می‌کنن؟",
    "نظرت درباره پورنوگرافی چیه؛ تماشای منظمش به کیفیت رابطه کمک می‌کنه یا تخریبش می‌کنه؟",
    "تا حالا حسودی کردی به اینکه پارتنرت با نفر قبلیش چه کارهایی رو امتحان کرده؟",
    "آیا شده با کسی باشی و بعد از تموم شدن کارت ناگهان حس نفرت نسبت بهش پیدا کنی (Post-nut clarity)؟",
    "تا حالا کشش جنسی به هم‌جنس خودت حس کردی، حتی برای چند لحظه؟",
    "اگر بدونی هیچ قضاوتی در کار نیست، چه تمایل جنسی هست که دوست داری فوراً تست کنی؟",
    "آیا تا حالا به رابطه سه نفره (Threesome) به عنوان یک فکر گذرا فکر کردی؟",
    "حاضری برای یک تجربه بی‌نظیر حسی با کسی باشی که اصلاً به شخصیتش احترام نمی‌ذاری؟",
    "بیشترین زمانی که تونستی در اوج هیجان خودت رو کنترل کنی چقدر بوده؟",
    "اعترافی که به هیچ پارتنری در زندگیت نکردی درباره بدنت یا تمایلاتت چیه؟",
    "اگر همین الان از پشت صفحه بپرم بیرون و توی اتاقت باشم، اولین واکنشت تماس فیزیکیه یا داد زدن؟"
]

def init_db():
    # فقط خطای duplicate-column در _add_column_if_missing نادیده گرفته می‌شود.
    with get_db_connection() as conn:
        cur = conn.cursor()

        cur.execute('''CREATE TABLE IF NOT EXISTS users
                     (user_id INTEGER PRIMARY KEY,
                      username TEXT,
                      first_name TEXT,
                      last_name TEXT,
                      display_name TEXT,
                      join_date TEXT,
                      age INTEGER,
                      education TEXT,
                      location TEXT,
                      gender TEXT,
                      last_seen TEXT,
                      karma INTEGER DEFAULT 0,
                      reports_count INTEGER DEFAULT 0,
                      photo_file_id TEXT,
                      bio TEXT,
                      unique_link TEXT)''')

        columns_to_add = [
            ("karma", "INTEGER DEFAULT 0"),
            ("reports_count", "INTEGER DEFAULT 0"),
            ("photo_file_id", "TEXT"),
            ("bio", "TEXT"),
            ("unique_link", "TEXT"),
            ("position", "TEXT"),
            ("coins", "INTEGER DEFAULT 0"),
            ("level", "INTEGER DEFAULT 0"),
            ("referrals_count", "INTEGER DEFAULT 0"),
            ("daily_chats_used", "INTEGER DEFAULT 0"),
            ("last_reset_date", "TEXT"),
            ("last_daily_reward", "TEXT"),
            ("likes_count", "INTEGER DEFAULT 0"),
            ("dislikes_count", "INTEGER DEFAULT 0"),
            ("invited_by", "INTEGER"),
            ("daily_streak", "INTEGER DEFAULT 0"),
            ("profile_id", "TEXT"),
            ("last_retention_date", "TEXT"),
            ("pending_return_reward", "INTEGER DEFAULT 0"),
            ("profile_reward_claimed", "INTEGER DEFAULT 0"),
            ("extra_chat_credits", "INTEGER DEFAULT 0"),
        ]
        for col_name, col_type in columns_to_add:
            _add_column_if_missing(cur, 'users', col_name, col_type)

        cur.execute('CREATE INDEX IF NOT EXISTS idx_profile_id ON users(profile_id)')
        cur.execute('CREATE INDEX IF NOT EXISTS idx_unique_link ON users(unique_link)')

        cur.execute('''CREATE TABLE IF NOT EXISTS chats
                     (chat_id INTEGER PRIMARY KEY AUTOINCREMENT,
                      user1_id INTEGER,
                      user2_id INTEGER,
                      start_time TEXT,
                      end_time TEXT,
                      via_link INTEGER DEFAULT 0)''')
        cur.execute('''CREATE TABLE IF NOT EXISTS required_channels
                     (channel_id INTEGER PRIMARY KEY AUTOINCREMENT,
                      channel_username TEXT UNIQUE)''')
        cur.execute('''CREATE TABLE IF NOT EXISTS blocked_users (user_id INTEGER PRIMARY KEY)''')
        cur.execute('''CREATE TABLE IF NOT EXISTS user_blocks
                     (blocker_id INTEGER, blocked_id INTEGER, UNIQUE(blocker_id, blocked_id))''')
        cur.execute('''CREATE TABLE IF NOT EXISTS user_profile_votes
                     (voter_id INTEGER, target_id INTEGER, vote_type TEXT, UNIQUE(voter_id, target_id))''')
        cur.execute('''CREATE TABLE IF NOT EXISTS chat_requests
                     (sender_id INTEGER, receiver_id INTEGER, last_action_time TEXT, status TEXT,
                      UNIQUE(sender_id, receiver_id))''')
        _add_column_if_missing(cur, 'chat_requests', 'fee_paid', 'INTEGER DEFAULT 0')
        cur.execute('''CREATE TABLE IF NOT EXISTS transactions
                     (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, amount INTEGER,
                      method TEXT, status TEXT, created_at TEXT)''')

        _add_column_if_missing(cur, 'transactions', 'charge_id', 'TEXT')
        _add_column_if_missing(cur, 'transactions', 'stars_paid', 'INTEGER')
        cur.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_tx_charge_id ON transactions(charge_id)')

        cur.execute('''CREATE TABLE IF NOT EXISTS user_reports
                     (id INTEGER PRIMARY KEY AUTOINCREMENT,
                      reporter_id INTEGER NOT NULL,
                      target_id INTEGER NOT NULL,
                      reason TEXT,
                      created_at TEXT,
                      UNIQUE(reporter_id, target_id))''')
        cur.execute('CREATE INDEX IF NOT EXISTS idx_reports_target ON user_reports(target_id)')
        cur.execute('CREATE INDEX IF NOT EXISTS idx_last_seen ON users(last_seen)')
        cur.execute('CREATE INDEX IF NOT EXISTS idx_chats_user1 ON chats(user1_id)')
        cur.execute('CREATE INDEX IF NOT EXISTS idx_chats_user2 ON chats(user2_id)')

        cur.execute('''CREATE TABLE IF NOT EXISTS friendships
                     (user1_id INTEGER NOT NULL,
                      user2_id INTEGER NOT NULL,
                      created_at TEXT NOT NULL,
                      origin_chat_id INTEGER,
                      alias_by_user1 TEXT,
                      alias_by_user2 TEXT,
                      PRIMARY KEY (user1_id, user2_id),
                      CHECK (user1_id < user2_id))''')
        cur.execute('CREATE INDEX IF NOT EXISTS idx_friendships_user1 ON friendships(user1_id)')
        cur.execute('CREATE INDEX IF NOT EXISTS idx_friendships_user2 ON friendships(user2_id)')

        cur.execute('''CREATE TABLE IF NOT EXISTS friend_requests
                     (chat_id INTEGER NOT NULL,
                      sender_id INTEGER NOT NULL,
                      receiver_id INTEGER NOT NULL,
                      status TEXT NOT NULL DEFAULT 'pending',
                      created_at REAL NOT NULL,
                      updated_at REAL NOT NULL,
                      PRIMARY KEY (chat_id, sender_id, receiver_id))''')
        cur.execute('CREATE INDEX IF NOT EXISTS idx_friend_requests_receiver_status ON friend_requests(receiver_id, status)')

        cur.execute('''CREATE TABLE IF NOT EXISTS support_tickets
                     (ticket_id INTEGER PRIMARY KEY AUTOINCREMENT,
                      user_id INTEGER NOT NULL,
                      category TEXT NOT NULL DEFAULT 'general',
                      status TEXT NOT NULL DEFAULT 'open',
                      created_at TEXT NOT NULL,
                      updated_at TEXT NOT NULL,
                      closed_by INTEGER)''')
        cur.execute('CREATE INDEX IF NOT EXISTS idx_support_tickets_user_status ON support_tickets(user_id, status)')
        cur.execute('CREATE INDEX IF NOT EXISTS idx_support_tickets_status_updated ON support_tickets(status, updated_at)')

        cur.execute('''CREATE TABLE IF NOT EXISTS support_ticket_messages
                     (id INTEGER PRIMARY KEY AUTOINCREMENT,
                      ticket_id INTEGER NOT NULL,
                      sender_id INTEGER NOT NULL,
                      sender_role TEXT NOT NULL,
                      content_type TEXT NOT NULL,
                      text_preview TEXT,
                      status TEXT NOT NULL DEFAULT 'pending',
                      locked_by_admin_id INTEGER,
                      locked_at REAL DEFAULT 0,
                      answered_by_admin_id INTEGER,
                      answered_by_name TEXT,
                      answered_at TEXT,
                      reply_preview TEXT,
                      created_at TEXT NOT NULL)''')
        cur.execute('CREATE INDEX IF NOT EXISTS idx_support_msgs_ticket ON support_ticket_messages(ticket_id)')
        cur.execute('CREATE INDEX IF NOT EXISTS idx_support_msgs_status ON support_ticket_messages(status)')

        cur.execute('''CREATE TABLE IF NOT EXISTS support_admin_notifications
                     (ticket_msg_id INTEGER NOT NULL,
                      admin_id INTEGER NOT NULL,
                      telegram_message_id INTEGER NOT NULL,
                      has_caption INTEGER NOT NULL DEFAULT 0,
                      PRIMARY KEY (ticket_msg_id, admin_id))''')

        # درخواست‌هایی که ربات هنگام وضعیت accepting خاموش شده، اتصال فعال قابل بازیابی ندارند.
        cur.execute("SELECT sender_id, COALESCE(fee_paid, 0) FROM chat_requests WHERE status='accepting'")
        interrupted_requests = cur.fetchall()
        for sender_id, fee_paid in interrupted_requests:
            if fee_paid:
                cur.execute('UPDATE users SET coins = COALESCE(coins, 0) + ? WHERE user_id = ?',
                            (CHAT_REQUEST_COST, sender_id))
        cur.execute("UPDATE chat_requests SET status='delivery_failed', fee_paid=0 WHERE status='accepting'")
        conn.commit()

# ==================================================
# 🌟 توابع کمکی سیستم لایک و دیس‌لایک
# ==================================================
def get_user_vote_stats(user_id):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT likes_count, dislikes_count FROM users WHERE user_id = ?', (user_id,))
        row = cur.fetchone()
        if row: return row[0] or 0, row[1] or 0
        return 0, 0

def cast_vote(voter_id, target_id, vote_type):
    """ثبت یا تغییر رأی با اعتبارسنجی و شمارنده‌های اتمیک."""
    if vote_type not in ('like', 'dislike'):
        return False, "نوع رأی نامعتبر است."
    if not isinstance(voter_id, int) or not isinstance(target_id, int):
        return False, "شناسه کاربر نامعتبر است."
    if voter_id == target_id:
        return False, "نمی‌توانید به پروفایل خودتان رأی بدهید."

    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')
        cur.execute('SELECT 1 FROM users WHERE user_id = ?', (voter_id,))
        if not cur.fetchone():
            conn.rollback()
            return False, "ابتدا پروفایل خودتان را بسازید."
        cur.execute('SELECT 1 FROM users WHERE user_id = ?', (target_id,))
        if not cur.fetchone():
            conn.rollback()
            return False, "پروفایل موردنظر پیدا نشد."

        cur.execute('SELECT vote_type FROM user_profile_votes WHERE voter_id = ? AND target_id = ?',
                    (voter_id, target_id))
        row = cur.fetchone()

        if row and row[0] == vote_type:
            conn.rollback()
            return False, "شما قبلاً همین رأی را ثبت کرده‌اید."

        if row and row[0] in ('like', 'dislike'):
            old_vote = row[0]
            cur.execute('UPDATE user_profile_votes SET vote_type = ? WHERE voter_id = ? AND target_id = ?',
                        (vote_type, voter_id, target_id))
            if old_vote == 'like':
                cur.execute('UPDATE users SET likes_count = MAX(0, COALESCE(likes_count, 0) - 1), '
                            'dislikes_count = COALESCE(dislikes_count, 0) + 1 WHERE user_id = ?', (target_id,))
            else:
                cur.execute('UPDATE users SET dislikes_count = MAX(0, COALESCE(dislikes_count, 0) - 1), '
                            'likes_count = COALESCE(likes_count, 0) + 1 WHERE user_id = ?', (target_id,))
            conn.commit()
            return True, "✅ رأی شما با موفقیت تغییر کرد."

        cur.execute('INSERT INTO user_profile_votes (voter_id, target_id, vote_type) VALUES (?, ?, ?)',
                    (voter_id, target_id, vote_type))
        column = 'likes_count' if vote_type == 'like' else 'dislikes_count'
        cur.execute(f'UPDATE users SET {column} = COALESCE({column}, 0) + 1 WHERE user_id = ?', (target_id,))
        conn.commit()
        return True, "✅ رأی شما با موفقیت ثبت شد."

def get_satisfaction_display(likes, dislikes):
    total_votes = likes + dislikes
    if total_votes == 0:
        return "⚪️ هنوز رای ثبت نشده"
    
    prior_weight = 3
    prior_prob = 0.7
    
    adjusted_likes = likes + (prior_weight * prior_prob)
    adjusted_total = total_votes + prior_weight
    percentage = int((adjusted_likes / adjusted_total) * 100)
    
    if total_votes < 5:
        badge = "🌱 نوپا"
    elif total_votes < 20:
        badge = "🔍 معمولی"
    else:
        badge = "⭐ معتبر"

    if percentage >= 85: return f"🟢 {percentage}٪ (عالی - {badge})"
    elif percentage >= 70: return f"🔵 {percentage}٪ (خوب - {badge})"
    elif percentage >= 50: return f"🟡 {percentage}٪ (متوسط - {badge})"
    else: return f"🔴 {percentage}٪ (ضعیف - {badge})"

# ============================================================================
# 2) Rate-limited last_seen (جلوگیری از قفل شدن دیتابیس)
# ============================================================================
def update_last_seen(user_id):
    """به‌جای یک UPDATE در هر پیام، حداکثر هر ۶۰ ثانیه یک write با قفل Thread-Safe انجام می‌دهد."""
    now_ts = time.time()
    with last_seen_lock:
        last_write = _last_seen_write.get(user_id, 0.0)
        if now_ts - last_write < LAST_SEEN_WRITE_TTL:
            return
        _last_seen_write[user_id] = now_ts

    now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    try:
        with get_db_connection() as conn:
            conn.execute('UPDATE users SET last_seen = ? WHERE user_id = ?', (now_str, user_id))
            conn.commit()
    except Exception:
        logger.error('update_last_seen failed', exc_info=True)

def add_user(user):
    with get_db_connection() as conn:
        cur = conn.cursor()
        # 🌟 واریز ۱۰ سکه هدیه اولیه به کاربر جدید در زمان ثبت‌نام
        cur.execute('''INSERT OR IGNORE INTO users
                       (user_id, username, first_name, last_name, display_name, join_date, age, education, location, gender, last_seen, coins)
                       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 10)''',
                       (user.id, user.username, user.first_name, user.last_name,
                        user.first_name, datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                        None, None, None, None, None))
        conn.commit()

def get_user(user_id):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT * FROM users WHERE user_id = ?', (user_id,))
        return cur.fetchone()

def get_or_create_user_link(user_id):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')
        cur.execute('SELECT unique_link FROM users WHERE user_id = ?', (user_id,))
        row = cur.fetchone()
        if not row:
            conn.rollback()
            return None

        current = row[0]
        if current and re.fullmatch(r'chat_[0-9a-fA-F]{32}', str(current)):
            conn.commit()
            return current

        new_link = _generate_private_link_token(cur)
        cur.execute('UPDATE users SET unique_link = ? WHERE user_id = ?', (new_link, user_id))
        if cur.rowcount != 1:
            conn.rollback()
            return None
        conn.commit()
        return new_link

def get_user_by_link(link_token):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT * FROM users WHERE unique_link = ?', (link_token,))
        return cur.fetchone()

def get_or_create_profile_id(user_id):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')
        cur.execute('SELECT profile_id FROM users WHERE user_id = ?', (user_id,))
        row = cur.fetchone()
        if not row:
            conn.rollback()
            return None

        if row[0]:
            conn.commit()
            return row[0]

        for _ in range(20):
            new_id = uuid.uuid4().hex[:16]
            cur.execute('SELECT 1 FROM users WHERE profile_id = ? LIMIT 1', (new_id,))
            if cur.fetchone() is None:
                break
        else:
            conn.rollback()
            raise RuntimeError('Could not generate a unique profile id')

        cur.execute('UPDATE users SET profile_id = ? WHERE user_id = ?', (new_id, user_id))
        if cur.rowcount != 1:
            conn.rollback()
            return None
        conn.commit()
        return new_id

def get_user_by_profile_id(profile_id):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT * FROM users WHERE profile_id = ?', (profile_id,))
        return cur.fetchone()

def update_age(user_id, age):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('UPDATE users SET age = ? WHERE user_id = ?', (age, user_id))
        conn.commit()

def update_education(user_id, education):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('UPDATE users SET education = ? WHERE user_id = ?', (education, user_id))
        conn.commit()

def update_location(user_id, location):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('UPDATE users SET location = ? WHERE user_id = ?', (location, user_id))
        conn.commit()

def update_gender(user_id, gender):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('UPDATE users SET gender = ? WHERE user_id = ?', (gender, user_id))
        conn.commit()

def update_position(user_id, pos):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('UPDATE users SET position = ? WHERE user_id = ?', (pos, user_id))
        conn.commit()

def get_user_position(user_id):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT position FROM users WHERE user_id = ?', (user_id,))
        row = cur.fetchone()
        return row[0] if row else None

def get_user_gender(user_id):
    row = get_user(user_id)
    return row[9] if row else None

def is_profile_complete(user_id):
    """بررسی می‌کند که آیا کاربر تمام ۵ مرحله ثبت‌نام را به طور کامل گذرانده است یا خیر"""
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT age, education, location, gender, position FROM users WHERE user_id = ?', (user_id,))
        row = cur.fetchone()
        if not row:
            return False
        # اگر حتی یکی از این فیلدهای اصلی خالی (None) باشد، پروفایل ناقص است
        return all(x is not None and str(x).strip() != '' for x in row)

# ======================================================================
# --- کش رم برای مسدودی‌ها (O(1)) و اطلاعات ربات (مقیاس‌پذیری ترافیک سنگین)
# ======================================================================
blocked_cache = set()
blocked_cache_lock = threading.Lock()
_bot_info_cache = {'id': None, 'username': None}
_bot_info_lock = threading.Lock()

def load_blocked_cache():
    """بارگذاری یک‌باره لیست مسدودها در رم هنگام روشن شدن ربات"""
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT user_id FROM blocked_users')
        rows = [r[0] for r in cur.fetchall()]
    with blocked_cache_lock:
        blocked_cache.clear()
        blocked_cache.update(rows)

def is_blocked(user_id):
    """بررسی آنی در رم بدون درگیر کردن دیتابیس در ترد اصلی گیت"""
    with blocked_cache_lock:
        return user_id in blocked_cache

def unblock_user(user_id):
    """رفع مسدودی همزمان در دیتابیس و کش رم"""
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('DELETE FROM blocked_users WHERE user_id = ?', (user_id,))
        conn.commit()
    with blocked_cache_lock:
        blocked_cache.discard(user_id)

def block_user(user_id):
    """مسدود کردن همزمان کاربر در دیتابیس و کش رم + قطع فوری چت فعال"""
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('INSERT OR IGNORE INTO blocked_users (user_id) VALUES (?)', (user_id,))
        conn.commit()
    with blocked_cache_lock:
        blocked_cache.add(user_id)
    try:
        force_end_chat(user_id, "⚠️ کاربر مقابل چت را پایان داد.")
    except Exception:
        logger.error(f"force_end_chat failed for banned user {user_id}", exc_info=True)

def get_bot_username():
    """دریافت نام کاربری ربات از کش رم بدون تماس تکراری با API تلگرام"""
    if _bot_info_cache['username']:
        return _bot_info_cache['username']
    with _bot_info_lock:
        if not _bot_info_cache['username']:
            me = bot.get_me()
            _bot_info_cache['id'] = me.id
            _bot_info_cache['username'] = me.username
        return _bot_info_cache['username']

def get_bot_id():
    """دریافت آیدی عددی ربات از کش رم"""
    if _bot_info_cache['id']:
        return _bot_info_cache['id']
    with _bot_info_lock:
        if not _bot_info_cache['id']:
            me = bot.get_me()
            _bot_info_cache['id'] = me.id
            _bot_info_cache['username'] = me.username
        return _bot_info_cache['id']

# ============================================================================
# 9) Central chat termination + pair hardening (مدیریت متمرکز و امن پایان چت‌ها)
# ============================================================================
def _chat_lifecycle_serialized(func):
    """همه عملیات پایان یک چت را بر اساس chat_id سریالی می‌کند."""
    @functools.wraps(func)
    def wrapper(chat_id, *args, **kwargs):
        try:
            chat_key = int(chat_id)
        except (TypeError, ValueError):
            logger.error("invalid chat_id for lifecycle lock: %r", chat_id)
            return False

        lock = chat_lifecycle_locks[chat_key % len(chat_lifecycle_locks)]
        with lock:
            return func(chat_id, *args, **kwargs)
    return wrapper


def _chat_message_serialized(func):
    """ارسال پیام و پایان همان Pair را روی یک قفل مشترک سریالی می‌کند."""
    @functools.wraps(func)
    def wrapper(message, *args, **kwargs):
        user = getattr(message, 'from_user', None)
        if user is None:
            return None
        user_id = user.id

        # Snapshot از Pair در قفل مرکزی گرفته می‌شود تا دیکشنری حین خواندن تغییر نکند.
        with queue_lock:
            pair = active_pairs.get(user_id)

        # خارج از چت بودن طبیعی است؛ هندلر پیام عادی «در چت نیستید» را نمایش می‌دهد.
        if not pair:
            return func(message, *args, **kwargs)

        try:
            partner_id, chat_id, _via_link = pair
            lock_index = int(chat_id) % len(chat_lifecycle_locks)
        except (TypeError, ValueError):
            logger.error("invalid active pair snapshot for user=%s: %r", user_id, pair)
            return None

        lock = chat_lifecycle_locks[lock_index]
        with lock:
            # اگر در فاصله snapshot تا گرفتن قفل، چت عوض شده باشد پیام قدیمی حذف می‌شود.
            with queue_lock:
                current_pair = active_pairs.get(user_id)

            if current_pair != pair:
                logger.info(
                    "dropped message because pair changed before relay user=%s old_chat=%s",
                    user_id, chat_id
                )
                return None

            # آپدیت قدیمی‌تر از شروع Pair نباید وارد چت جدید شود.
            message_date = getattr(message, 'date', None)
            chat_start = chat_start_times.get(chat_id)
            try:
                if message_date is not None and chat_start is not None:
                    # تاریخ تلگرام دقت ثانیه‌ای دارد؛ یک ثانیه تلورانس حفظ می‌شود.
                    if float(message_date) < float(chat_start) - 1:
                        logger.info(
                            "dropped stale pre-chat update user=%s chat=%s message=%s",
                            user_id, chat_id, getattr(message, 'message_id', None)
                        )
                        return None
            except (TypeError, ValueError):
                logger.warning(
                    "could not validate message timestamp user=%s chat=%s",
                    user_id, chat_id
                )

            return func(message, *args, **kwargs)
    return wrapper


@_chat_lifecycle_serialized
def terminate_chat_session(chat_id, u1=None, u2=None, *, notice_user=None,
                            notice_text=None, send_feedback=True):
    """تنها مسیر مجاز و امن برای جمع‌کردن state یک chat."""
    with queue_lock:
        if u1 is None or u2 is None:
            for uid, data in list(active_pairs.items()):
                if data[1] == chat_id:
                    if u1 is None:
                        u1 = uid
                    elif u2 is None and uid != u1:
                        u2 = uid
        if u1 is None:
            return False

        active1 = active_pairs.get(u1)
        if active1 and active1[1] == chat_id:
            active_pairs.pop(u1, None)
        if u2 is not None:
            active2 = active_pairs.get(u2)
            if active2 and active2[1] == chat_id:
                active_pairs.pop(u2, None)

        if u1 is not None:
            user_queue_times.pop(u1, None)
        if u2 is not None:
            user_queue_times.pop(u2, None)

        profile_viewed_tracker.discard((u1, chat_id))
        if u2 is not None:
            profile_viewed_tracker.discard((u2, chat_id))
        game_sessions.discard(chat_id)
        if chat_id not in game_reveal_choices:
            game_chat_groups.pop(chat_id, None)

    with mapping_lock:
        if u1 is not None:
            message_mapping.pop(u1, None)
        if u2 is not None:
            message_mapping.pop(u2, None)

    try:
        cleanup_secure_chat(chat_id, u1, u2 if u2 is not None else u1)
    except Exception:
        logger.error('terminate_chat_session cleanup failed', exc_info=True)
    try:
        end_chat(chat_id)
    except Exception:
        logger.error('terminate_chat_session DB end failed', exc_info=True)

    if send_feedback and u2 is not None:
        try:
            send_feedback_menu(u1, u2, chat_id)
            send_feedback_menu(u2, u1, chat_id)
        except Exception:
            logger.error('feedback dispatch failed', exc_info=True)

    if notice_user and notice_text:
        try:
            bot.send_message(notice_user, notice_text, reply_markup=types.ReplyKeyboardRemove())
        except Exception:
            pass
    return True

def force_end_chat(user_id, partner_notice='⚠️ کاربر مقابل چت را ترک کرد.'):
    """قطع امن چت با اعتبارسنجی دوباره Pair زیر قفل چرخه عمر همان چت."""
    # ابتدا کاربر از صف خارج می‌شود؛ سپس رزرو احتمالی او آزاد می‌شود.
    removed_mode = remove_from_waiting(user_id)

    with queue_lock:
        pair = active_pairs.get(user_id)

    if not pair:
        released = release_search_reservation(user_id)
        return bool(removed_mode is not None or released)

    partner_id, chat_id, _via_link = pair
    lifecycle_lock = chat_lifecycle_locks[int(chat_id) % len(chat_lifecycle_locks)]

    # قفل RLock است؛ terminate_chat_session در نسخه مرحله ۲ نیز همین قفل را می‌گیرد.
    with lifecycle_lock:
        with queue_lock:
            # ممکن است از زمان snapshot، چت قبلی بسته و Pair جدید ساخته شده باشد.
            if active_pairs.get(user_id) != pair:
                return False

            partner_pair = active_pairs.get(partner_id)
            partner_matches = bool(
                partner_pair
                and partner_pair[0] == user_id
                and partner_pair[1] == chat_id
            )

        terminated = terminate_chat_session(
            chat_id,
            user_id,
            partner_id,
            send_feedback=True
        )
        if not terminated:
            return False

        if partner_matches:
            # اگر طرف مقابل تا این لحظه وارد چت دیگری شده، اعلان قدیمی نفرست.
            with queue_lock:
                partner_is_in_another_chat = partner_id in active_pairs

            if not partner_is_in_another_chat:
                try:
                    bot.send_message(
                        partner_id,
                        partner_notice,
                        reply_markup=types.ReplyKeyboardRemove()
                    )
                except Exception:
                    logger.warning(
                        'Could not notify partner after force_end_chat',
                        exc_info=True
                    )

                try:
                    show_main_menu(partner_id, partner_id in ADMINS)
                except Exception:
                    logger.warning(
                        'Could not show main menu after force_end_chat',
                        exc_info=True
                    )

    return True

def pair_users(u1, u2, via_link=False, is_game=False):
    """Pair two users and serialize connection notices with this chat's lifecycle."""
    if u1 == u2:
        return False

    with queue_lock:
        if u1 in active_pairs or u2 in active_pairs:
            return False

        global waiting_pool
        waiting_pool = deque(
            item for item in waiting_pool
            if item['user_id'] not in (u1, u2)
        )
        search_msg1 = search_message_ids.pop(u1, None)
        search_msg2 = search_message_ids.pop(u2, None)
        user_queue_times.pop(u1, None)
        user_queue_times.pop(u2, None)

        try:
            chat_id = start_chat(u1, u2, via_link)
        except Exception:
            logger.error('start_chat failed in pair_users', exc_info=True)
            release_search_reservation(u1)
            release_search_reservation(u2)
            return False

        pair1 = (u2, chat_id, via_link)
        pair2 = (u1, chat_id, via_link)
        active_pairs[u1] = pair1
        active_pairs[u2] = pair2
        with mapping_lock:
            message_mapping[u1] = {}
            message_mapping[u2] = {}

    # Every lifecycle operation for this chat uses the same re-entrant lock.
    # Do not hold queue_lock while making Telegram API calls.
    lifecycle_lock = chat_lifecycle_locks[int(chat_id) % len(chat_lifecycle_locks)]

    chat_markup = types.ReplyKeyboardMarkup(resize_keyboard=True, row_width=2)
    if not is_game:
        chat_markup.add(
            types.KeyboardButton('مشاهده پروفایل مخاطب 👤'),
            types.KeyboardButton('ارسال آیدی من👻')
        )
        chat_markup.row(
            types.KeyboardButton('افزودن به لیست دوستان 🤝')
        )
        chat_markup.row(
            types.KeyboardButton('حالت چت امن 🔒'),
            types.KeyboardButton('🎲 سوال تصادفی')
        )
        chat_markup.add(
            types.KeyboardButton('نفر بعدی ⏭'),
            types.KeyboardButton('پایان چت ❌')
        )
        msg_text = (
            "🎉 <b>اتصال برقرار شد! پیدا کردم و وصل‌تون کردم 🤩</b>\n\n"
            "شما الان با یک کاربر ناشناس در حال چت هستید 🎭\n"
            "به مخاطبت یه 👋 بفرست و سر صحبت رو باز کن 🗣\n\n"
            "👇 <i>از طریق دکمه‌های زیر می‌تونید پروفایل طرف مقابل رو ببینید، بدون لو رفتن آیدی تلگرام به لیست دوستان هم اضافه بشید یا چت رو تغییر بدید.</i>\n\n"
            "🚨 <b>هشدار امنیتی:</b> درخواست انتقال سریع به پی‌وی یا ارسال اطلاعات شخصی را با احتیاط بررسی کنید."
        )
    else:
        chat_markup.add(
            types.KeyboardButton('افزودن به لیست دوستان 🤝'),
            types.KeyboardButton('ارسال آیدی من👻')
        )
        chat_markup.row(
            types.KeyboardButton('حالت چت امن 🔒'),
            types.KeyboardButton('🎲 سوال تصادفی')
        )
        chat_markup.add(types.KeyboardButton('پایان چت ❌'))
        msg_text = (
            "💘 <b>بلایند دیت (Blind Ship) شروع شد!</b>\n\n"
            "شما بر اساس <b>تطابق هوشمند گرایش و پوزیشن</b> به یک هم‌بازی از گروه متصل شدید 🎭\n"
            "⏱ <b>مدت زمان دیت:</b> ۱۵ دقیقه\n"
            "🔒 <i>در طول بازی پروفایل‌ها مخفی است و در پایان ۱۵ دقیقه، در صورت رضایت دوطرفه آیدی‌ها رد و بدل می‌شود!</i>"
        )

    first_notice = None
    second_notice = None

    def pair_is_current():
        with queue_lock:
            return (
                active_pairs.get(u1) == pair1
                and active_pairs.get(u2) == pair2
            )

    def rollback_pair(failed_user, available_user, notice, delivered_messages):
        # The lifecycle RLock is already held by this function.
        for target_user, sent_message in delivered_messages:
            if sent_message is None:
                continue
            try:
                bot.delete_message(target_user, sent_message.message_id)
            except Exception:
                logger.debug(
                    'could not remove partial pair notice user=%s chat=%s',
                    target_user, chat_id,
                    exc_info=True
                )

        with queue_lock:
            # The exact Pair is still current because this function owns its lifecycle lock.
            if active_pairs.get(u1) == pair1 and active_pairs.get(u2) == pair2:
                release_search_reservation(u1, allow_active=True)
                release_search_reservation(u2, allow_active=True)

        terminate_chat_session(chat_id, u1, u2, send_feedback=False)

        try:
            bot.send_message(
                available_user,
                notice,
                reply_markup=types.ReplyKeyboardRemove()
            )
            show_main_menu(available_user, available_user in ADMINS)
        except Exception:
            logger.warning(
                'could not notify available user after pair rollback user=%s',
                available_user,
                exc_info=True
            )

        try:
            bot.send_message(
                failed_user,
                '❌ اتصال برقرار نشد؛ لطفاً دوباره تلاش کنید.',
                reply_markup=types.ReplyKeyboardRemove()
            )
        except Exception:
            logger.debug(
                'could not notify failed user after pair rollback user=%s',
                failed_user,
                exc_info=True
            )

    with lifecycle_lock:
        # A stop/next action may have terminated this Pair before this thread
        # acquired the lifecycle lock. Never send stale connection notices.
        if not pair_is_current():
            logger.info(
                'pair notification skipped because chat already ended chat=%s users=%s,%s',
                chat_id, u1, u2
            )
            return False

        for target_user, search_message_id in (
            (u1, search_msg1),
            (u2, search_msg2)
        ):
            if search_message_id:
                try:
                    bot.delete_message(target_user, search_message_id)
                except Exception:
                    pass

        try:
            first_notice = bot.send_message(
                u1,
                msg_text,
                reply_markup=chat_markup,
                parse_mode='HTML'
            )
        except Exception:
            logger.warning('pair_users: first user unavailable', exc_info=True)
            rollback_pair(
                u1,
                u2,
                '❌ اتصال برقرار نشد؛ سهمیه/هزینه جستجو بازگردانده شد.',
                ((u1, first_notice), (u2, second_notice))
            )
            return False

        try:
            second_notice = bot.send_message(
                u2,
                msg_text,
                reply_markup=chat_markup,
                parse_mode='HTML'
            )
        except Exception:
            logger.warning('pair_users: second user unavailable', exc_info=True)
            rollback_pair(
                u2,
                u1,
                '❌ اتصال برقرار نشد؛ سهمیه/هزینه جستجو بازگردانده شد.',
                ((u1, first_notice), (u2, second_notice))
            )
            return False

        with queue_lock:
            if active_pairs.get(u1) != pair1 or active_pairs.get(u2) != pair2:
                pair_still_current = False
            else:
                search_reservations.pop(u1, None)
                search_reservations.pop(u2, None)
                pair_still_current = True

        if not pair_still_current:
            # Defensive cleanup if another code path changed active_pairs directly.
            for target_user, sent_message in ((u1, first_notice), (u2, second_notice)):
                try:
                    bot.delete_message(target_user, sent_message.message_id)
                except Exception:
                    pass
            return False

    return True

def _notice_allowed(kind, user_id, cooldown):
    now = time.time()
    key = (kind, user_id)
    if now - _notice_last.get(key, 0) < cooldown:
        return False
    _notice_last[key] = now
    if len(_notice_last) > 20000:
        for k in [k for k, t in _notice_last.items() if now - t > 300]:
            _notice_last.pop(k, None)
    return True

def are_users_blocked_mutually(user1_id, user2_id):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('''SELECT 1 FROM user_blocks 
                       WHERE (blocker_id = ? AND blocked_id = ?) 
                          OR (blocker_id = ? AND blocked_id = ?)''', 
                    (user1_id, user2_id, user2_id, user1_id))
        return cur.fetchone() is not None

def update_display_name(user_id, new_name):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('UPDATE users SET display_name = ? WHERE user_id = ?', (new_name, user_id))
        conn.commit()

def is_user_in_chat(user_id):
    # 🚀 نسخه سوپرسریع: فقط چک کردن متغیر رم (RAM) به جای درگیر کردن دیتابیس
    return user_id in active_pairs

def contains_link(text):
    return bool(text and LINK_PATTERN.search(text))

def notify_admins(text, markup=None):
    for a in ADMINS:
        try:
            bot.send_message(a, text, parse_mode='HTML', reply_markup=markup)
        except telebot.apihelper.ApiTelegramException as e:
            if e.error_code == 403:
                pass  # ادمین ربات را بلاک کرده است
        except Exception:
            pass

# ----------------- توابع آنتی اسپم چت -----------------
def _finish_chat_request(sender_id, receiver_id, new_status, *, refund_if_paid=False, expected_status='pending'):
    """تغییر وضعیت و بازپرداخت هزینه در یک تراکنش؛ بدون پرداخت دوباره."""
    allowed = {'accepted', 'rejected', 'cancelled', 'expired', 'delivery_failed'}
    if new_status not in allowed or expected_status not in ('pending', 'accepting'):
        raise ValueError('invalid chat request transition')

    now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')

        cur.execute(
            '''SELECT COALESCE(fee_paid, 0)
               FROM chat_requests
               WHERE sender_id = ? AND receiver_id = ? AND status = ?''',
            (sender_id, receiver_id, expected_status)
        )
        row = cur.fetchone()
        if not row:
            conn.rollback()
            return False, False

        paid = bool(row[0])
        cur.execute(
            '''UPDATE chat_requests
               SET status = ?, last_action_time = ?, fee_paid = 0
               WHERE sender_id = ? AND receiver_id = ? AND status = ?''',
            (new_status, now_str, sender_id, receiver_id, expected_status)
        )
        if cur.rowcount != 1:
            conn.rollback()
            return False, False

        refunded = bool(refund_if_paid and paid)
        if refunded:
            cur.execute(
                'UPDATE users SET coins = COALESCE(coins, 0) + ? WHERE user_id = ?',
                (CHAT_REQUEST_COST, sender_id)
            )
            if cur.rowcount != 1:
                # وضعیت درخواست هم rollback می‌شود تا هزینه گم نشود.
                conn.rollback()
                return False, False

        conn.commit()
        return True, refunded

@bot.callback_query_handler(func=lambda call: call.data.startswith('req_reject_'))
def process_reject_request(call):
    try:
        sender_id = int(call.data[len('req_reject_'):])
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 درخواست نامعتبر است.', show_alert=True)
        return

    receiver_id = call.from_user.id
    update_last_seen(receiver_id)
    try:
        changed, refunded = _finish_chat_request(sender_id, receiver_id, 'rejected', refund_if_paid=True)
    except Exception:
        logger.error('reject chat request failed', exc_info=True)
        bot.answer_callback_query(call.id, 'خطا در پردازش درخواست؛ دوباره تلاش کنید.', show_alert=True)
        return

    if not changed:
        bot.answer_callback_query(call.id, '⚠️ این درخواست قبلاً تعیین تکلیف یا منقضی شده است.', show_alert=True)
        return

    bot.answer_callback_query(call.id, '❌ درخواست رد شد.' + (' هزینه بازگردانده شد.' if refunded else ''))
    try:
        bot.edit_message_text('❌ درخواست چت رد شد.', call.message.chat.id, call.message.message_id)
    except Exception:
        pass
    try:
        bot.send_message(sender_id, '❌ درخواست چت شما رد شد.' + (' هزینه درخواست به حسابتان برگشت.' if refunded else ''))
    except Exception:
        pass


@bot.callback_query_handler(func=lambda call: call.data.startswith('req_accept_'))
def process_accept_request(call):
    try:
        sender_id = int(call.data[len('req_accept_'):])
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 درخواست نامعتبر است.', show_alert=True)
        return

    receiver_id = call.from_user.id
    update_last_seen(receiver_id)
    if sender_id == receiver_id:
        bot.answer_callback_query(call.id, 'درخواست نامعتبر است.', show_alert=True)
        return

    sender_user = get_user(sender_id)
    receiver_user = get_user(receiver_id)
    if (not sender_user or not receiver_user or is_blocked(sender_id) or is_blocked(receiver_id)
            or are_users_blocked_mutually(sender_id, receiver_id)):
        _finish_chat_request(sender_id, receiver_id, 'cancelled', refund_if_paid=True)
        bot.answer_callback_query(call.id, '❌ ارتباط با این کاربر ممکن نیست؛ هزینه بازگردانده شد.', show_alert=True)
        return

    try:
        sender_age = int(sender_user[6])
        receiver_age = int(receiver_user[6])
    except (TypeError, ValueError):
        sender_age = None
        receiver_age = None

    if (not is_profile_complete(sender_id) or not is_profile_complete(receiver_id)
            or sender_age is None or receiver_age is None
            or ((sender_age >= 18) != (receiver_age >= 18))):
        _finish_chat_request(sender_id, receiver_id, 'cancelled', refund_if_paid=True)
        bot.answer_callback_query(call.id, '❌ شرایط لازم برای این ارتباط برقرار نیست؛ هزینه بازگردانده شد.', show_alert=True)
        return

    now_dt = datetime.now()
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')
        cur.execute('''SELECT status, last_action_time, COALESCE(fee_paid, 0)
                       FROM chat_requests WHERE sender_id = ? AND receiver_id = ?''',
                    (sender_id, receiver_id))
        row = cur.fetchone()
        if not row or row[0] != 'pending':
            conn.rollback()
            bot.answer_callback_query(call.id, '⚠️ این درخواست فعال نیست یا قبلاً بررسی شده است.', show_alert=True)
            try:
                bot.delete_message(call.message.chat.id, call.message.message_id)
            except Exception:
                pass
            return

        expired = True
        try:
            request_time = datetime.strptime(row[1], '%Y-%m-%d %H:%M:%S')
            expired = (now_dt - request_time).total_seconds() > CHAT_REQUEST_TTL_SECONDS
        except Exception:
            expired = True

        if expired:
            cur.execute('''UPDATE chat_requests SET status='expired', last_action_time=?, fee_paid=0
                           WHERE sender_id=? AND receiver_id=? AND status='pending' ''',
                        (now_dt.strftime('%Y-%m-%d %H:%M:%S'), sender_id, receiver_id))
            refunded = bool(row[2])
            if refunded:
                cur.execute('UPDATE users SET coins = COALESCE(coins, 0) + ? WHERE user_id = ?',
                            (CHAT_REQUEST_COST, sender_id))
            conn.commit()
            bot.answer_callback_query(call.id, '⏳ مهلت درخواست تمام شده است.' + (' هزینه بازگردانده شد.' if refunded else ''), show_alert=True)
            try:
                bot.edit_message_text('⏳ این درخواست منقضی شده است.', call.message.chat.id, call.message.message_id)
            except Exception:
                pass
            return
        conn.commit()

    busy_sender = False
    with queue_lock:
        if is_user_in_chat(receiver_id):
            bot.answer_callback_query(call.id, 'شما در حال حاضر در یک چت دیگر هستید؛ ابتدا آن را پایان دهید.', show_alert=True)
            return
        if is_user_in_chat(sender_id):
            busy_sender = True
        else:
            remove_from_waiting(receiver_id)
            remove_from_waiting(sender_id)
            release_search_reservation(receiver_id)
            release_search_reservation(sender_id)

    if busy_sender:
        _changed, refunded = _finish_chat_request(sender_id, receiver_id, 'cancelled', refund_if_paid=True)
        bot.answer_callback_query(call.id, 'درخواست‌دهنده مشغول چت دیگری است.' + (' هزینه بازگردانده شد.' if refunded else ''), show_alert=True)
        try:
            bot.edit_message_text('❌ درخواست به دلیل مشغول بودن درخواست‌دهنده لغو شد.', call.message.chat.id, call.message.message_id)
        except Exception:
            pass
        return

    # قفل منطقی در دیتابیس مانع پذیرش/رد هم‌زمان یا چندبار کلیک می‌شود.
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')
        cur.execute('''UPDATE chat_requests SET status='accepting'
                       WHERE sender_id=? AND receiver_id=? AND status='pending' ''',
                    (sender_id, receiver_id))
        claimed = cur.rowcount == 1
        if claimed:
            conn.commit()
        else:
            conn.rollback()

    if not claimed:
        bot.answer_callback_query(call.id, '⚠️ این درخواست توسط اقدام دیگری تعیین تکلیف شده است.', show_alert=True)
        return

    paired = False
    try:
        paired = pair_users(sender_id, receiver_id, via_link=True)
    except Exception:
        logger.error('pair_users raised while accepting chat request', exc_info=True)

    status = 'accepted' if paired else 'delivery_failed'
    _changed, refunded = _finish_chat_request(
        sender_id, receiver_id, status,
        refund_if_paid=not paired,
        expected_status='accepting'
    )
    try:
        bot.delete_message(call.message.chat.id, call.message.message_id)
    except Exception:
        pass

    if paired:
        bot.answer_callback_query(call.id, '✅ اتصال برقرار شد.')
    else:
        bot.answer_callback_query(call.id, '⚠️ اتصال برقرار نشد.' + (' هزینه درخواست بازگردانده شد.' if refunded else ''), show_alert=True)
 
def start_chat(user1_id, user2_id, via_link=False):
    with get_db_connection() as conn:
        cur = conn.cursor()
        start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        cur.execute('''INSERT INTO chats
                       (user1_id, user2_id, start_time, via_link)
                       VALUES (?, ?, ?, ?)''',
                       (user1_id, user2_id, start_time, 1 if via_link else 0))
        conn.commit()
        chat_id = cur.lastrowid
        
        # 🌟 ثبت زمان شروع چت برای فیلتر امنیتی
        chat_last_activity[chat_id] = time.time()
        chat_start_times[chat_id] = time.time() # 🌟
        return chat_id

def end_chat(chat_id):
    with get_db_connection() as conn:
        cur = conn.cursor()
        end_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        cur.execute('UPDATE chats SET end_time = ? WHERE chat_id = ?', (end_time, chat_id))
        conn.commit()
        
    # 🌟 پاک کردن زمان‌سنج از حافظه
    chat_last_activity.pop(chat_id, None)
    chat_start_times.pop(chat_id, None) # 🌟
         
# ============================================================================
# 8) Queue hardening: eliminate duplicate queue entries + minor/adult isolation
# ============================================================================
def remove_from_waiting(user_id, delete_search_msg=True):
    removed_mode = None
    with queue_lock:
        global waiting_pool
        kept = deque()
        for item in waiting_pool:
            if item['user_id'] == user_id:
                if removed_mode is None:
                    removed_mode = item['mode']
                continue
            kept.append(item)
        waiting_pool = kept

        user_queue_times.pop(user_id, None)
        if delete_search_msg:
            search_msg_id = search_message_ids.pop(user_id, None)
        else:
            search_msg_id = None

    if search_msg_id:
        try: bot.delete_message(user_id, search_msg_id)
        except Exception: pass
    return removed_mode

def get_excluded_partners(user_id):
    """بلاک دوطرفه و کسانی که قبلاً با او چت کرده‌اند را استخراج می‌کند"""
    blocked, past = set(), set()
    with get_db_connection() as conn:
        cur = conn.cursor()

        sql = 'SELECT user1_id, user2_id FROM chats WHERE (user1_id = ? OR user2_id = ?)'
        params = [user_id, user_id]
        if REMATCH_COOLDOWN_DAYS:
            cutoff = (datetime.now() - timedelta(days=REMATCH_COOLDOWN_DAYS)).strftime("%Y-%m-%d %H:%M:%S")
            sql += ' AND start_time >= ?'
            params.append(cutoff)
        cur.execute(sql, params)
        for u1, u2 in cur.fetchall():
            past.add(u2 if u1 == user_id else u1)

        cur.execute('SELECT blocker_id, blocked_id FROM user_blocks WHERE blocker_id = ? OR blocked_id = ?',
                    (user_id, user_id))
        for b1, b2 in cur.fetchall():
            blocked.add(b2 if b1 == user_id else b1)

    blocked.discard(user_id)
    past.discard(user_id)
    past -= blocked
    return blocked, past

def try_pairing(user_id, mode):
    global waiting_pool
    my_row = get_user(user_id)
    if not my_row: return False

    my_age = my_row[6]
    my_loc = my_row[8]
    my_gender = my_row[9]
    my_pos = get_user_position(user_id)
    my_filters = advanced_search_filters.get(user_id, {'gender': 'any', 'city': 'any', 'age': 'any', 'pos': 'any'})

    # مسدودها/گذشته‌ها بیرون از قفل محاسبه می‌شوند تا ربات کند نشود
    blocked, past_partners = get_excluded_partners(user_id)

    with queue_lock:
        if user_id in active_pairs: return False

        now_ts = time.time()
        my_wait_start = user_queue_times.get(user_id, now_ts)
        was_in_pool = False
        for existing_item in waiting_pool:
            if existing_item['user_id'] == user_id:
                my_wait_start = existing_item.get('time', my_wait_start)
                was_in_pool = True
                break

        # حذف هرگونه ریکورد تکراری از کاربر قبل از بررسی یا اضافه شدن مجدد
        waiting_pool = deque(item for item in waiting_pool if item['user_id'] != user_id)

        candidates = list(waiting_pool)
        cand_ids = [x['user_id'] for x in candidates if x['user_id'] != user_id]

        cand_data_map = {}
        if cand_ids:
            placeholders = ','.join('?' for _ in cand_ids)
            with get_db_connection() as conn:
                cur = conn.cursor()
                cur.execute(f'''SELECT u.user_id, u.age, u.location, u.gender, u.position
                                FROM users u
                                LEFT JOIN blocked_users b ON b.user_id = u.user_id
                                WHERE u.user_id IN ({placeholders})
                                  AND b.user_id IS NULL''', cand_ids)
                for row in cur.fetchall():
                    cand_data_map[row[0]] = {
                        'age': row[1], 'location': row[2], 'gender': row[3], 'position': row[4]
                    }

        def matches_filters(fit, gen, loc, age, pos):
            if fit.get('gender', 'any') != 'any' and gen != fit.get('gender'): return False
            if fit.get('city', 'any') != 'any' and loc != fit.get('city'): return False
            
            # 🌟 هوشمندی تطابق چندگانه ورس در جستجوی پیشرفته
            t_pos = fit.get('pos', 'any')
            if t_pos != 'any':
                if t_pos == 'top' and pos not in ('top', 'vers'): return False
                if t_pos == 'bottom' and pos not in ('bottom', 'vers'): return False
                if t_pos == 'vers' and pos != 'vers': return False

            t_age = fit.get('age', 'any')
            if t_age != 'any':
                if age is None: return False
                if t_age == '12-18' and not (12 <= age <= 18): return False
                if t_age == '19-25' and not (19 <= age <= 25): return False
                if t_age == '26-35' and not (26 <= age <= 35): return False
                if t_age == '36-99' and age < 36: return False
            return True

        def evaluate(mode_name, fit, self_gen, self_loc, self_age, self_pos, other_gen, other_loc, other_age, other_pos):
            # 🌟 سیستم امنیتی P0: جداسازی سنین زیر ۱۸ سال از بالای ۱۸ سال
            if self_age is not None and other_age is not None:
                if (self_age >= 18) != (other_age >= 18): return False

            if mode_name == 'general': return True
            if mode_name == 'advanced': return matches_filters(fit, other_gen, other_loc, other_age, other_pos)
            if mode_name.startswith('nearby_'):
                target_g = mode_name.split('_', 1)[1]
                if target_g != 'any' and other_gen != target_g: return False
                if not self_loc or not other_loc: return False
                p1 = self_loc.split(' - ')[0] if ' - ' in self_loc else self_loc
                p2 = other_loc.split(' - ')[0] if ' - ' in other_loc else other_loc
                return p1 == p2
            if mode_name.startswith('pos_'):
                target_p = mode_name.split('_')[1]
                # 🌟 هوشمندی تطابق چندگانه ورس در جستجوی اختصاصی
                if target_p == 'top' and other_pos not in ('top', 'vers'): return False
                if target_p == 'bottom' and other_pos not in ('bottom', 'vers'): return False
                if target_p == 'vers' and other_pos != 'vers': return False
                return True
            if mode_name in GENDER_MAP: return other_gen == mode_name
            return True

        def compatible(a_mode, a_fit, a_gen, a_loc, a_age, a_pos, b_mode, b_fit, b_gen, b_loc, b_age, b_pos):
            if a_age is not None and b_age is not None and ((a_age >= 18) != (b_age >= 18)): return False
            a_ok = evaluate(a_mode, a_fit, a_gen, a_loc, a_age, a_pos, b_gen, b_loc, b_age, b_pos)
            b_ok = evaluate(b_mode, b_fit, b_gen, b_loc, b_age, b_pos, a_gen, a_loc, a_age, a_pos)
            return a_ok and b_ok

        matched_item = None

        for allow_past in (False, True):
            if matched_item is not None: break
            for item in list(waiting_pool):
                cand_id = item['user_id']
                if cand_id == user_id or cand_id in blocked: continue
                is_past = cand_id in past_partners
                if is_past != allow_past: continue
                if is_past:
                    cand_waited = now_ts - item.get('time', now_ts)
                    my_waited = now_ts - my_wait_start
                    if REMATCH_RELAX_AFTER_SECONDS is None or max(cand_waited, my_waited) < REMATCH_RELAX_AFTER_SECONDS:
                        continue

                info = cand_data_map.get(cand_id)
                if not info: continue
                if compatible(
                    mode, my_filters, my_gender, my_loc, my_age, my_pos,
                    item['mode'], item.get('filters', {}), info['gender'], info['location'], info['age'], info['position']
                ):
                    matched_item = item
                    break

        if matched_item is None:
            waiting_pool.append({
                'user_id': user_id,
                'mode': mode,
                'filters': my_filters,
                'time': my_wait_start,
                'last_retry': now_ts if was_in_pool else 0.0
            })
            user_queue_times[user_id] = my_wait_start
            return False

        partner_id = matched_item['user_id']
        waiting_pool = deque(item for item in waiting_pool if item['user_id'] not in (user_id, partner_id))
        user_queue_times.pop(user_id, None)
        user_queue_times.pop(partner_id, None)

    # اتصال بیرون از قفل صف؛ تاخیر تلگرام دیگر کل ربات را نمی‌خواباند
    return pair_users(user_id, partner_id)

def is_user_waiting(user_id):
    """بررسی می‌کند که آیا کاربر هم‌‌اکنون در صف انتظار است یا خیر"""
    with queue_lock:
        return any(item['user_id'] == user_id for item in waiting_pool)

def get_required_channels():
    now = time.time()
    with cache_lock:
        if now - channels_cache['t'] < CHANNELS_CACHE_TTL:
            return list(channels_cache['v'])
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT channel_username FROM required_channels')
        rows = [row[0] for row in cur.fetchall()]
    with cache_lock:
        channels_cache['v'] = rows
        channels_cache['t'] = now
    return list(rows)

def invalidate_channels_cache():
    """بعد از اضافه/حذف کانال صدا زده می‌شود."""
    with cache_lock:
        channels_cache['t'] = 0.0
        membership_cache.clear()

# خطاهایی که یعنی «خودِ کانال قابل بررسی نیست» (تنظیمات ربات/کانال) و نه «کاربر عضو نیست»
_CHANNEL_UNUSABLE_ERRORS = ('chat not found', 'bot is not a member', 'member list is inaccessible',
                            'chat_admin_required', 'not enough rights', 'channel_private')

def _cached_membership(user_id):
    """True / False اگر در کش معتبر باشد، وگرنه None (بدون هیچ تماس شبکه)"""
    if user_id in ADMINS:
        return True
    if not get_required_channels():
        return True
    with cache_lock:
        cached = membership_cache.get(user_id)
    if cached and cached[1] > time.time():
        return cached[0]
    return None

def _fetch_member_status(channel, user_id):
    """get_chat_member با سقف زمانی کوتاه (تماس معلق، thread را نمی‌خوابانَد)"""
    fut = api_pool.submit(bot.get_chat_member, channel, user_id)
    try:
        return fut.result(timeout=MEMBERSHIP_API_TIMEOUT)
    except FuturesTimeout:
        fut.cancel()
        raise TimeoutError(f"get_chat_member timeout for {channel}")

def _refresh_membership(user_id, channels):
    # عضویت را بررسی می‌کند؛ خطای API را با عدم عضویت اشتباه نمی‌گیرد.
    not_member = False
    unknown = False

    for channel in channels:
        try:
            member = _fetch_member_status(channel, user_id)
            if member.status in ('left', 'kicked'):
                not_member = True
                break
            if member.status == 'restricted' and getattr(member, 'is_member', True) is False:
                not_member = True
                break
        except telebot.apihelper.ApiTelegramException as exc:
            desc = (getattr(exc, 'description', None) or str(exc)).lower()
            logger.error('[JOIN] could not verify channel %s for user %s: %s', channel, user_id, desc)
            unknown = True
        except Exception:
            logger.error('[JOIN] timeout/unexpected error for channel %s / user %s', channel, user_id, exc_info=True)
            unknown = True

    now = time.time()
    if not_member:
        cached_value, ttl, public_value = False, MEMBERSHIP_TTL_NO, False
    elif unknown:
        cached_value, ttl, public_value = GATE_ERROR, MEMBERSHIP_TTL_UNKNOWN, None
    else:
        cached_value, ttl, public_value = True, MEMBERSHIP_TTL_OK, True

    with cache_lock:
        membership_cache[user_id] = (cached_value, now + ttl)
        if len(membership_cache) > 20000:
            for key, value in list(membership_cache.items()):
                if value[1] < now:
                    membership_cache.pop(key, None)
    return public_value

def check_channels_membership(user_id, force=False):
    # True=عضو، False=عضو نیست، None=خطای بررسی
    if user_id in ADMINS:
        return True

    channels = get_required_channels()
    if not channels:
        return True

    if not force:
        state = _cached_membership(user_id)
        if state == GATE_ERROR:
            return None
        if state is not None:
            return state

    # قفل ثابت شاردشده؛ قفل‌ها حذف و دوباره ساخته نمی‌شوند.
    ulock = user_check_locks[int(user_id) % len(user_check_locks)]
    with ulock:
        if not force:
            state = _cached_membership(user_id)
            if state == GATE_ERROR:
                return None
            if state is not None:
                return state
        try:
            return _refresh_membership(user_id, channels)
        except Exception:
            logger.error('membership refresh failed for user=%s', user_id, exc_info=True)
            return None

def add_required_channel(channel_username):
    try:
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('INSERT INTO required_channels (channel_username) VALUES (?)', (channel_username,))
            conn.commit()
            return True
    except sqlite3.IntegrityError:
        return False

def remove_required_channel(channel_username):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('DELETE FROM required_channels WHERE channel_username = ?', (channel_username,))
        conn.commit()
        affected = cur.rowcount > 0
        return affected

def get_online_count():
    with get_db_connection() as conn:
        cur = conn.cursor()
        five_min_ago = (datetime.now() - timedelta(minutes=5)).strftime("%Y-%m-%d %H:%M:%S")
        cur.execute('SELECT COUNT(*) FROM users WHERE last_seen > ? AND last_seen IS NOT NULL', (five_min_ago,))
        count = cur.fetchone()[0]
        return count

def get_bot_stats():
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT COUNT(*) FROM users')
        total_users = cur.fetchone()[0]
        cur.execute('SELECT COUNT(*) FROM chats')
        total_chats = cur.fetchone()[0]
        cur.execute('SELECT COUNT(*) FROM chats WHERE end_time IS NULL')
        active_chats = cur.fetchone()[0]
        online = get_online_count()
        return {
            'total_users': total_users,
            'total_chats': total_chats,
            'active_chats': active_chats,
            'online_users': online
        }

@bot.callback_query_handler(func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('vote_'))
def handle_profile_vote(call):
    """ثبت رأی پروفایل فقط از دکمه واقعیِ پیام خصوصی ربات."""
    try:
        parts = call.data.split('_', 2)
        if len(parts) != 3 or parts[0] != 'vote' or parts[1] not in ('like', 'dislike'):
            raise ValueError('invalid vote callback')

        vote_type = parts[1]
        target_id = int(parts[2])
        if target_id <= 0:
            raise ValueError('invalid target id')
    except (TypeError, ValueError):
        bot.answer_callback_query(
            call.id,
            '🚫 درخواست رأی نامعتبر است.',
            show_alert=True
        )
        return

    voter_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    message_sender = getattr(message, 'from_user', None) if message else None

    # رأی فقط باید از پیام ربات در گفتگوی خصوصیِ خود رأی‌دهنده صادر شود.
    if (
        not isinstance(voter_id, int)
        or chat is None
        or getattr(chat, 'type', None) != 'private'
        or getattr(chat, 'id', None) != voter_id
        or message_sender is None
    ):
        bot.answer_callback_query(
            call.id,
            '🚫 این دکمه فقط در پیام خصوصی اصلی ربات قابل استفاده است.',
            show_alert=True
        )
        return

    try:
        if getattr(message_sender, 'id', None) != get_bot_id():
            bot.answer_callback_query(
                call.id,
                '🚫 پیام رأی معتبر نیست.',
                show_alert=True
            )
            return
    except Exception:
        logger.warning('could not validate bot identity for profile vote', exc_info=True)
        bot.answer_callback_query(
            call.id,
            '⚠️ بررسی پیام ممکن نشد؛ لطفاً دوباره تلاش کنید.',
            show_alert=True
        )
        return

    # اطمینان از اینکه همین پیام واقعاً دکمه رأی برای همین هدف را دارد.
    markup = getattr(message, 'reply_markup', None)
    rows = getattr(markup, 'keyboard', None) if markup else None
    if rows is None and markup is not None:
        rows = getattr(markup, 'inline_keyboard', None)

    expected_callback = f'vote_{vote_type}_{target_id}'
    has_matching_button = False
    for row in rows or []:
        for button in row:
            if getattr(button, 'callback_data', None) == expected_callback:
                has_matching_button = True
                break
        if has_matching_button:
            break

    if not has_matching_button:
        bot.answer_callback_query(
            call.id,
            '🚫 این دکمه متعلق به این پروفایل نیست یا منقضی شده است.',
            show_alert=True
        )
        return

    try:
        success, result_text = cast_vote(voter_id, target_id, vote_type)
    except Exception:
        logger.error('profile vote database operation failed', exc_info=True)
        bot.answer_callback_query(
            call.id,
            '❌ ثبت رأی به دلیل خطای موقت انجام نشد.',
            show_alert=True
        )
        return

    if not success:
        bot.answer_callback_query(call.id, result_text, show_alert=True)
        return

    bot.answer_callback_query(call.id, result_text, show_alert=False)

    # بروزرسانی شمارنده‌های روی همان پیام؛ شکست ویرایش، رأی ثبت‌شده را برنمی‌گرداند.
    try:
        likes, dislikes = get_user_vote_stats(target_id)
        if markup is not None and rows is not None:
            for row in rows:
                for button in row:
                    callback_data = getattr(button, 'callback_data', None)
                    if callback_data == f'vote_like_{target_id}':
                        button.text = f'👍 لایک ({likes})'
                    elif callback_data == f'vote_dislike_{target_id}':
                        button.text = f'👎 دیس‌لایک ({dislikes})'
            bot.edit_message_reply_markup(
                chat_id=chat.id,
                message_id=message.message_id,
                reply_markup=markup
            )
    except Exception:
        logger.warning('could not refresh profile vote keyboard', exc_info=True)

# ============================================================================
# 5) Atomic daily reward (امنیت جایزه روزانه)
# ============================================================================
@bot.callback_query_handler(func=lambda call: call.data == 'daily_reward')
def process_daily_reward(call):
    """ثبت امن جایزه روزانه؛ فقط از منوی خصوصی خود کاربر."""
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None

    # callbackهای بدون پیام، گروهی یا متعلق به گفتگوی خصوصی کاربر دیگر معتبر نیستند.
    if (
        not isinstance(user_id, int)
        or user_id <= 0
        or message is None
        or chat is None
        or getattr(chat, 'type', None) != 'private'
        or getattr(chat, 'id', None) != user_id
    ):
        try:
            bot.answer_callback_query(
                call.id,
                '🚫 این دکمه فقط از منوی خصوصی حساب خودتان قابل استفاده است.',
                show_alert=True
            )
        except Exception:
            logger.warning('daily reward invalid callback could not be answered', exc_info=True)
        return

    # مطمئن شو callback واقعاً از دکمه daily_reward همین پیام آمده است.
    reply_markup = getattr(message, 'reply_markup', None)
    keyboard = getattr(reply_markup, 'keyboard', None) if reply_markup else None
    has_daily_reward_button = False
    for row in keyboard or []:
        for button in row:
            if getattr(button, 'callback_data', None) == 'daily_reward':
                has_daily_reward_button = True
                break
        if has_daily_reward_button:
            break

    if not has_daily_reward_button:
        try:
            bot.answer_callback_query(
                call.id,
                '⚠️ این دکمه قدیمی یا نامعتبر است.',
                show_alert=True
            )
        except Exception:
            logger.warning('daily reward stale callback could not be answered', exc_info=True)
        return

    update_last_seen(user_id)
    today = datetime.now().strftime('%Y-%m-%d')
    yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
    reward_map = {1: 10, 2: 20, 3: 30, 4: 40, 5: 50, 6: 60, 7: 70, 8: 80, 9: 90, 10: 120}

    try:
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('BEGIN IMMEDIATE')
            cur.execute(
                'SELECT last_daily_reward, daily_streak FROM users WHERE user_id = ?',
                (user_id,)
            )
            row = cur.fetchone()

            if not row:
                conn.rollback()
                try:
                    bot.answer_callback_query(call.id, '❌ حساب کاربری پیدا نشد.', show_alert=True)
                except Exception:
                    pass
                return

            last_reward_date, streak = row
            streak = int(streak or 0)
            if last_reward_date == today:
                conn.rollback()
                try:
                    bot.answer_callback_query(
                        call.id,
                        '❌ شما جایزه امروز را قبلاً گرفته‌اید!',
                        show_alert=True
                    )
                except Exception:
                    pass
                return

            streak = min(streak + 1 if last_reward_date == yesterday else 1, 10)
            reward = reward_map[streak]

            cur.execute(
                '''UPDATE users
                   SET coins = COALESCE(coins, 0) + ?,
                       daily_streak = ?,
                       last_daily_reward = ?
                   WHERE user_id = ?
                     AND (last_daily_reward IS NULL OR last_daily_reward <> ?)''',
                (reward, streak, today, user_id, today)
            )
            if cur.rowcount != 1:
                conn.rollback()
                try:
                    bot.answer_callback_query(
                        call.id,
                        '⚠️ جایزه هم‌زمان توسط درخواست دیگری ثبت شد.',
                        show_alert=True
                    )
                except Exception:
                    pass
                return

            conn.commit()

    except Exception:
        logger.error('daily reward transaction failed for user=%s', user_id, exc_info=True)
        try:
            bot.answer_callback_query(
                call.id,
                '❌ ثبت جایزه با خطا مواجه شد؛ سکه‌ای اضافه نشد. لطفاً دوباره تلاش کنید.',
                show_alert=True
            )
        except Exception:
            pass
        return

    # تراکنش انجام شده؛ خطای به‌روزرسانی منو نباید نتیجه ثبت جایزه را عوض کند.
    try:
        bot.answer_callback_query(
            call.id,
            f'🎁 جایزه روز {streak}م: {reward} سکه',
            show_alert=True
        )
    except Exception:
        logger.warning('daily reward callback answer failed for user=%s', user_id, exc_info=True)

    try:
        back_to_levelup_callback(call)
    except Exception:
        logger.warning('daily reward menu refresh failed for user=%s', user_id, exc_info=True)
        try:
            bot.send_message(user_id, '🎁 جایزه روزانه ثبت شد؛ برای دیدن موجودی، منوی سکه را دوباره باز کنید.')
        except Exception:
            logger.warning('daily reward fallback notification failed for user=%s', user_id, exc_info=True)
# ============================================================================
# 6) Atomic quota + paid search reservation (امنیت سهمیه چت و جستجو)
# ============================================================================
def _max_daily_chats(level):
    if level == 0: return 10
    if level == 1: return 15
    if level == 2: return 20
    if level == 3: return 30
    return 999999

def _reset_daily_counters_locked(cur, user_id, today):
    cur.execute('SELECT last_reset_date FROM users WHERE user_id = ?', (user_id,))
    row = cur.fetchone()
    if not row: return False
    if row[0] != today:
        cur.execute('''UPDATE users SET daily_chats_used = 0,
                                         last_reset_date = ?
                       WHERE user_id = ?''', (today, user_id))
    return True

def has_chat_quota(user_id):
    today = datetime.now().strftime('%Y-%m-%d')
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')
        if not _reset_daily_counters_locked(cur, user_id, today):
            conn.rollback()
            return False
        cur.execute('''SELECT COALESCE(level, 0), COALESCE(daily_chats_used, 0), COALESCE(extra_chat_credits, 0)
                       FROM users WHERE user_id = ?''', (user_id,))
        row = cur.fetchone()
        conn.commit()

    if not row: return False
    level, used, extra = row
    return used < _max_daily_chats(level) or extra > 0

def reserve_search_charge(user_id, coin_cost=0):
    """رزرو همزمان سهمیه چت و سکه در یک تراکنش امن"""
    if coin_cost < 0: return False, None

    with queue_lock:
        if user_id in active_pairs or is_user_waiting(user_id): return False, None

        today = datetime.now().strftime('%Y-%m-%d')
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('BEGIN IMMEDIATE')
            if not _reset_daily_counters_locked(cur, user_id, today):
                conn.rollback()
                return False, None

            cur.execute('''SELECT COALESCE(level, 0), COALESCE(daily_chats_used, 0),
                                  COALESCE(extra_chat_credits, 0)
                           FROM users WHERE user_id = ?''', (user_id,))
            row = cur.fetchone()
            if not row:
                conn.rollback()
                return False, None

            level, used, extra = row
            limit = _max_daily_chats(level)
            if used < limit:
                cur.execute('UPDATE users SET daily_chats_used = COALESCE(daily_chats_used, 0) + 1 WHERE user_id = ?', (user_id,))
                source = 'daily'
            elif extra > 0:
                cur.execute('''UPDATE users SET extra_chat_credits = extra_chat_credits - 1
                               WHERE user_id = ? AND extra_chat_credits > 0''', (user_id,))
                if cur.rowcount != 1:
                    conn.rollback()
                    return False, None
                source = 'extra'
            else:
                conn.rollback()
                return False, None

            if coin_cost > 0:
                cur.execute('''UPDATE users SET coins = COALESCE(coins, 0) - ?
                               WHERE user_id = ? AND COALESCE(coins, 0) >= ?''',
                            (coin_cost, user_id, coin_cost))
                if cur.rowcount != 1:
                    if source == 'daily':
                        cur.execute('''UPDATE users SET daily_chats_used = MAX(0, COALESCE(daily_chats_used, 0) - 1)
                                       WHERE user_id = ?''', (user_id,))
                    elif source == 'extra':
                        cur.execute('UPDATE users SET extra_chat_credits = COALESCE(extra_chat_credits, 0) + 1 WHERE user_id = ?', (user_id,))
                    conn.rollback()
                    return False, None

            conn.commit()

        reservation = {
            'quota_source': source,
            'coin_cost': coin_cost,
            'created_at': time.time(),
        }
        search_reservations[user_id] = reservation
        return True, reservation

def release_search_reservation(user_id, *, allow_active=False):
    # رزرو تا commit موفق از RAM حذف نمی‌شود.
    with queue_lock:
        if user_id in active_pairs and not allow_active:
            return False
        reservation = search_reservations.get(user_id)
        if not reservation:
            return False

        source = reservation.get('quota_source')
        cost = int(reservation.get('coin_cost', 0) or 0)

        try:
            with get_db_connection() as conn:
                cur = conn.cursor()
                cur.execute('BEGIN IMMEDIATE')
                if source == 'daily':
                    cur.execute('''UPDATE users SET daily_chats_used = MAX(0, COALESCE(daily_chats_used, 0) - 1)
                                   WHERE user_id = ?''', (user_id,))
                elif source == 'extra':
                    cur.execute('''UPDATE users SET extra_chat_credits = COALESCE(extra_chat_credits, 0) + 1
                                   WHERE user_id = ?''', (user_id,))
                if cost > 0:
                    cur.execute('UPDATE users SET coins = COALESCE(coins, 0) + ? WHERE user_id = ?',
                                (cost, user_id))
                conn.commit()
        except Exception:
            logger.error('release_search_reservation failed for user %s', user_id, exc_info=True)
            return False

        # فقط پس از بازپرداخت موفق، رزرو از حافظه پاک می‌شود.
        search_reservations.pop(user_id, None)
        return True

@bot.callback_query_handler(func=lambda call: call.data == 'buy_extra_chats')
def process_buy_extra_chats(call):
    """خرید ۵ اعتبار چت با ۵ سکه، فقط از پنل خصوصی معتبر کاربر."""
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None)
    sender = getattr(message, 'from_user', None)

    # callback باید از پیام خصوصی خود کاربر و از پیام ارسال‌شده توسط ربات باشد.
    if (
        not isinstance(user_id, int)
        or user_id <= 0
        or chat is None
        or getattr(chat, 'type', None) != 'private'
        or getattr(chat, 'id', None) != user_id
        or sender is None
        or not getattr(sender, 'is_bot', False)
    ):
        try:
            bot.answer_callback_query(
                call.id,
                '🚫 این دکمه فقط از پنل خصوصی خودتان قابل استفاده است.',
                show_alert=True
            )
        except Exception:
            pass
        return

    # اطمینان از اینکه دکمه واقعاً در صفحه خرید اعتبار چت قرار داشته است.
    reply_markup = getattr(message, 'reply_markup', None)
    rows = (
        getattr(reply_markup, 'keyboard', None)
        or getattr(reply_markup, 'inline_keyboard', None)
        or []
    )
    has_purchase_button = any(
        getattr(button, 'callback_data', None) == 'buy_extra_chats'
        for row in rows
        for button in row
    )
    if not has_purchase_button:
        bot.answer_callback_query(
            call.id,
            '⚠️ این دکمه منقضی شده یا متعلق به پنل خرید اعتبار نیست.',
            show_alert=True
        )
        return

    try:
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('BEGIN IMMEDIATE')

            cur.execute('SELECT 1 FROM users WHERE user_id = ?', (user_id,))
            if cur.fetchone() is None:
                conn.rollback()
                bot.answer_callback_query(
                    call.id,
                    '❌ حساب کاربری پیدا نشد.',
                    show_alert=True
                )
                return

            # کسر سکه و افزودن اعتبار در یک تراکنش واحد انجام می‌شود.
            cur.execute(
                '''UPDATE users
                   SET coins = COALESCE(coins, 0) - 5,
                       extra_chat_credits = COALESCE(extra_chat_credits, 0) + 5
                   WHERE user_id = ? AND COALESCE(coins, 0) >= 5''',
                (user_id,)
            )
            if cur.rowcount != 1:
                conn.rollback()
                bot.answer_callback_query(
                    call.id,
                    '❌ موجودی سکه کافی نیست؛ برای خرید به حداقل ۵ سکه نیاز دارید.',
                    show_alert=True
                )
                return

            conn.commit()

    except Exception:
        logger.error(
            'buy_extra_chats transaction failed for user=%s',
            user_id,
            exc_info=True
        )
        try:
            bot.answer_callback_query(
                call.id,
                '❌ خرید انجام نشد؛ هیچ اعتباری اضافه نشده است. لطفاً دوباره تلاش کنید.',
                show_alert=True
            )
        except Exception:
            pass
        return

    try:
        bot.answer_callback_query(
            call.id,
            '✅ ۵ اعتبار چت با موفقیت اضافه شد و ۵ سکه کسر شد.',
            show_alert=True
        )
    except Exception:
        logger.warning('Could not answer buy_extra_chats callback for user=%s', user_id)

    # نمایش دوباره پنل، موجودی و اعتبار به‌روز را از دیتابیس می‌خواند.
    try:
        back_to_levelup_callback(call)
    except Exception:
        logger.error('Could not refresh level-up panel after purchase user=%s', user_id, exc_info=True)
        try:
            bot.send_message(
                user_id,
                '✅ خرید ثبت شد؛ ۵ اعتبار چت به حسابتان اضافه شده است. برای دیدن موجودی به بخش سکه برگردید.'
            )
        except Exception:
            pass

def check_daily_reset(user_id):
    today = datetime.now().strftime('%Y-%m-%d')
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')
        _reset_daily_counters_locked(cur, user_id, today)
        conn.commit()

LEVEL_PERKS = {
    0: {"max_chats": 10, "name": "تازه‌‌وارد 🐣", "next": "تکمیل پروفایل (عکس + بیو)"},
    1: {"max_chats": 15, "name": "تایید شده 👤", "next": "۲ دعوت موفق یا ۱۰ کاریزما"},
    2: {"max_chats": 20, "name": "فعال ⚡️", "next": "۵ دعوت موفق یا ۳۰ کاریزما"},
    3: {"max_chats": 30, "name": "محبوب 🌟", "next": "۱۰ دعوت موفق یا ۱۰۰ کاریزما"},
    4: {"max_chats": 999, "name": "پیشکسوت 👑", "next": "شما در بالاترین سطح هستید!"}
}

def _render_level_up_panel(user_id, notify_level_up=False):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute(
            '''SELECT COALESCE(level, 0), photo_file_id, bio, COALESCE(daily_chats_used, 0),
                      COALESCE(referrals_count, 0), COALESCE(coins, 0), COALESCE(karma, 0)
               FROM users WHERE user_id = ?''',
            (user_id,)
        )
        user_data = cur.fetchone()

    if not user_data:
        return None, None

    level, photo, bio, chats_used, refs, coins, karma = user_data

    new_level = 0
    if photo and bio:
        new_level = 1
        if refs >= 2 or karma >= 10:
            new_level = 2
            if refs >= 5 or karma >= 30:
                new_level = 3
                if refs >= 10 or karma >= 100:
                    new_level = 4

    if new_level > level:
        level = new_level
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('UPDATE users SET level = ? WHERE user_id = ?', (level, user_id))
            conn.commit()
        if notify_level_up:
            bot.send_message(user_id, f"🎉 <b>تبریک! شما به Level {level} ارتقا یافتید!</b>", parse_mode='HTML')

    current_perk = LEVEL_PERKS.get(level, LEVEL_PERKS[0])
    max_chats = current_perk["max_chats"]
    rem_chats = max(0, max_chats - chats_used)
    chat_display = "نامحدود ♾" if max_chats == 999 else f"مانده {rem_chats} از {max_chats}"

    text = (
        "🪙 <b>| پنل مدیریت سکه و سطح‌بندی</b>\n"
        "━━━━━━━━━━━━━━━━━━━\n"
        "💬 <i>سطح شما با فعالیت مثبت (کاریزما) یا دعوت دوستان رشد می‌کند.</i>\n\n"
        f"🏅 <b>سطح فعلی:</b> <code>Level {level} ({current_perk['name']})</code>\n"
        f"💰 <b>موجودی سکه:</b> <code>{coins} سکه 🪙</code>\n"
        f"✨ <b>امتیاز کاریزما:</b> <code>{karma}</code>\n"
        f"👥 <b>دعوت‌های موفق:</b> <code>{refs} نفر</code>\n\n"
        f"📌 <b>ماموریت سطح بعدی:</b>\n"
        f"🔹 {current_perk['next']}\n\n"
        "📊 <b>سقف‌های مصرفی امروز:</b>\n"
        f"  ▫️ <b>درخواست چت:</b> <code>{chat_display}</code>\n"
        "━━━━━━━━━━━━━━━━━━━"
    )

    markup = types.InlineKeyboardMarkup(row_width=1)
    markup.add(
        types.InlineKeyboardButton("🛒 فروشگاه سکه", callback_data="store_select_method"),
        types.InlineKeyboardButton("🎁 دریافت جایزه روزانه", callback_data="daily_reward"),
        types.InlineKeyboardButton("🪙 خرید ۵ چت اضافه (۵ سکه)", callback_data="buy_extra_chats"),
        types.InlineKeyboardButton("دعوت دوستان 👥", callback_data="invite_friends"),
        types.InlineKeyboardButton("جدول Level ها 📊", callback_data="levels_info")
    )
    return text, markup

@bot.message_handler(func=lambda message: message.text in ['سکه 🪙', 'Level Up 🚀'])
def show_level_up_menu(message):
    user_id = message.from_user.id
    update_last_seen(user_id)
    check_daily_reset(user_id)

    if is_user_in_chat(user_id):
        bot.send_message(user_id, "⚠️ شما در حال چت هستید! ابتدا چت را پایان دهید.")
        return

    text, markup = _render_level_up_panel(user_id, notify_level_up=True)
    if not text:
        return

    if hasattr(message, 'message_id'):
        try: bot.edit_message_text(text, message.chat.id, message.message_id, reply_markup=markup, parse_mode='HTML')
        except Exception: bot.send_message(message.chat.id, text, reply_markup=markup, parse_mode='HTML')
    else:
        bot.send_message(message.chat.id, text, reply_markup=markup, parse_mode='HTML')


@bot.callback_query_handler(func=lambda call: call.data == 'levels_info')
def levels_info_callback(call):
    user_id = call.from_user.id
    update_last_seen(user_id)
    
    text = (
        "📈 <b>راهنما و جدول سطوح (Levels)</b>\n\n"
        "در این سیستم جدید، شما می‌تونید هم با دعوت دوستان و هم با خوش‌اخلاقی در چت (گرفتن کاریزما) لول آپ کنید:\n\n"
        "🔹 <b>Level 0 (تازه‌وارد):</b> ۱۰ چت در روز\n"
        "🔹 <b>Level 1 (تایید شده):</b> ۱۵ چت در روز\n"
        "  └ <i>شرط ارتقا: تکمیل اطلاعات پروفایل (عکس + بیو)</i>\n\n"
        "🔹 <b>Level 2 (فعال ⚡️):</b> ۲۰ چت در روز\n"
        "  └ <i>شرط ارتقا: ۲ دعوت موفق یا گرفتن ۱۰ امتیاز کاریزما</i>\n\n"
        "🔹 <b>Level 3 (محبوب 🌟):</b> ۳۰ چت در روز\n"
        "  └ <i>شرط ارتقا: ۵ دعوت موفق یا گرفتن ۳۰ امتیاز کاریزما</i>\n\n"
        "🔹 <b>Level 4 (پیشکسوت 👑):</b> چت نامحدود!\n"
        "  └ <i>شرط ارتقا: ۱۰ دعوت موفق یا گرفتن ۱۰۰ امتیاز کاریزما</i>\n\n"
        "💡 <i>نکته: با زدن دکمه جایزه روزانه، هر روز سکه رایگان می‌گیرید!</i>"
    )

    markup = types.InlineKeyboardMarkup(row_width=1)
    markup.add(
        types.InlineKeyboardButton("🔙 بازگشت به منوی Level Up", callback_data="back_to_levelup")
    )

    try: bot.edit_message_text(text, call.message.chat.id, call.message.message_id, reply_markup=markup, parse_mode='HTML')
    except: bot.send_message(call.message.chat.id, text, reply_markup=markup, parse_mode='HTML')

@bot.callback_query_handler(func=lambda call: call.data == 'invite_friends')
def invite_friends_callback(call):
    user_id = call.from_user.id
    update_last_seen(user_id)
    
    bot_username = get_bot_username()
    user_link = f"https://t.me/{bot_username}?start=ref_{user_id}"
    
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT referrals_count, coins FROM users WHERE user_id = ?', (user_id,))
        row = cur.fetchone()
        refs = row[0] if row else 0
        coins = row[1] if row else 0

    share_text = "سلام! از طریق لینک زیر می‌تونی وارد ربات چت ناشناس بشی و کلی دوست جدید پیدا کنی 👇\n" + user_link

    markup = types.InlineKeyboardMarkup(row_width=1)
    markup.add(
        types.InlineKeyboardButton("اشتراک‌گذاری دعوت‌نامه 📤", switch_inline_query=share_text),
        types.InlineKeyboardButton("🔙 بازگشت به منوی Level Up", callback_data="back_to_levelup")
    )

    text = (
        "👥 <b>سیستم دعوت از دوستان (رفرال)</b>\n\n"
        "🔗 با ارسال لینک اختصاصی خود به دوستانتان، به ازای هر نفری که عضو کانال‌ها شده و ثبت‌نام خود را در ربات کامل کند، <b>۱۰ سکه پاداش</b> بگیرید!\n\n"
        "📊 <b>آمار شما:</b>\n"
        f"• تعداد دعوت‌های موفق: <code>{refs} نفر</code>\n"
        f"• موجودی سکه شما: <code>{coins} سکه</code>\n\n"
        "📎 <b>لینک اختصاصی دعوت شما:</b>\n"
        f"<code>{user_link}</code>"
    )

    try:
        bot.edit_message_text(text, call.message.chat.id, call.message.message_id, reply_markup=markup, parse_mode='HTML')
    except Exception:
        bot.send_message(call.message.chat.id, text, reply_markup=markup, parse_mode='HTML')

@bot.callback_query_handler(func=lambda call: call.data == 'back_to_levelup')
def back_to_levelup_callback(call):
    user_id = call.from_user.id
    update_last_seen(user_id)
    check_daily_reset(user_id)

    text, markup = _render_level_up_panel(user_id, notify_level_up=False)
    if not text:
        return

    try: bot.edit_message_text(text, call.message.chat.id, call.message.message_id, reply_markup=markup, parse_mode='HTML')
    except Exception: bot.answer_callback_query(call.id)

def show_main_menu(chat_id, is_admin=False):
    markup = types.ReplyKeyboardMarkup(resize_keyboard=True, row_width=2)
    btn_start = types.KeyboardButton('شروع چت ناشناس 🎭')
    # WebApp button opens the profile editor UI. Ensure WEBAPP_PUBLIC_URL env var is set to the HTTPS URL serving `webapp_profile.html`.
    webapp_url = os.getenv('WEBAPP_PUBLIC_URL')
    if not webapp_url:
        logger.warning('WEBAPP_PUBLIC_URL not set; profile button will open default reply keyboard')
        btn_profile = types.KeyboardButton('پروفایل من 👤')
    else:
        btn_profile = types.KeyboardButton('پروفایل من 👤', web_app=types.WebAppInfo(url=webapp_url))
    btn_friends = types.KeyboardButton('دوستان من 🤝')
    btn_link = types.KeyboardButton('🔗 لینک ناشناس من')
    btn_level = types.KeyboardButton('سکه 🪙')
    btn_support = types.KeyboardButton('پشتیبانی 🎧')
    
    markup.add(btn_start)
    markup.add(btn_profile, btn_friends)
    markup.add(btn_level, btn_link)
    
    if chat_id in ADMINS or is_admin:
        btn_admin = types.KeyboardButton('پنل مدیریت 👨‍💻')
        markup.add(btn_support, btn_admin)
    else:
        markup.add(btn_support)
        
    welcome_text = (
        "🎭 <b>به دنیای چت ناشناس خوش آمدید!</b>\n\n"
        "هویت شما در اینجا کاملاً مخفی و امن است 🥷\n"
        "هیچ‌کس نمی‌داند شما چه کسی هستید، مگر اینکه خودتان بخواهید!\n\n"
        "👇 <i>برای شروع یک مکالمه جدید، از دکمه‌های پایین استفاده کنید:</i>"
    )
        
    bot.send_message(chat_id, welcome_text, reply_markup=markup, parse_mode='HTML')

def admin_only(func):
    @functools.wraps(func)
    def wrapper(obj, *args, **kwargs):
        user = getattr(obj, 'from_user', None)
        if user is None or user.id not in ADMINS:
            logger.warning(f"admin_only denied: user={getattr(user, 'id', None)} func={func.__name__}")
            if isinstance(obj, types.CallbackQuery):
                try: bot.answer_callback_query(obj.id, "⛔ شما دسترسی ادمین ندارید.", show_alert=True)
                except Exception: pass
            return
        return func(obj, *args, **kwargs)
    return wrapper

def start_questionnaire(message):
    user_id = message.from_user.id
    # استفاده از escape_html برای جلوگیری از باگ کاراکترهای خاص در نام کاربر
    first_name = escape_html(message.from_user.first_name or "کاربر")
    update_last_seen(user_id)
    
    # 🌟 ترکیب پیام خوش‌آمدگویی با سوال اول (سن) برای پاک شدن خودکار
    welcome_text = (
        f"سلام {first_name} عزیز ✋\n\n"
        "🎭 <b>به دنیای چت ناشناس خوش آمدید!</b>\n\n"
        "هویت شما در اینجا کاملاً مخفی و امن است 🥷\n"
        "هیچ‌کس نمی‌داند شما چه کسی هستید، مگر اینکه خودتان بخواهید!\n\n"
        "👇 <b>اما قبل از شروع، باید پروفایل ناشناست رو بسازیم!</b>\n"
        "لطفاً سن خودت رو انتخاب کن:"
    )
    
    # شروع روند ثبت‌نام (مرحله اول: گرفتن سن)
    markup = types.InlineKeyboardMarkup(row_width=5)
    buttons = [types.InlineKeyboardButton(str(age), callback_data=f"regage_{age}_{user_id}") for age in range(12, 41)]
    markup.add(*buttons)
    
    # ارسال یکپارچه متن و دکمه (این پیام پس از انتخاب سن کاملاً محو خواهد شد)
    bot.send_message(user_id, welcome_text, reply_markup=markup, parse_mode='HTML')

def handle_private_link(message):
    """Validate a private invite and serialize its connection notices with chat termination."""
    user_id = message.from_user.id
    update_last_seen(user_id)
    message_text = (getattr(message, 'text', None) or '').strip()

    if is_blocked(user_id):
        bot.send_message(user_id, '🚫 حساب شما مسدود است.')
        return False

    parts = message_text.split(maxsplit=1)
    if len(parts) != 2:
        bot.send_message(user_id, '❌ لینک دعوت نامعتبر است.')
        return False

    command = parts[0].split('@', 1)[0].lower()
    if command != '/start':
        bot.send_message(user_id, '❌ لینک دعوت نامعتبر است.')
        return False

    link_token = parts[1].strip()
    if not link_token.startswith('chat_') or len(link_token) < 20 or len(link_token) > 40:
        bot.send_message(user_id, '❌ ساختار لینک دعوت معتبر نیست.')
        return False

    # عضویت را حتی در مسیر بازیابی لینک دوباره کنترل می‌کنیم.
    membership_state = check_channels_membership(user_id)
    if membership_state is None:
        _store_pending_start_link(user_id, message_text)
        bot.send_message(user_id, '⚠️ فعلاً بررسی عضویت ممکن نیست؛ بعداً دوباره /start را بزنید.')
        return True
    if membership_state is False:
        _store_pending_start_link(user_id, message_text)
        show_channel_join_message(user_id)
        return True

    my_user = get_user(user_id)
    if not my_user:
        bot.send_message(user_id, '❌ حساب کاربری پیدا نشد؛ دوباره /start را بزنید.')
        return False

    if not is_profile_complete(user_id):
        _store_pending_start_link(user_id, message_text)
        start_questionnaire(message)
        return True

    try:
        target_user = get_user_by_link(link_token)
        if not target_user:
            bot.send_message(user_id, '❌ این لینک نامعتبر یا باطل شده است.')
            return False

        target_user_id = int(target_user[0])
        if target_user_id == user_id:
            bot.send_message(user_id, '❌ نمی‌توانید با لینک خودتان چت کنید.')
            return False
        if is_blocked(target_user_id) or are_users_blocked_mutually(user_id, target_user_id):
            bot.send_message(user_id, '❌ ارتباط با این کاربر ممکن نیست.')
            return False
        if not is_profile_complete(target_user_id):
            bot.send_message(user_id, '⚠️ پروفایل مخاطب هنوز کامل نیست.')
            return False

        try:
            my_age = int(my_user[6])
            target_age = int(target_user[6])
        except (TypeError, ValueError):
            bot.send_message(user_id, '🔒 سن پروفایل یکی از کاربران معتبر نیست؛ امکان اتصال وجود ندارد.')
            return False

        # رده‌بندی سنی مبتنی بر اطلاعات پروفایل است و احراز سن واقعی محسوب نمی‌شود.
        if (my_age >= 18) != (target_age >= 18):
            bot.send_message(user_id, '🔒 امکان اتصال این دو رده سنی وجود ندارد.')
            return False

        search_messages_to_delete = []
        with queue_lock:
            # بررسی وضعیت و ساخت Pair در یک بخش اتمیک انجام می‌شود.
            if user_id in active_pairs:
                already_in_chat = True
                target_is_busy = False
                chat_id = None
            elif target_user_id in active_pairs:
                already_in_chat = False
                target_is_busy = True
                chat_id = None
            else:
                already_in_chat = False
                target_is_busy = False

                # حذف از صف بدون انجام درخواست شبکه‌ای زیر queue_lock.
                global waiting_pool
                waiting_pool = deque(
                    item for item in waiting_pool
                    if item['user_id'] not in (user_id, target_user_id)
                )
                user_queue_times.pop(user_id, None)
                user_queue_times.pop(target_user_id, None)

                for uid in (user_id, target_user_id):
                    search_message_id = search_message_ids.pop(uid, None)
                    if search_message_id:
                        search_messages_to_delete.append((uid, search_message_id))

                # رزروهای قدیمی پیش از ثبت Pair آزاد می‌شوند.
                release_search_reservation(user_id)
                release_search_reservation(target_user_id)

                chat_id = start_chat(user_id, target_user_id, via_link=True)
                user_pair = (target_user_id, chat_id, True)
                target_pair = (user_id, chat_id, True)
                active_pairs[user_id] = user_pair
                active_pairs[target_user_id] = target_pair
                # Mapping state must exist before another lifecycle operation can end this Pair.
                with mapping_lock:
                    message_mapping[user_id] = {}
                    message_mapping[target_user_id] = {}

        if already_in_chat:
            bot.send_message(user_id, '⚠️ شما در حال حاضر در یک چت هستید.')
            return False
        if target_is_busy:
            bot.send_message(user_id, '⚠️ این کاربر هم‌اکنون در چت دیگری است.')
            return False

        # حذف پیام‌های جستجو بیرون از قفل مرکزی صف.
        for target_uid, search_message_id in search_messages_to_delete:
            try:
                bot.delete_message(target_uid, search_message_id)
            except Exception:
                pass

        chat_markup = types.ReplyKeyboardMarkup(resize_keyboard=True, row_width=2)
        chat_markup.add(
            types.KeyboardButton('مشاهده پروفایل مخاطب 👤'),
            types.KeyboardButton('ارسال آیدی من👻')
        )
        chat_markup.row(
            types.KeyboardButton('حالت چت امن 🔒'),
            types.KeyboardButton('🎲 سوال تصادفی')
        )
        chat_markup.add(
            types.KeyboardButton('نفر بعدی ⏭'),
            types.KeyboardButton('پایان چت ❌')
        )

        target_name = escape_html(target_user[4] or 'کاربر ناشناس')
        first_notice = None
        second_notice = None
        lifecycle_lock = chat_lifecycle_locks[int(chat_id) % len(chat_lifecycle_locks)]

        def pair_is_current():
            with queue_lock:
                return (
                    active_pairs.get(user_id) == user_pair
                    and active_pairs.get(target_user_id) == target_pair
                )

        with lifecycle_lock:
            # ممکن است چت در فاصله ساخت Pair تا گرفتن lifecycle lock پایان یافته باشد.
            if not pair_is_current():
                logger.info(
                    'private-link notice skipped because chat is no longer active chat=%s users=%s,%s',
                    chat_id, user_id, target_user_id
                )
                return False

            try:
                first_notice = bot.send_message(
                    user_id,
                    f'✅ با لینک اختصاصی به {target_name} متصل شدید!\nاکنون می‌توانید گفت‌وگو کنید.',
                    reply_markup=chat_markup,
                    parse_mode='HTML'
                )
                second_notice = bot.send_message(
                    target_user_id,
                    '🎉 یک نفر با لینک اختصاصی شما وارد چت ناشناس شد!\nاکنون می‌توانید گفت‌وگو کنید.',
                    reply_markup=chat_markup
                )
            except Exception:
                logger.error('private-link delivery failed; rolling back pair', exc_info=True)

                # اگر پیام اول ارسال شده ولی دومی شکست خورده، اعلان ناقص را پاک کن.
                if first_notice is not None:
                    try:
                        bot.delete_message(user_id, first_notice.message_id)
                    except Exception:
                        logger.warning(
                            'could not remove partial private-link notice user=%s chat=%s',
                            user_id, chat_id,
                            exc_info=True
                        )
                if second_notice is not None:
                    try:
                        bot.delete_message(target_user_id, second_notice.message_id)
                    except Exception:
                        logger.warning(
                            'could not remove partial private-link notice user=%s chat=%s',
                            target_user_id, chat_id,
                            exc_info=True
                        )

                # فقط Pair دقیق ساخته‌شده توسط همین عملیات را rollback کن.
                if pair_is_current():
                    terminate_chat_session(chat_id, user_id, target_user_id, send_feedback=False)

                for uid in (user_id, target_user_id):
                    try:
                        bot.send_message(
                            uid,
                            '❌ اتصال برقرار نشد؛ لطفاً دوباره تلاش کنید.',
                            reply_markup=types.ReplyKeyboardRemove()
                        )
                        show_main_menu(uid, uid in ADMINS)
                    except Exception:
                        logger.warning(
                            'could not show main menu after private-link rollback user=%s',
                            uid,
                            exc_info=True
                        )
                return False

        # لینک معلق یک‌بارمصرف است و بعد از موفقیت دیگر نباید دوباره اجرا شود.
        _pop_pending_start_link(user_id)
        return True

    except Exception:
        logger.error('handle_private_link failed for user=%s', user_id, exc_info=True)
        try:
            bot.send_message(user_id, '❌ خطایی در پردازش لینک رخ داد؛ لطفاً دوباره تلاش کنید.')
        except Exception:
            pass
        return False

@bot.message_handler(commands=['start', 'cancel'])
def handle_start(message):
    user = message.from_user
    user_id = user.id
    message_text = (message.text or '').strip()

    # تشخیص دستور؛ /cancel@BotName نیز پشتیبانی می‌شود.
    command = message_text.split(maxsplit=1)[0].split('@', 1)[0].lower() if message_text else ''

    is_new_user = get_user(user_id) is None
    add_user(user)
    update_last_seen(user_id)

    if is_blocked(user_id):
        bot.send_message(message.chat.id, "متاسفانه حساب شما مسدود شده است.")
        return

    if command == '/cancel':
        handle_cancel(message)
        return

    # استخراج پارامتر /start و تفکیک رفرال از لینک چت ناشناس
    parts = message_text.split(maxsplit=1)
    payload = parts[1].strip() if len(parts) > 1 else None
    link_message_text = None

    if payload:
        if payload.startswith('ref_'):
            # ثبت رفرال فقط برای کاربر تازه‌وارد و شناسه عددی معتبر
            if is_new_user:
                inviter_text = payload[4:]
                if inviter_text.isdigit():
                    inviter_id = int(inviter_text)
                    if inviter_id > 0 and inviter_id != user_id:
                        try:
                            with get_db_connection() as conn:
                                cur = conn.cursor()
                                cur.execute(
                                    'UPDATE users SET invited_by = ? WHERE user_id = ?',
                                    (inviter_id, user_id)
                                )
                                conn.commit()
                        except Exception:
                            logger.error(
                                'Could not save referral for user %s',
                                user_id,
                                exc_info=True
                            )
        else:
            # handle_private_link به متن کامل /start <payload> نیاز دارد.
            link_message_text = message_text

    # بررسی عضویت اجباری قبل از پردازش لینک یا نمایش منو
    membership_state = check_channels_membership(user_id)

    # خطای موقت API را با «عضو نیست» اشتباه نگیریم.
    if membership_state is None:
        bot.send_message(
            message.chat.id,
            '⚠️ در حال حاضر امکان بررسی عضویت در کانال‌ها وجود ندارد. '
            'لطفاً چند لحظه بعد همان دستور /start یا لینک دعوت را دوباره ارسال کنید.'
        )
        return

    if membership_state is False:
        # لینک چت را تا زمان تأیید عضویت نگه می‌داریم.
        if link_message_text:
            _store_pending_start_link(user_id, link_message_text)

        show_channel_join_message(message.chat.id)
        return

    # عضویت تأیید شده است؛ حالا لینک چت یا روند عادی را اجرا کن.
    if link_message_text:
        handle_private_link(message)
        return

    if not is_profile_complete(user_id):
        start_questionnaire(message)
        return

    show_main_menu(message.chat.id, user_id in ADMINS)

@bot.callback_query_handler(func=lambda call: call.data.startswith('regage_'))
def process_age_inline(call):
    parts = call.data.split('_')
    age = int(parts[1])
    user_id = int(parts[2])
    
    if call.from_user.id != user_id:
        bot.answer_callback_query(call.id, "این دکمه برای شما نیست ❌", show_alert=True)
        return
        
    update_last_seen(user_id)
    update_age(user_id, age)
    bot.delete_message(call.message.chat.id, call.message.message_id)
    
    # دکمه‌های تحصیلات در ردیف‌های ۲ تایی
    markup = types.InlineKeyboardMarkup(row_width=2)
    edus = ['زیر دیپلم', 'دیپلم', 'فوق دیپلم', 'لیسانس', 'فوق لیسانس', 'دکترا']
    buttons = [types.InlineKeyboardButton(e, callback_data=f"regedu_{edus.index(e)}_{user_id}") for e in edus]
    markup.add(*buttons)
    bot.send_message(user_id, "🎓 میزان تحصیلات خود را انتخاب کنید:", reply_markup=markup)

@bot.callback_query_handler(func=lambda call: call.data.startswith('regedu_'))
def process_edu_inline(call):
    parts = call.data.split('_')
    edus = ['زیر دیپلم', 'دیپلم', 'فوق دیپلم', 'لیسانس', 'فوق لیسانس', 'دکترا']
    edu = edus[int(parts[1])]
    user_id = int(parts[2])
    
    if call.from_user.id != user_id:
        bot.answer_callback_query(call.id, "این دکمه برای شما نیست ❌", show_alert=True)
        return
        
    update_last_seen(user_id)
    update_education(user_id, edu)
    bot.delete_message(call.message.chat.id, call.message.message_id)
    
    # دکمه‌های استان در ردیف‌های ۳ تایی
    markup = types.InlineKeyboardMarkup(row_width=3)
    buttons = [types.InlineKeyboardButton(p, callback_data=f"regloc_{provinces.index(p)}_{user_id}") for p in provinces]
    markup.add(*buttons)
    bot.send_message(user_id, "📍 استان خود را انتخاب کنید:", reply_markup=markup)

@bot.callback_query_handler(func=lambda call: call.data.startswith('regloc_'))
def process_loc_inline(call):
    parts = call.data.split('_')
    prov_index = int(parts[1])
    province = provinces[prov_index]
    user_id = int(parts[2])
    
    if call.from_user.id != user_id:
        bot.answer_callback_query(call.id, "این دکمه برای شما نیست ❌", show_alert=True)
        return
        
    update_last_seen(user_id)
    bot.delete_message(call.message.chat.id, call.message.message_id)
    
    # دکمه‌های شهر (مخصوص استان انتخاب شده)
    markup = types.InlineKeyboardMarkup(row_width=3)
    cities = iran_cities.get(province, ['مرکز استان', 'سایر'])
    buttons = [types.InlineKeyboardButton(c, callback_data=f"regcity_{prov_index}_{cities.index(c)}_{user_id}") for c in cities]
    markup.add(*buttons)
    bot.send_message(user_id, f"🏙 لطفاً شهر خود را در استان {province} انتخاب کنید:", reply_markup=markup)

@bot.callback_query_handler(func=lambda call: call.data.startswith('regcity_'))
def process_city_inline(call):
    parts = call.data.split('_')
    prov_index = int(parts[1])
    city_index = int(parts[2])
    user_id = int(parts[3])
    
    if call.from_user.id != user_id:
        bot.answer_callback_query(call.id, "این دکمه برای شما نیست ❌", show_alert=True)
        return
        
    province = provinces[prov_index]
    cities = iran_cities.get(province, ['مرکز استان', 'سایر'])
    city = cities[city_index]
    
    full_location = f"{province} - {city}"
    
    update_last_seen(user_id)
    update_location(user_id, full_location)
    bot.delete_message(call.message.chat.id, call.message.message_id)
    
    # 🌟 تولید خودکار دکمه‌های شیشه‌ای برای تمام گرایش‌ها و جنسیت‌ها (در ردیف‌های ۲ تایی)
    markup = types.InlineKeyboardMarkup(row_width=2)
    buttons = [types.InlineKeyboardButton(g_val[0], callback_data=f"gender_{g_key}_{user_id}") for g_key, g_val in GENDER_MAP.items()]
    markup.add(*buttons)
    bot.send_message(user_id, "🚻 لطفاً جنسیت/گرایش خود را با دقت انتخاب کنید:", reply_markup=markup)


# --- توابع مربوط به سیستم Level Up و محدودیت‌های روزانه ---


@bot.callback_query_handler(func=lambda call: call.data.startswith('gender_'))
def process_gender(call):
    parts = call.data.split('_')
    gender = parts[1]
    user_id = int(parts[2])
    
    if call.from_user.id != user_id:
        bot.answer_callback_query(call.id, "این دکمه برای شما نیست ❌", show_alert=True)
        return
        
    update_gender(user_id, gender)
    update_last_seen(user_id)
    bot.delete_message(call.message.chat.id, call.message.message_id)
    
    markup = types.InlineKeyboardMarkup(row_width=3)
    markup.add(
        types.InlineKeyboardButton("تاپ 🔝", callback_data=f"regpos_top_{user_id}"),
        types.InlineKeyboardButton("ورس ↔️", callback_data=f"regpos_vers_{user_id}"),
        types.InlineKeyboardButton("بات 🔽", callback_data=f"regpos_bottom_{user_id}")
    )
    markup.add(types.InlineKeyboardButton("ترجیح می‌دهم نگویم 👤", callback_data=f"regpos_unknown_{user_id}"))
    
    bot.send_message(user_id, "🎭 لطفاً گرایش نقشی (پوز) خود را انتخاب کنید:", reply_markup=markup)

@bot.callback_query_handler(func=lambda call: bool(call.data) and call.data.startswith('regpos_'))   
def process_position_reg(call):
    try:
        parts = call.data.split('_')
        if len(parts) != 3 or parts[0] != 'regpos':
            raise ValueError('invalid callback')
        pos = parts[1]
        user_id = int(parts[2])
    except (TypeError, ValueError, IndexError):
        bot.answer_callback_query(call.id, '❌ درخواست ثبت‌نام نامعتبر است.', show_alert=True)
        return

    if call.from_user.id != user_id:
        bot.answer_callback_query(call.id, '❌ این دکمه متعلق به شما نیست.', show_alert=True)
        return
    if pos not in POS_MAP:
        bot.answer_callback_query(call.id, '❌ گزینه انتخاب‌شده معتبر نیست.', show_alert=True)
        return

    inviter_id = None
    referral_rewarded = False
    try:
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('BEGIN IMMEDIATE')
            cur.execute('SELECT position, invited_by FROM users WHERE user_id = ?', (user_id,))
            row = cur.fetchone()
            if not row:
                conn.rollback()
                bot.answer_callback_query(call.id, '❌ حساب کاربری پیدا نشد.', show_alert=True)
                return

            old_position, inviter_id = row
            cur.execute('UPDATE users SET position = ? WHERE user_id = ?', (pos, user_id))
            if cur.rowcount != 1:
                conn.rollback()
                bot.answer_callback_query(call.id, '❌ ذخیره گزینه انتخابی ناموفق بود.', show_alert=True)
                return

            # فقط اولین تکمیل مرحله position پاداش رفرال ایجاد می‌کند.
            if old_position is None and inviter_id and int(inviter_id) != user_id:
                cur.execute(
                    '''UPDATE users
                       SET coins = COALESCE(coins, 0) + 10,
                           referrals_count = COALESCE(referrals_count, 0) + 1
                       WHERE user_id = ?''',
                    (int(inviter_id),)
                )
                referral_rewarded = cur.rowcount == 1

            conn.commit()
    except Exception:
        logger.error('process_position_reg transaction failed user=%s', user_id, exc_info=True)
        bot.answer_callback_query(call.id, '❌ خطا در تکمیل ثبت‌نام.', show_alert=True)
        return

    update_last_seen(user_id)
    try:
        bot.delete_message(call.message.chat.id, call.message.message_id)
    except Exception:
        pass
    bot.answer_callback_query(call.id, '✅ ثبت‌نام با موفقیت تکمیل شد!')

    if referral_rewarded:
        try:
            new_user_name = escape_html(call.from_user.first_name or 'کاربر')
            bot.send_message(
                int(inviter_id),
                f'🎉 <b>تبریک!</b>\nیک کاربر جدید ({new_user_name}) ثبت‌نامش را کامل کرد.\n'
                '🎁 <b>۱۰ سکه</b> پاداش دعوت به حسابت واریز شد.',
                parse_mode='HTML'
            )
        except Exception:
            logger.warning('Referral reward notification failed inviter=%s', inviter_id, exc_info=True)

    pending_text = _pop_pending_start_link(user_id)
    if pending_text:
        handled = handle_private_link(SimpleNamespace(from_user=call.from_user, text=pending_text))
        if handled:
            return

    show_main_menu(user_id, user_id in ADMINS)

    # پیشنهاد تکمیل پروفایل؛ مانند رفتار قبلی حفظ شده است.
    try:
        with get_db_connection() as conn:
            row = conn.execute(
                'SELECT profile_reward_claimed, photo_file_id, bio FROM users WHERE user_id = ?',
                (user_id,)
            ).fetchone()
        if row and row[0] == 0 and (not row[1] or not row[2]):
            markup = types.InlineKeyboardMarkup()
            markup.add(types.InlineKeyboardButton('🎁 تکمیل پروفایل (۱۵ سکه)', callback_data='show_my_profile_btn'))
            promo_text = (
                '🎉 <b>ثبت‌نام اولیه شما کامل شد!</b>\n\n'
                '🎁 اگر عکس و بیوگرافی پروفایلت را کامل کنی، <b>۱۵ سکه رایگان</b> دریافت می‌کنی.\n\n'
                '👇 برای دریافت جایزه روی دکمه زیر بزن:'
            )
            bot.send_message(user_id, promo_text, reply_markup=markup, parse_mode='HTML')
    except Exception:
        logger.warning('profile completion promo failed', exc_info=True)

# 🌟 هندلر دکمه تکمیل پروفایل
@bot.callback_query_handler(func=lambda call: call.data == 'show_my_profile_btn')
def show_my_profile_callback(call):
    bot.answer_callback_query(call.id)
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
    force_show_profile(call.from_user.id)


def show_channel_join_message(chat_id):
    required_channels = get_required_channels()
    markup = types.InlineKeyboardMarkup()
    for channel in required_channels:
        markup.add(types.InlineKeyboardButton(
            text=f"عضویت در {channel}",
            url=f"https://t.me/{channel[1:]}"
        ))
    markup.add(types.InlineKeyboardButton(
        text="بررسی عضویت",
        callback_data="check_membership"
    ))
    bot.send_message(
        chat_id,
        "برای استفاده از ربات باید در کانال‌های زیر عضو شوید:",
        reply_markup=markup
    )

@bot.callback_query_handler(func=lambda call: call.data == "check_membership")
def check_membership(call):
    user_id = call.from_user.id
    update_last_seen(user_id)

    member_state = check_channels_membership(user_id, force=True)
    if member_state is None:
        bot.answer_callback_query(call.id, '⚠️ بررسی عضویت موقتاً ممکن نیست؛ کمی بعد دوباره تلاش کنید.', show_alert=True)
        return
    if not member_state:
        bot.answer_callback_query(call.id, '❌ هنوز در همه کانال‌های لازم عضو نشده‌اید.', show_alert=True)
        return

    bot.answer_callback_query(call.id, '✅ عضویت شما تأیید شد.')
    try:
        bot.delete_message(call.message.chat.id, call.message.message_id)
    except Exception:
        pass

    if not is_profile_complete(user_id):
        # لینک معلق را نگه می‌داریم تا آخرین مرحله ثبت‌نام آن را بازیابی کند.
        start_questionnaire(SimpleNamespace(from_user=call.from_user))
        return

    pending_text = _pop_pending_start_link(user_id)
    if pending_text:
        handled = handle_private_link(SimpleNamespace(from_user=call.from_user, text=pending_text))
        if handled:
            return

    show_main_menu(call.message.chat.id, user_id in ADMINS)

def cleanup_secure_chat(chat_id, u1, u2):
    # ابتدا چت را از حالت امن خارج و همه پیام‌های آن را وارد صف حذف پایدار کن.
    with secure_tracker_lock:
        secure_chats.discard(chat_id)
        try:
            with get_db_connection() as conn:
                cur = conn.cursor()
                cur.execute(
                    '''UPDATE secure_chat_messages
                       SET delete_requested = 1, retry_at = 0
                       WHERE chat_id = ?''',
                    (int(chat_id),)
                )
                conn.commit()
        except Exception:
            logger.critical(
                'could not queue secure chat messages for deletion chat=%s',
                chat_id,
                exc_info=True
            )

        # snapshot آخرین ۵۰ شناسه برای fallback در صورت خطای ثبت SQLite
        fallback_tracker = chat_msg_tracker.get(chat_id, {})
        fallback_messages = {
            int(uid): list(messages)
            for uid, messages in fallback_tracker.items()
        }
        chat_msg_tracker.pop(chat_id, None)

    # تلاش فوری برای حذف همه پیام‌هایی که در SQLite ثبت شده‌اند.
    _process_secure_chat_message_deletions(chat_id=chat_id)

    # Fallback برای آخرین شناسه‌هایی که شاید ثبتشان در SQLite خطا داده باشد.
    # بعضی از این پیام‌ها ممکن است در مرحله قبل حذف شده باشند؛ خطای آن‌ها بی‌خطر است.
    for uid, messages in fallback_messages.items():
        unique_ids = list(dict.fromkeys(int(mid) for mid in messages if mid is not None))
        for start in range(0, len(unique_ids), 100):
            chunk = unique_ids[start:start + 100]
            if not chunk:
                continue
            try:
                bot.delete_messages(uid, chunk)
            except Exception:
                for message_id in chunk:
                    try:
                        bot.delete_message(uid, message_id)
                    except Exception:
                        pass

    # مدیای انتخاب‌نشده را پاک کن.
    with pending_secure_media_lock:
        keys_to_delete = []
        for key, data in list(pending_secure_media.items()):
            sender_id = key.split('_', 1)[0]
            if sender_id in (str(u1), str(u2)) or (len(data) >= 4 and data[3] == chat_id):
                keys_to_delete.append(key)
        for key in keys_to_delete:
            pending_secure_media.pop(key, None)

    # رکورد زمان‌بندی جداگانه مدیای زمان‌دار دیگر لازم نیست؛
    # شناسه پیام‌های آن نیز در secure_chat_messages ثبت شده است.
    try:
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute(
                'DELETE FROM secure_media_deletions WHERE chat_id = ?',
                (chat_id,)
            )
            conn.commit()
    except Exception:
        logger.error(
            'secure media deletion queue cleanup failed for chat %s',
            chat_id,
            exc_info=True
        )

@bot.message_handler(func=lambda message: message.text == 'حالت چت امن 🔒')
def activate_secure_mode(message):
    """فعال‌سازی حالت چت امن بدون نگه‌داشتن queue_lock هنگام درخواست شبکه."""
    user_id = message.from_user.id
    update_last_seen(user_id)

    # فقط وضعیت اولیه را زیر قفل صف می‌خوانیم؛ هیچ درخواست تلگرامی اینجا اجرا نمی‌شود.
    with queue_lock:
        initial_pair = active_pairs.get(user_id)

    if not initial_pair:
        try:
            bot.send_message(user_id, '⚠️ شما در حال حاضر در چتی نیستید.')
        except Exception:
            logger.warning('could not notify user about missing chat', exc_info=True)
        return

    partner_id, chat_id, _via_link = initial_pair
    lifecycle_lock = chat_lifecycle_locks[int(chat_id) % len(chat_lifecycle_locks)]

    state_notice = None
    notification_failed = False
    persistence_failed = False
    first_message = None
    second_message = None

    # این قفل با مسیر پایان چت و ارسال پیام هماهنگ است؛ queue_lock در طول API call آزاد می‌ماند.
    with lifecycle_lock:
        with queue_lock:
            current_pair = active_pairs.get(user_id)
            partner_pair = active_pairs.get(partner_id)

            pair_is_valid = bool(
                current_pair == initial_pair
                and partner_pair
                and partner_pair[0] == user_id
                and partner_pair[1] == chat_id
            )

        if not pair_is_valid:
            state_notice = '⚠️ وضعیت چت تغییر کرده یا معتبر نیست؛ دوباره جستجو کنید.'
        else:
            # این قفل با ثبت پایدار شناسه‌ها هماهنگ است تا delete_requested اشتباه ثبت نشود.
            with secure_tracker_lock:
                if chat_id in secure_chats:
                    state_notice = '⚠️ حالت چت امن از قبل فعال شده است.'
                else:
                    secure_chats.add(chat_id)

        if state_notice is None:
            try:
                first_message = bot.send_message(
                    user_id,
                    '✅ <b>حالت چت امن فعال شد! (رایگان)</b>\n\n'
                    '🔒 پیام‌ها با قابلیت محافظت محتوای تلگرام ارسال می‌شوند تا فوروارد و ذخیره مستقیم محدود شود.\n'
                    '⏱ عکس‌ها و ویدیوها با تایمر انتخابی برای حذف ارسال می‌شوند.\n'
                    '🧹 ربات پس از پایان چت برای حذف پیام‌های ثبت‌شده تلاش می‌کند.\n\n'
                    '⚠️ <b>مهم:</b> جلوگیری قطعی از اسکرین‌شات، ضبط صفحه یا ثبت محتوا با دستگاه دیگر ممکن نیست؛ اطلاعات حساس نفرستید.',
                    parse_mode='HTML',
                    protect_content=True
                )
                second_message = bot.send_message(
                    partner_id,
                    '🔒 <b>مخاطب حالت چت امن را فعال کرد.</b>\n\n'
                    'فوروارد و ذخیره مستقیم پیام‌های محافظت‌شده محدود می‌شود و ربات پس از پایان چت برای پاک‌کردن پیام‌های ثبت‌شده تلاش می‌کند.\n\n'
                    '⚠️ جلوگیری قطعی از اسکرین‌شات یا ضبط صفحه ممکن نیست؛ اطلاعات حساس نفرستید.',
                    parse_mode='HTML',
                    protect_content=True
                )
            except Exception:
                logger.error('secure mode notification failed', exc_info=True)

                # چون هنوز trackerای ثبت نشده، حالت امن را برمی‌گردانیم.
                with secure_tracker_lock:
                    secure_chats.discard(chat_id)

                if first_message is not None:
                    try:
                        bot.delete_message(user_id, first_message.message_id)
                    except Exception:
                        logger.warning(
                            'could not remove partial secure-mode notification chat=%s user=%s',
                            chat_id, user_id,
                            exc_info=True
                        )

                notification_failed = True

            else:
                # ثبت هر شناسه در SQLite انجام می‌شود؛ در صورت خطا، fail-safe چت را می‌بندد.
                try:
                    chat_msg_tracker[chat_id][user_id].extend([
                        message.message_id,
                        first_message.message_id
                    ])
                    chat_msg_tracker[chat_id][partner_id].append(second_message.message_id)
                except SecureTrackerPersistenceError:
                    # خود tracker حذف فوری و shutdown جداگانه را زمان‌بندی کرده است.
                    logger.critical(
                        'secure mode tracker persistence failed chat=%s',
                        chat_id,
                        exc_info=True
                    )
                    persistence_failed = True
                except Exception:
                    logger.critical(
                        'unexpected secure mode tracker failure chat=%s',
                        chat_id,
                        exc_info=True
                    )
                    _schedule_secure_tracker_shutdown(user_id, chat_id)
                    persistence_failed = True

    # پیام‌های وضعیت و خطا بیرون از قفل چرخه ارسال می‌شوند.
    if state_notice:
        try:
            bot.send_message(user_id, state_notice)
        except Exception:
            logger.warning('could not send secure-mode state notice', exc_info=True)
        return

    if notification_failed:
        for uid in (user_id, partner_id):
            try:
                bot.send_message(uid, '⚠️ فعال‌سازی حالت امن کامل نشد؛ لطفاً دوباره تلاش کنید.')
            except Exception:
                logger.warning(
                    'could not send secure-mode failure notice to user=%s',
                    uid,
                    exc_info=True
                )
        return

    if persistence_failed:
        # چت عمداً در حالت امن باقی می‌ماند تا fail-safe آن را پایان دهد.
        return

@bot.callback_query_handler(func=lambda call: bool(call.data) and call.data.startswith('secmedia_'))
def process_secure_media(call):
    """ارسال مدیای زمان‌دار بدون نگه‌داشتن queue_lock هنگام تماس شبکه.

    قفل چرخه عمر همان chat_id در طول ارسال و ثبت صف حذف نگه داشته می‌شود؛
    بنابراین terminate_chat_session نمی‌تواند وسط ارسال، Pair را جمع کند.
    """
    try:
        parts = (call.data or '').split('_')
        if len(parts) != 6 or parts[0] != 'secmedia':
            raise ValueError('invalid callback shape')

        seconds = int(parts[1])
        sender_id = int(parts[2])
        original_message_id = int(parts[3])
        expected_partner_id = int(parts[4])
        expected_chat_id = int(parts[5])

        if seconds not in (3, 10, 30):
            raise ValueError('invalid secure media timer')
    except (TypeError, ValueError):
        try:
            bot.answer_callback_query(
                call.id,
                '🚫 درخواست مدیا نامعتبر است.',
                show_alert=True
            )
        except Exception:
            pass
        return

    if not call.message:
        try:
            bot.answer_callback_query(
                call.id,
                '❌ پیام انتخاب مدیا در دسترس نیست.',
                show_alert=True
            )
        except Exception:
            pass
        return

    user_id = call.from_user.id
    if user_id != sender_id:
        bot.answer_callback_query(
            call.id,
            '🚫 این دکمه متعلق به ارسال‌کننده مدیاست.',
            show_alert=True
        )
        return

    # بررسی سبک Pair؛ queue_lock فقط برای خواندن state گرفته می‌شود.
    with queue_lock:
        pair_data = active_pairs.get(user_id)
        partner_pair = active_pairs.get(expected_partner_id)
        pair_is_valid = bool(
            pair_data
            and pair_data[0] == expected_partner_id
            and pair_data[1] == expected_chat_id
            and partner_pair
            and partner_pair[0] == user_id
            and partner_pair[1] == expected_chat_id
        )

    if not pair_is_valid:
        bot.answer_callback_query(
            call.id,
            '🚫 چت تغییر کرده یا این دکمه منقضی شده است.',
            show_alert=True
        )
        try:
            bot.delete_message(call.message.chat.id, call.message.message_id)
        except Exception:
            pass
        return

    partner_id = expected_partner_id
    chat_id = expected_chat_id
    unique_media_key = f'{sender_id}_{original_message_id}'

    # رزرو پردازش مدیا؛ کلیک دوباره روی همان دکمه پذیرفته نمی‌شود.
    with pending_secure_media_lock:
        data = pending_secure_media.get(unique_media_key)
        if not data or len(data) < 6:
            bot.answer_callback_query(
                call.id,
                '❌ مدیا پیدا نشد یا منقضی شده است.',
                show_alert=True
            )
            return

        orig_msg, created_at, prompt_msg_id, stored_chat_id, stored_partner_id, processing = data

        try:
            expired = time.time() - float(created_at) > 300
        except (TypeError, ValueError):
            expired = True

        if expired:
            pending_secure_media.pop(unique_media_key, None)
            bot.answer_callback_query(
                call.id,
                '⏳ مهلت انتخاب مدیا تمام شده است.',
                show_alert=True
            )
            return

        original_sender_id = getattr(
            getattr(orig_msg, 'from_user', None), 'id', None
        )
        if (
            stored_chat_id != chat_id
            or stored_partner_id != partner_id
            or original_sender_id != sender_id
            or getattr(orig_msg, 'message_id', None) != original_message_id
        ):
            bot.answer_callback_query(
                call.id,
                '🚫 این مدیا متعلق به چت فعلی نیست.',
                show_alert=True
            )
            return

        if processing:
            bot.answer_callback_query(
                call.id,
                '⏳ این مدیا قبلاً در حال ارسال است.',
                show_alert=True
            )
            return

        pending_secure_media[unique_media_key] = (
            orig_msg,
            created_at,
            prompt_msg_id,
            stored_chat_id,
            stored_partner_id,
            True
        )

    try:
        bot.edit_message_text(
            f'⏳ مدیا برای مخاطب ارسال می‌شود و پس از {seconds} ثانیه حذف خواهد شد.',
            chat_id=call.message.chat.id,
            message_id=call.message.message_id
        )
    except Exception:
        pass

    base_caption = orig_msg.caption or ''
    warning_text = f'\n\n🔥 این مدیا پس از {seconds} ثانیه حذف می‌شود.'
    final_caption = base_caption + warning_text if base_caption else warning_text.strip()

    sent_msg = None
    persistence_failed = False

    # این قفل با Decoratorهای _chat_lifecycle_serialized و
    # _chat_message_serialized مشترک است. برخلاف نسخه قبلی، queue_lock
    # در زمان send_photo/send_video یا ثبت شبکه‌ای نگه داشته نمی‌شود.
    lifecycle_lock = chat_lifecycle_locks[int(chat_id) % len(chat_lifecycle_locks)]

    try:
        with lifecycle_lock:
            # پس از گرفتن قفل چرخه عمر، Pair و وضعیت حالت امن را دوباره بررسی کن.
            with queue_lock:
                current_pair = active_pairs.get(user_id)
                current_partner_pair = active_pairs.get(partner_id)
                current_pair_valid = bool(
                    current_pair
                    and current_pair[0] == partner_id
                    and current_pair[1] == chat_id
                    and current_partner_pair
                    and current_partner_pair[0] == user_id
                    and current_partner_pair[1] == chat_id
                )
                secure_mode_active = chat_id in secure_chats

            if not current_pair_valid or not secure_mode_active:
                with pending_secure_media_lock:
                    pending_secure_media.pop(unique_media_key, None)
                bot.answer_callback_query(
                    call.id,
                    '❌ چت امن پیش از ارسال پایان یافته یا غیرفعال شده است.',
                    show_alert=True
                )
                try:
                    bot.delete_message(call.message.chat.id, call.message.message_id)
                except Exception:
                    pass
                return

            # چک نهایی مالکیت و TTL؛ ممکن است وضعیت در زمان انتظار برای قفل عوض شده باشد.
            with pending_secure_media_lock:
                current = pending_secure_media.get(unique_media_key)
                if (
                    not current
                    or len(current) < 6
                    or current[3] != chat_id
                    or current[4] != partner_id
                    or current[5] is not True
                ):
                    bot.answer_callback_query(
                        call.id,
                        '❌ انتخاب مدیا منقضی یا لغو شده است.',
                        show_alert=True
                    )
                    return

            # عملیات شبکه بدون queue_lock انجام می‌شود تا Matchmaking مسدود نشود.
            if orig_msg.content_type == 'photo':
                sent_msg = bot.send_photo(
                    partner_id,
                    orig_msg.photo[-1].file_id,
                    caption=final_caption,
                    protect_content=True
                )
            elif orig_msg.content_type == 'video':
                sent_msg = bot.send_video(
                    partner_id,
                    orig_msg.video.file_id,
                    caption=final_caption,
                    protect_content=True
                )
            else:
                raise ValueError('unsupported secure media type')

            # هر دو شناسه پیش از آزادشدن قفل چرخه عمر ثبت پایدار می‌شوند.
            # اگر ثبت یکی شکست بخورد، Tracker خودش تلاش فوری برای حذف و Fail-Safe می‌کند.
            chat_msg_tracker[chat_id][partner_id].append(sent_msg.message_id)
            chat_msg_tracker[chat_id][sender_id].append(original_message_id)

            try:
                with get_db_connection() as conn:
                    cur = conn.cursor()
                    cur.execute(
                        '''INSERT INTO secure_media_deletions
                           (chat_id, sender_id, receiver_id, original_message_id,
                            sent_message_id, status_chat_id, status_message_id,
                            delete_at, attempts, retry_at)
                           VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 0)''',
                        (
                            chat_id,
                            sender_id,
                            partner_id,
                            original_message_id,
                            sent_msg.message_id,
                            call.message.chat.id,
                            call.message.message_id,
                            time.time() + seconds
                        )
                    )
                    conn.commit()

            except Exception:
                persistence_failed = True
                logger.critical(
                    'could not persist secure-media deletion; queueing immediate deletion',
                    exc_info=True
                )

                # چون زمان‌بندی اختصاصی ذخیره نشده، هر دو پیام فوراً وارد صف حذف پایدار شوند.
                try:
                    with get_db_connection() as conn:
                        cur = conn.cursor()
                        cur.execute(
                            '''UPDATE secure_chat_messages
                               SET delete_requested = 1, retry_at = 0
                               WHERE chat_id = ? AND (
                                   (user_id = ? AND message_id = ?)
                                   OR (user_id = ? AND message_id = ?)
                               )''',
                            (
                                chat_id,
                                partner_id,
                                sent_msg.message_id,
                                sender_id,
                                original_message_id
                            )
                        )
                        conn.commit()
                except Exception:
                    logger.critical(
                        'could not queue immediate secure-media deletion chat=%s',
                        chat_id,
                        exc_info=True
                    )

                # تلاش فوری؛ اگر شکست خورد، صف پایدار در Worker دوباره امتحان می‌کند.
                for target_chat, message_id in (
                    (partner_id, sent_msg.message_id),
                    (sender_id, original_message_id)
                ):
                    try:
                        result = bot.delete_message(target_chat, message_id)
                        if result is not True:
                            logger.warning(
                                'immediate secure-media delete not confirmed chat=%s message=%s result=%r',
                                target_chat, message_id, result
                            )
                    except Exception:
                        logger.error(
                            'immediate secure-media rollback deletion failed chat=%s message=%s',
                            target_chat, message_id,
                            exc_info=True
                        )

                try:
                    _process_secure_chat_message_deletions(chat_id=chat_id)
                except Exception:
                    logger.error(
                        'immediate secure-media deletion queue processing failed chat=%s',
                        chat_id,
                        exc_info=True
                    )

            with pending_secure_media_lock:
                pending_secure_media.pop(unique_media_key, None)

        # از این نقطه به بعد قفل lifecycle آزاد شده است.
        try:
            bot.edit_message_reply_markup(
                chat_id=call.message.chat.id,
                message_id=call.message.message_id,
                reply_markup=None
            )
        except Exception:
            pass

        if persistence_failed:
            bot.answer_callback_query(
                call.id,
                '⚠️ ثبت تایمر ناموفق بود؛ ربات برای حذف فوری پیام‌ها تلاش کرد و صف حذف را فعال کرد.',
                show_alert=True
            )
        else:
            bot.answer_callback_query(
                call.id,
                f'✅ مدیا ارسال شد و پس از {seconds} ثانیه حذف می‌شود.'
            )

    except SecureTrackerPersistenceError:
        # Tracker در این وضعیت حذف فوری و بستن Fail-Safe چت را آغاز کرده است.
        with pending_secure_media_lock:
            pending_secure_media.pop(unique_media_key, None)
        try:
            bot.answer_callback_query(
                call.id,
                '⚠️ ثبت پیام امن ناموفق بود؛ چت برای حفظ حریم خصوصی در حال پایان است.',
                show_alert=True
            )
        except Exception:
            pass

    except Exception as exc:
        logger.error(
            'secure media send failed user=%s partner=%s chat=%s',
            user_id, partner_id, chat_id,
            exc_info=True
        )
        with pending_secure_media_lock:
            current = pending_secure_media.get(unique_media_key)
            if current and len(current) >= 6:
                pending_secure_media[unique_media_key] = (
                    current[0], current[1], current[2],
                    current[3], current[4], False
                )

        try:
            bot.answer_callback_query(
                call.id,
                '❌ ارسال کامل نشد؛ می‌توانید تا پایان مهلت دوباره تلاش کنید.',
                show_alert=True
            )
        except Exception:
            pass

# ============================================================================
# 12) Feedback: one rating per ended chat + no self vote (سیستم ضد تقلب کارما)
# ============================================================================

def send_feedback_menu(to_user_id, target_user_id, chat_id=None):
    try:
        # اگر آیدی چت ارسال نشده بود، آخرین چت بسته شده بین این دو نفر را پیدا کن
        if chat_id is None:
            with get_db_connection() as conn:
                cur = conn.cursor()
                cur.execute('''SELECT chat_id FROM chats
                               WHERE ((user1_id = ? AND user2_id = ?) OR (user1_id = ? AND user2_id = ?))
                                 AND end_time IS NOT NULL
                               ORDER BY chat_id DESC LIMIT 1''',
                            (to_user_id, target_user_id, target_user_id, to_user_id))
                row = cur.fetchone()
                chat_id = row[0] if row else None

        if chat_id is None:
            return
            
        markup = types.InlineKeyboardMarkup(row_width=5)
        markup.add(*[
            types.InlineKeyboardButton(f'{score} ⭐', callback_data=f'karma_{chat_id}_{target_user_id}_{score}')
            for score in range(1, 6)
        ])
        if (
            not are_users_friends(to_user_id, target_user_id)
            and not are_users_blocked_mutually(to_user_id, target_user_id)
        ):
            markup.row(
                types.InlineKeyboardButton(
                    'افزودن به لیست دوستان 🤝',
                    callback_data=f'friend_postchat_{chat_id}_{target_user_id}'
                )
            )
        markup.row(
            types.InlineKeyboardButton('بلاک 🚫', callback_data=f'block_user_{target_user_id}'),
            types.InlineKeyboardButton('گزارش ⚠️', callback_data=f'report_user_{target_user_id}')
        )
        bot.send_message(to_user_id, '💬 ✨ چت شما با کاربر تموم شد\n⭐ لطفاً بهش امتیاز بدین', reply_markup=markup)
    except Exception:
        logger.error('send_feedback_menu failed', exc_info=True)

@bot.callback_query_handler(func=lambda call: call.data.startswith('karma_'))
def process_karma(call):
    user_id = call.from_user.id
    parts = call.data.split('_')
    
    # 🌟 بک‌وارد کامپتیبیلیتی: جلوگیری از کرش کردن با دکمه‌های قدیمی
    if len(parts) != 4:
        bot.answer_callback_query(call.id, '🚫 این رأی مربوط به نسخه قدیمی است و منقضی شده.', show_alert=True)
        return
        
    try:
        chat_id = int(parts[1])
        target_user_id = int(parts[2])
        score = int(parts[3])
    except ValueError:
        bot.answer_callback_query(call.id, '🚫 داده نامعتبر است.', show_alert=True)
        return

    if user_id == target_user_id or score not in range(1, 6):
        bot.answer_callback_query(call.id, '❌ امتیاز نامعتبر است.', show_alert=True)
        return

    karma_change = {1: -1, 2: 0, 3: 1, 4: 2, 5: 3}[score]

    with get_db_connection() as conn:
        cur = conn.cursor()
        
        # ۱. بررسی اینکه آیا واقعاً این چت تمام شده است؟
        cur.execute('''SELECT 1 FROM chats
                       WHERE chat_id = ?
                         AND end_time IS NOT NULL
                         AND ((user1_id = ? AND user2_id = ?) OR (user1_id = ? AND user2_id = ?))''',
                    (chat_id, user_id, target_user_id, target_user_id, user_id))
        if not cur.fetchone():
            bot.answer_callback_query(call.id, '🚫 این امتیاز برای چت شما نیست.', show_alert=True)
            return

        # ۲. بررسی اینکه آیا قبلاً امتیاز داده یا نه (جلوگیری از مزرعه کارما)
        cur.execute('SELECT 1 FROM chat_feedback WHERE chat_id = ? AND rater_id = ? AND target_id = ?',
                    (chat_id, user_id, target_user_id))
        if cur.fetchone():
            bot.answer_callback_query(call.id, '⚠️ شما قبلاً به این چت امتیاز داده‌اید.', show_alert=True)
            return

        # ۳. ثبت امتیاز در دفتر کل بازخوردها
        cur.execute('''INSERT INTO chat_feedback(chat_id, rater_id, target_id, score, created_at)
                       VALUES (?, ?, ?, ?, ?)''',
                    (chat_id, user_id, target_user_id, score, datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
        if karma_change:
            cur.execute('UPDATE users SET karma = COALESCE(karma, 0) + ? WHERE user_id = ?',
                        (karma_change, target_user_id))
        conn.commit()

    bot.answer_callback_query(call.id, f'✅ امتیاز {score} ستاره ثبت شد.')
    try:
        bot.delete_message(call.message.chat.id, call.message.message_id)
    except Exception:
        pass

def has_reported(reporter_id, target_id):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT 1 FROM user_reports WHERE reporter_id = ? AND target_id = ?', (reporter_id, target_id))
        return cur.fetchone() is not None

# ============================================================================
# 10) Block / Cancel / stale-chat safety (جلوگیری از باگ دکمه‌های رها شده)
# ============================================================================

@bot.callback_query_handler(func=lambda call: call.data.startswith('block_user_'))
def process_user_block(call):
    """ثبت بلاک با اعتبارسنجی زوج چت و جلوگیری از قطع یک چت جدید."""
    user_id = call.from_user.id

    # دکمه بلاک فقط باید از پیام خصوصی خود کاربر اجرا شود.
    if getattr(getattr(call, 'message', None), 'chat', None) is None or call.message.chat.id != user_id:
        bot.answer_callback_query(
            call.id,
            '🚫 این دکمه فقط از گفت‌وگوی خصوصی خودتان قابل استفاده است.',
            show_alert=True
        )
        return

    parts = (call.data or '').split('_')
    if len(parts) != 3 or parts[0] != 'block' or parts[1] != 'user':
        bot.answer_callback_query(call.id, '❌ درخواست نامعتبر است.', show_alert=True)
        return

    try:
        target_user_id = int(parts[2])
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '❌ شناسه کاربر نامعتبر است.', show_alert=True)
        return

    if target_user_id <= 0 or target_user_id == user_id:
        bot.answer_callback_query(call.id, '❌ نمی‌توانید خودتان را بلاک کنید.', show_alert=True)
        return

    if not get_user(target_user_id):
        bot.answer_callback_query(call.id, '❌ کاربر پیدا نشد.', show_alert=True)
        return

    # ابتدا بلاک در دیتابیس ثبت و هرگونه دوستی/درخواست دوستی بین دو کاربر حذف می‌شود.
    try:
        u_min, u_max = (user_id, target_user_id) if user_id < target_user_id else (target_user_id, user_id)
        now_ts = time.time()
        with get_db_connection() as conn:
            conn.execute('BEGIN IMMEDIATE')
            conn.execute(
                'INSERT OR IGNORE INTO user_blocks(blocker_id, blocked_id) VALUES (?, ?)',
                (user_id, target_user_id)
            )
            conn.execute(
                'DELETE FROM friendships WHERE user1_id = ? AND user2_id = ?',
                (u_min, u_max)
            )
            conn.execute(
                '''UPDATE friend_requests
                   SET status = 'cancelled', updated_at = ?
                   WHERE ((sender_id = ? AND receiver_id = ?) OR (sender_id = ? AND receiver_id = ?))
                     AND status = 'pending' ''',
                (now_ts, user_id, target_user_id, target_user_id, user_id)
            )
            conn.commit()
    except Exception:
        logger.error('Could not save user block: blocker=%s target=%s', user_id, target_user_id, exc_info=True)
        bot.answer_callback_query(
            call.id,
            '❌ ثبت بلاک انجام نشد؛ لطفاً دوباره تلاش کنید.',
            show_alert=True
        )
        return

    disconnected = None

    # زوج فعلی را به‌صورت اتمیک می‌خوانیم و فقط همان چت را هدف می‌گیریم.
    with queue_lock:
        pair_snapshot = active_pairs.get(user_id)

    if pair_snapshot and pair_snapshot[0] == target_user_id:
        partner_id, chat_id, _via_link = pair_snapshot
        lifecycle_lock = chat_lifecycle_locks[int(chat_id) % len(chat_lifecycle_locks)]

        # RLock است؛ force_end_chat می‌تواند همین قفل را دوباره بگیرد.
        with lifecycle_lock:
            with queue_lock:
                current_user_pair = active_pairs.get(user_id)
                current_partner_pair = active_pairs.get(partner_id)
                pair_is_still_exact = bool(
                    current_user_pair == pair_snapshot
                    and current_partner_pair
                    and current_partner_pair[0] == user_id
                    and current_partner_pair[1] == chat_id
                )

            if pair_is_still_exact:
                try:
                    disconnected = force_end_chat(
                        user_id,
                        '⚠️ کاربر مقابل شما را بلاک کرد؛ این ارتباط پایان یافت.'
                    )
                except Exception:
                    logger.error(
                        'Could not end blocked active chat: blocker=%s target=%s chat_id=%s',
                        user_id, target_user_id, chat_id, exc_info=True
                    )
                    disconnected = False

    try:
        if disconnected is True:
            bot.answer_callback_query(
                call.id,
                '🚫 کاربر بلاک شد و ارتباط فعلی نیز پایان یافت.',
                show_alert=True
            )
        elif disconnected is False:
            bot.answer_callback_query(
                call.id,
                '✅ بلاک ثبت شد؛ اما پایان ارتباط فعال کامل نشد. وضعیت چت را بررسی کنید.',
                show_alert=True
            )
        else:
            bot.answer_callback_query(
                call.id,
                '🚫 کاربر بلاک شد و دیگر در مچینگ‌های بعدی با شما قرار نمی‌گیرد.',
                show_alert=True
            )
    finally:
        try:
            bot.delete_message(call.message.chat.id, call.message.message_id)
        except Exception:
            pass

# ============================================================================
# 11) Stale Next/End callbacks cannot touch a new chat
# ============================================================================
def handle_cancel(message):
    """Cancel the caller's current chat/search without touching a newer pair."""
    user_id = message.from_user.id
    update_last_seen(user_id)

    # /cancel must only affect the user's own private conversation with the bot.
    if getattr(message.chat, 'id', None) != user_id:
        return

    pair_snapshot = active_pairs.get(user_id)
    if pair_snapshot:
        try:
            partner_id, chat_id, _via_link = pair_snapshot
            partner_id = int(partner_id)
            chat_id = int(chat_id)
        except (TypeError, ValueError):
            bot.send_message(user_id, '⚠️ وضعیت چت نامعتبر است؛ لطفاً دوباره /cancel را بفرستید.')
            return

        if partner_id == user_id:
            bot.send_message(user_id, '⚠️ وضعیت چت نامعتبر است؛ لطفاً با پشتیبانی تماس بگیرید.')
            return

        lifecycle_lock = chat_lifecycle_locks[chat_id % len(chat_lifecycle_locks)]
        state_changed = False
        partner_matches = False
        terminated = False

        # Lock order is lifecycle -> queue, matching the chat message/termination flow.
        with lifecycle_lock:
            with queue_lock:
                if active_pairs.get(user_id) != pair_snapshot:
                    state_changed = True
                else:
                    partner_pair = active_pairs.get(partner_id)
                    partner_matches = bool(
                        partner_pair
                        and partner_pair[0] == user_id
                        and partner_pair[1] == chat_id
                    )

            if not state_changed:
                terminated = terminate_chat_session(
                    chat_id,
                    user_id,
                    partner_id,
                    send_feedback=partner_matches
                )

        if state_changed:
            bot.send_message(
                user_id,
                'ℹ️ وضعیت چت هم‌زمان تغییر کرد؛ برای جلوگیری از قطع ارتباط جدید، دوباره /cancel را ارسال کنید.'
            )
            return

        if not terminated:
            bot.send_message(user_id, 'ℹ️ این چت قبلاً پایان یافته است.')
            return

        # Notify the caller independently from menu rendering.
        try:
            bot.send_message(
                user_id,
                '✅ چت شما با موفقیت پایان یافت.',
                reply_markup=types.ReplyKeyboardRemove()
            )
        except Exception:
            logger.warning('Could not send /cancel confirmation to user=%s', user_id, exc_info=True)
        try:
            show_main_menu(user_id, user_id in ADMINS)
        except Exception:
            logger.warning('Could not show main menu after /cancel for user=%s', user_id, exc_info=True)

        # Never send an old-chat notice/menu if the partner has already moved on.
        if partner_matches:
            with queue_lock:
                partner_is_free = partner_id not in active_pairs and not is_user_waiting(partner_id)
            if partner_is_free:
                try:
                    bot.send_message(
                        partner_id,
                        '⚠️ کاربر مقابل چت را پایان داد.',
                        reply_markup=types.ReplyKeyboardRemove()
                    )
                except Exception:
                    logger.warning('Could not notify chat partner after /cancel', exc_info=True)
                try:
                    show_main_menu(partner_id, partner_id in ADMINS)
                except Exception:
                    logger.warning('Could not show partner menu after /cancel', exc_info=True)
        return

    # There was no active pair at entry. Do not cancel a pair created concurrently.
    with queue_lock:
        if active_pairs.get(user_id) is not None:
            state_changed = True
            removed = False
            released = False
        else:
            state_changed = False
            removed = remove_from_waiting(user_id)
            released = release_search_reservation(user_id)

    if state_changed:
        bot.send_message(
            user_id,
            'ℹ️ وضعیت جستجو تغییر کرد؛ برای جلوگیری از لغو ارتباط جدید، دوباره /cancel را ارسال کنید.'
        )
        return

    if removed or released:
        try:
            bot.send_message(
                user_id,
                '✅ جستجو متوقف شد و هزینه/سهمیه مربوطه بازگردانده شد.',
                reply_markup=types.ReplyKeyboardRemove()
            )
        except Exception:
            logger.warning('Could not send search-cancel confirmation for user=%s', user_id, exc_info=True)
        try:
            show_main_menu(user_id, user_id in ADMINS)
        except Exception:
            logger.warning('Could not show menu after search cancellation for user=%s', user_id, exc_info=True)
        return

    # Last-resort cleanup for an unfinished DB chat whose in-memory pair is missing.
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute(
            '''SELECT chat_id, user1_id, user2_id FROM chats
               WHERE (user1_id = ? OR user2_id = ?) AND end_time IS NULL
               ORDER BY chat_id DESC LIMIT 1''',
            (user_id, user_id)
        )
        stale = cur.fetchone()

    if stale:
        stale_chat_id, u1, u2 = stale
        stale_chat_id = int(stale_chat_id)
        partner_id = int(u2 if user_id == u1 else u1)
        lifecycle_lock = chat_lifecycle_locks[stale_chat_id % len(chat_lifecycle_locks)]
        state_changed = False
        still_open = False
        partner_matches = False
        terminated = False

        with lifecycle_lock:
            with queue_lock:
                # If a new in-memory pair appeared, this DB fallback is now stale.
                if active_pairs.get(user_id) is not None:
                    state_changed = True
                else:
                    partner_pair = active_pairs.get(partner_id)
                    partner_matches = bool(
                        partner_pair
                        and partner_pair[0] == user_id
                        and partner_pair[1] == stale_chat_id
                    )

            if not state_changed:
                with get_db_connection() as conn:
                    cur = conn.cursor()
                    cur.execute(
                        '''SELECT 1 FROM chats
                           WHERE chat_id = ? AND end_time IS NULL
                             AND ((user1_id = ? AND user2_id = ?)
                               OR (user1_id = ? AND user2_id = ?))''',
                        (stale_chat_id, user_id, partner_id, partner_id, user_id)
                    )
                    still_open = cur.fetchone() is not None

                if still_open:
                    terminated = terminate_chat_session(
                        stale_chat_id,
                        user_id,
                        partner_id,
                        send_feedback=partner_matches
                    )

        if state_changed:
            bot.send_message(
                user_id,
                'ℹ️ وضعیت چت تغییر کرد؛ برای جلوگیری از لغو ارتباط جدید، دوباره /cancel را ارسال کنید.'
            )
            return

        if still_open and terminated:
            try:
                bot.send_message(
                    user_id,
                    '✅ چت باز قبلی پاک‌سازی شد.',
                    reply_markup=types.ReplyKeyboardRemove()
                )
            except Exception:
                logger.warning('Could not notify user after stale-chat cleanup', exc_info=True)
            try:
                show_main_menu(user_id, user_id in ADMINS)
            except Exception:
                logger.warning('Could not show menu after stale-chat cleanup', exc_info=True)

            if partner_matches:
                with queue_lock:
                    partner_is_free = partner_id not in active_pairs and not is_user_waiting(partner_id)
                if partner_is_free:
                    try:
                        bot.send_message(
                            partner_id,
                            '⚠️ چت قبلی پایان یافت.',
                            reply_markup=types.ReplyKeyboardRemove()
                        )
                    except Exception:
                        logger.warning('Could not notify partner after stale-chat cleanup', exc_info=True)
                    try:
                        show_main_menu(partner_id, partner_id in ADMINS)
                    except Exception:
                        logger.warning('Could not show partner menu after stale-chat cleanup', exc_info=True)
            return

    bot.send_message(user_id, 'ℹ️ شما در حال حاضر در چت یا صف نیستید.')
    try:
        show_main_menu(user_id, user_id in ADMINS)
    except Exception:
        logger.warning('Could not show idle main menu for user=%s', user_id, exc_info=True)

@bot.message_handler(func=lambda message: message.text in ['نفر بعدی ⏭', '/next'])
def handle_next(message):
    user_id = message.from_user.id
    if getattr(message.chat, 'type', 'private') != 'private':
        return

    update_last_seen(user_id)
    now = time.time()

    # شمارش درخواست‌ها فقط داخل قفل ضداسپم انجام می‌شود؛ ارسال تلگرام بیرون قفل است.
    wait_sec = None
    with spam_lock:
        timeout_until = user_timeouts.get(user_id)
        if timeout_until and now < timeout_until:
            wait_sec = max(1, int(timeout_until - now))
            is_spamming = False
        else:
            user_timeouts.pop(user_id, None)
            presses = [t for t in user_next_presses[user_id] if now - t <= 10]
            presses.append(now)
            user_next_presses[user_id] = presses
            is_spamming = len(presses) > 5
            if is_spamming:
                user_timeouts[user_id] = now + 180
                user_next_presses.pop(user_id, None)

    if wait_sec is not None:
        bot.send_message(user_id, f'🚫 {wait_sec} ثانیه دیگر صبر کنید.')
        return

    if is_spamming:
        # زوج زیر queue_lock خوانده می‌شود تا با pair_users هم‌زمانی ناسازگار نداشته باشد.
        with queue_lock:
            pair_data = active_pairs.get(user_id)
            if not pair_data:
                remove_from_waiting(user_id)
                release_search_reservation(user_id)

        if pair_data:
            # force_end_chat خودش قفل چرخه عمر، اعتبارسنجی مجدد زوج و اعلان ایمن را انجام می‌دهد.
            try:
                force_end_chat(
                    user_id,
                    '⚠️ ارتباط به دلیل اسپم کاربر مقابل قطع شد.'
                )
            except Exception:
                logger.error('handle_next: force_end_chat failed during spam handling', exc_info=True)

        try:
            bot.send_message(
                user_id,
                '🚨 به‌دلیل اسپم ۳ دقیقه از جستجو محروم شدید.',
                reply_markup=types.ReplyKeyboardRemove()
            )
        except Exception:
            logger.warning('handle_next: could not notify spam timeout', exc_info=True)
        try:
            show_main_menu(user_id, user_id in ADMINS)
        except Exception:
            logger.warning('handle_next: could not show menu after spam timeout', exc_info=True)
        return

    # Pair و صف به‌صورت اتمیک نسبت به pair_users بررسی می‌شوند.
    with queue_lock:
        pair_data = active_pairs.get(user_id)
        if not pair_data:
            remove_from_waiting(user_id)
            release_search_reservation(user_id)

    if pair_data:
        try:
            partner_id, chat_id, _via_link = pair_data
        except (TypeError, ValueError):
            logger.error('handle_next: malformed active pair for user=%s', user_id)
            bot.send_message(user_id, '⚠️ وضعیت چت شما نامعتبر است؛ لطفاً دوباره تلاش کنید.')
            return

        # فقط وقتی زوج هنوز دوطرفه و متعلق به همین چت است، پیام تأیید ساخته می‌شود.
        lock = chat_lifecycle_locks[int(chat_id) % len(chat_lifecycle_locks)]
        with lock:
            with queue_lock:
                current_pair = active_pairs.get(user_id)
                partner_pair = active_pairs.get(partner_id)
                pair_is_current = bool(
                    current_pair == pair_data
                    and partner_pair
                    and partner_pair[0] == user_id
                    and partner_pair[1] == chat_id
                )

        if not pair_is_current:
            bot.send_message(user_id, '⚠️ وضعیت چت تغییر کرده است؛ لطفاً دوباره درخواست بدهید.')
            return

        markup = types.InlineKeyboardMarkup(row_width=2)
        markup.add(
            types.InlineKeyboardButton(
                'بله، نفر بعدی ⏭',
                callback_data=f'nextchat_yes_{chat_id}_{partner_id}'
            ),
            types.InlineKeyboardButton(
                'خیر، ادامه چت 🙂',
                callback_data=f'nextchat_no_{chat_id}_{partner_id}'
            )
        )
        try:
            bot.send_message(
                user_id,
                'مطمئنی می‌خوای این چت رو قطع کنی و بری نفر بعدی؟',
                reply_markup=markup
            )
        except Exception:
            logger.warning('handle_next: could not send next-chat confirmation', exc_info=True)
        return

    try:
        bot.send_message(
            user_id,
            'جستجوی قبلی لغو شد.',
            reply_markup=types.ReplyKeyboardRemove()
        )
    except Exception:
        logger.warning('handle_next: could not send previous-search notice', exc_info=True)

    # شروع جستجوی جدید بعد از آزادشدن صف/رزرو و خارج از queue_lock انجام می‌شود.
    try:
        start_anonymous_chat(message)
    except Exception:
        logger.error('handle_next: start_anonymous_chat failed', exc_info=True)
        try:
            bot.send_message(user_id, '❌ شروع جستجوی جدید ناموفق بود؛ لطفاً دوباره تلاش کنید.')
        except Exception:
            pass

@bot.callback_query_handler(func=lambda call: call.data.startswith('nextchat_'))
def process_next_chat_decision(call):
    """اعتبارسنجی امن دکمه رفتن به گفت‌وگوی بعدی."""
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    data = getattr(call, 'data', None)
    message = getattr(call, 'message', None)
    message_chat = getattr(message, 'chat', None)

    def answer_callback(text, show_alert=False):
        try:
            bot.answer_callback_query(call.id, text, show_alert=show_alert)
        except Exception:
            logger.debug('nextchat callback answer failed', exc_info=True)

    # این دکمه فقط باید از پیام خصوصی خود کاربر اجرا شود.
    if user_id is None or getattr(message_chat, 'id', None) != user_id:
        answer_callback('🚫 این دکمه فقط از گفت‌وگوی خصوصی ربات قابل استفاده است.', show_alert=True)
        return

    if not isinstance(data, str):
        answer_callback('🚫 دکمه نامعتبر یا منقضی است.', show_alert=True)
        return

    parts = data.split('_')
    if len(parts) != 4 or parts[0] != 'nextchat':
        answer_callback('🚫 دکمه نامعتبر یا منقضی است.', show_alert=True)
        return

    action = parts[1]
    if action not in ('yes', 'no'):
        answer_callback('🚫 عملیات درخواستی معتبر نیست.', show_alert=True)
        return

    try:
        expected_chat_id = int(parts[2])
        expected_partner_id = int(parts[3])
    except (TypeError, ValueError):
        answer_callback('🚫 اطلاعات دکمه معتبر نیست.', show_alert=True)
        return

    if expected_partner_id == user_id:
        answer_callback('🚫 اطلاعات طرف مقابل معتبر نیست.', show_alert=True)
        return

    # برای هر چت از همان قفلی استفاده می‌شود که چرخهٔ عمر چت از آن استفاده می‌کند.
    lifecycle_lock = chat_lifecycle_locks[expected_chat_id % len(chat_lifecycle_locks)]
    with lifecycle_lock:
        # هر دو سمت Pair باید هنوز به همین چت و همین یکدیگر متصل باشند.
        with queue_lock:
            user_pair = active_pairs.get(user_id)
            partner_pair = active_pairs.get(expected_partner_id)

            pair_is_valid = (
                isinstance(user_pair, (tuple, list))
                and len(user_pair) >= 2
                and user_pair[0] == expected_partner_id
                and user_pair[1] == expected_chat_id
                and isinstance(partner_pair, (tuple, list))
                and len(partner_pair) >= 2
                and partner_pair[0] == user_id
                and partner_pair[1] == expected_chat_id
            )

        if not pair_is_valid:
            answer_callback('🚫 این دکمه مربوط به چت قبلی است یا چت دیگر فعال نیست.', show_alert=True)
            return

        if action == 'no':
            try:
                bot.delete_message(user_id, message.message_id)
            except Exception:
                logger.debug('could not delete nextchat confirmation message', exc_info=True)
            answer_callback('خوبه، گفت‌وگو ادامه پیدا می‌کند. 🙂')
            return

        # تا وقتی قفل چت در اختیار ماست، پایان همان Pair را ثبت می‌کنیم.
        ended = terminate_chat_session(
            expected_chat_id,
            user_id,
            expected_partner_id,
            send_feedback=True
        )
        if not ended:
            answer_callback('⚠️ پایان چت انجام نشد؛ دوباره وضعیت را بررسی کنید.', show_alert=True)
            return

    # عملیات شبکه‌ای و باز کردن منوی جستجو بعد از آزاد شدن قفل انجام می‌شود.
    try:
        bot.delete_message(user_id, message.message_id)
    except Exception:
        logger.debug('could not delete completed nextchat confirmation', exc_info=True)

    answer_callback('✅ چت قبلی پایان یافت؛ منوی جستجو باز می‌شود.')

    try:
        bot.send_message(
            user_id,
            '✅ چت قبلی پایان یافت.',
            reply_markup=types.ReplyKeyboardRemove()
        )
    except Exception:
        logger.warning('could not notify user after nextchat', exc_info=True)

    try:
        bot.send_message(
            expected_partner_id,
            '⚠️ کاربر مقابل چت را ترک کرد و به جستجوی نفر بعدی رفت.',
            reply_markup=types.ReplyKeyboardRemove()
        )
    except Exception:
        logger.warning('could not notify partner after nextchat', exc_info=True)

    try:
        show_main_menu(expected_partner_id, expected_partner_id in ADMINS)
    except Exception:
        logger.warning('could not show partner main menu after nextchat', exc_info=True)

    try:
        show_main_menu(user_id, user_id in ADMINS)
    except Exception:
        logger.warning('could not show user main menu after nextchat', exc_info=True)

    # همان مسیر قبلی برای نمایش گزینه‌های جستجو حفظ می‌شود.
    try:
        fake_message = SimpleNamespace(
            from_user=SimpleNamespace(id=user_id),
            chat=SimpleNamespace(id=user_id)
        )
        start_anonymous_chat(fake_message)
    except Exception:
        logger.exception('could not reopen anonymous search menu after nextchat')

@bot.message_handler(func=lambda message: getattr(message, 'text', None) == 'پایان چت ❌')
def ask_end_chat(message):
    """Create an end-chat confirmation only for the user's current private chat."""
    user = getattr(message, 'from_user', None)
    chat = getattr(message, 'chat', None)
    if user is None or chat is None:
        return

    user_id = int(user.id)

    # This control is intended only for the user's private conversation with the bot.
    if getattr(chat, 'type', None) != 'private' or int(chat.id) != user_id:
        return

    update_last_seen(user_id)

    with queue_lock:
        pair = active_pairs.get(user_id)

    if not pair:
        try:
            bot.send_message(user_id, '⚠️ شما در چت فعالی نیستید.')
        except Exception:
            logger.warning('ask_end_chat: could not notify user without an active chat', exc_info=True)
        return

    partner_id, chat_id, _via_link = pair
    lifecycle_lock = chat_lifecycle_locks[int(chat_id) % len(chat_lifecycle_locks)]

    # Serialize confirmation creation with messages and chat termination for this chat.
    with lifecycle_lock:
        with queue_lock:
            current_pair = active_pairs.get(user_id)
            partner_pair = active_pairs.get(partner_id)
            pair_is_valid = bool(
                current_pair == pair
                and partner_pair
                and partner_pair[0] == user_id
                and partner_pair[1] == chat_id
            )

        if not pair_is_valid:
            try:
                bot.send_message(user_id, '⚠️ وضعیت چت تغییر کرده است؛ لطفاً دوباره بررسی کنید.')
            except Exception:
                logger.warning('ask_end_chat: could not notify about a changed chat', exc_info=True)
            return

        markup = types.InlineKeyboardMarkup(row_width=2)
        markup.add(
            types.InlineKeyboardButton(
                'بله، قطع کن ❌',
                callback_data=f'endchat_yes_{chat_id}_{partner_id}'
            ),
            types.InlineKeyboardButton(
                'ادامه دادن چت 🙂',
                callback_data=f'endchat_no_{chat_id}_{partner_id}'
            )
        )

        try:
            bot.send_message(
                user_id,
                'مطمئنی می‌خوای چت را تمام کنی؟',
                reply_markup=markup
            )
        except Exception:
            logger.warning('ask_end_chat: could not send confirmation prompt', exc_info=True)

@bot.callback_query_handler(func=lambda call: bool(call.data) and call.data.startswith('endchat_'))
def process_end_chat_decision(call):
    """پردازش ایمن دکمه پایان چت؛ Callback قدیمی یا نامعتبر نباید Pair دیگری را ببندد."""
    user_id = call.from_user.id
    parts = (call.data or '').split('_')

    if len(parts) != 4 or parts[0] != 'endchat':
        bot.answer_callback_query(
            call.id,
            '🚫 دکمه نامعتبر یا منقضی است.',
            show_alert=True
        )
        return

    action = parts[1]
    if action not in ('yes', 'no'):
        bot.answer_callback_query(
            call.id,
            '🚫 انتخاب دکمه معتبر نیست.',
            show_alert=True
        )
        return

    try:
        expected_chat_id = int(parts[2])
        expected_partner_id = int(parts[3])
        callback_chat_id = int(call.message.chat.id)
    except (TypeError, ValueError, AttributeError):
        bot.answer_callback_query(
            call.id,
            '🚫 اطلاعات دکمه معتبر نیست.',
            show_alert=True
        )
        return

    # این دکمه فقط باید از پیام خصوصی خود کاربر اجرا شود.
    if callback_chat_id != user_id or expected_partner_id == user_id:
        bot.answer_callback_query(
            call.id,
            '🚫 این دکمه متعلق به شما نیست.',
            show_alert=True
        )
        return

    lifecycle_lock = chat_lifecycle_locks[
        int(expected_chat_id) % len(chat_lifecycle_locks)
    ]

    terminated = False
    with lifecycle_lock:
        current_pair = active_pairs.get(user_id)
        partner_pair = active_pairs.get(expected_partner_id)

        # هر دو سمت Pair باید هنوز دقیقاً به همین چت اشاره کنند.
        pair_is_current = bool(
            current_pair
            and current_pair[0] == expected_partner_id
            and current_pair[1] == expected_chat_id
            and partner_pair
            and partner_pair[0] == user_id
            and partner_pair[1] == expected_chat_id
        )

        if not pair_is_current:
            bot.answer_callback_query(
                call.id,
                '⚠️ این دکمه مربوط به چت قبلی است یا چت قبلاً پایان یافته.',
                show_alert=True
            )
            return

        if action == 'yes':
            # terminate_chat_session در نسخه‌های قبلی نیز از همین قفل استفاده می‌کند؛
            # قفل‌ها RLock هستند و ورود مجدد از همین Thread مجاز است.
            terminated = terminate_chat_session(
                expected_chat_id,
                user_id,
                expected_partner_id,
                send_feedback=True
            )

    try:
        bot.delete_message(callback_chat_id, call.message.message_id)
    except Exception:
        pass

    if action == 'no':
        bot.answer_callback_query(call.id, 'خوبه، ادامه بدید! 😉')
        return

    if not terminated:
        bot.answer_callback_query(
            call.id,
            '⚠️ پایان چت انجام نشد؛ وضعیت اتصال را دوباره بررسی کنید.',
            show_alert=True
        )
        return

    bot.answer_callback_query(call.id, '✅ چت پایان یافت.')

    try:
        bot.send_message(
            user_id,
            '✅ چت شما با موفقیت پایان یافت.',
            reply_markup=types.ReplyKeyboardRemove()
        )
    except Exception:
        logger.warning('Could not notify user=%s after ending chat', user_id, exc_info=True)

    try:
        bot.send_message(
            expected_partner_id,
            '⚠️ کاربر مقابل چت را پایان داد.',
            reply_markup=types.ReplyKeyboardRemove()
        )
    except Exception:
        logger.warning(
            'Could not notify partner=%s after ending chat=%s',
            expected_partner_id,
            expected_chat_id,
            exc_info=True
        )

    try:
        show_main_menu(expected_partner_id, expected_partner_id in ADMINS)
    except Exception:
        logger.warning('Could not show main menu for partner=%s', expected_partner_id, exc_info=True)

    try:
        show_main_menu(user_id, user_id in ADMINS)
    except Exception:
        logger.warning('Could not show main menu for user=%s', user_id, exc_info=True)

def get_reports_today(reporter_id):
    since = (datetime.now() - timedelta(hours=24)).strftime("%Y-%m-%d %H:%M:%S")
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT COUNT(*) FROM user_reports WHERE reporter_id = ? AND created_at >= ?', (reporter_id, since))
        return cur.fetchone()[0]

# ============================================================================
# 25) Report anti-brigading: require an actual chat relationship
# ============================================================================

def has_user_chatted(user1_id, user2_id):
    with get_db_connection() as conn:
        row = conn.execute('''SELECT 1 FROM chats
                              WHERE ((user1_id = ? AND user2_id = ?) OR (user1_id = ? AND user2_id = ?))
                              LIMIT 1''',
                           (user1_id, user2_id, user2_id, user1_id)).fetchone()
        return row is not None

@bot.callback_query_handler(func=lambda call: bool(call.data) and call.data.startswith('report_user_'))
def process_report_start(call):
    """شروع امن فرایند گزارش فقط از پیام خصوصی ربات."""
    reporter_id = call.from_user.id

    # callbackهای بدون پیام عادی (inline mode) و پیام‌های گروهی پذیرفته نیستند.
    callback_message = getattr(call, 'message', None)
    callback_chat = getattr(callback_message, 'chat', None) if callback_message else None
    if (
        callback_message is None
        or callback_chat is None
        or getattr(callback_chat, 'type', None) != 'private'
        or callback_chat.id != reporter_id
    ):
        bot.answer_callback_query(
            call.id,
            '🚫 ثبت گزارش فقط از پیام خصوصی ربات امکان‌پذیر است.',
            show_alert=True
        )
        return

    # قالب callback باید دقیقاً report_user_<positive_user_id> باشد.
    parts = call.data.split('_')
    if len(parts) != 3 or parts[0] != 'report' or parts[1] != 'user':
        bot.answer_callback_query(call.id, '❌ درخواست گزارش نامعتبر است.', show_alert=True)
        return

    try:
        target_user_id = int(parts[2])
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '❌ شناسه کاربر نامعتبر است.', show_alert=True)
        return

    if target_user_id <= 0 or target_user_id == reporter_id:
        bot.answer_callback_query(call.id, '❌ نمی‌توانید خودتان را گزارش دهید.', show_alert=True)
        return

    # فقط وجود رابطه واقعی در جدول چت‌ها، اجازه آغاز گزارش را می‌دهد.
    try:
        if not has_user_chatted(reporter_id, target_user_id):
            bot.answer_callback_query(
                call.id,
                '❌ شما تا به حال با این کاربر چت نکرده‌اید؛ امکان ثبت گزارش وجود ندارد.',
                show_alert=True
            )
            return

        if not get_user(target_user_id):
            bot.answer_callback_query(call.id, '❌ کاربر مورد نظر یافت نشد.', show_alert=True)
            return

        if has_reported(reporter_id, target_user_id):
            bot.answer_callback_query(
                call.id,
                '⚠️ شما قبلاً این کاربر را گزارش کرده‌اید. گزارش شما در حال بررسی است.',
                show_alert=True
            )
            try:
                bot.delete_message(callback_chat.id, callback_message.message_id)
            except Exception:
                pass
            return

        if get_reports_today(reporter_id) >= MAX_REPORTS_PER_DAY:
            bot.answer_callback_query(
                call.id,
                '⚠️ سقف گزارش‌های روزانه‌ی شما پر شده است. فردا دوباره تلاش کنید.',
                show_alert=True
            )
            return

    except Exception:
        logger.exception('Could not validate report start: reporter=%s target=%s', reporter_id, target_user_id)
        bot.answer_callback_query(
            call.id,
            '⚠️ بررسی درخواست گزارش با خطا روبه‌رو شد؛ لطفاً بعداً دوباره تلاش کنید.',
            show_alert=True
        )
        return

    try:
        prompt = bot.send_message(
            reporter_id,
            '📝 لطفاً دلیل گزارش تخلف (ریپورت) خود را به صورت متن ارسال کنید:'
        )
        bot.register_next_step_handler(prompt, process_report_reason, target_user_id)
    except Exception:
        logger.exception('Could not start report reason step: reporter=%s target=%s', reporter_id, target_user_id)
        bot.answer_callback_query(
            call.id,
            '❌ شروع فرایند گزارش ممکن نشد؛ لطفاً دوباره تلاش کنید.',
            show_alert=True
        )
        return

    bot.answer_callback_query(call.id, 'لطفاً دلیل گزارش را در پیام خصوصی ارسال کنید.')
    try:
        bot.delete_message(callback_chat.id, callback_message.message_id)
    except Exception:
        pass

def process_report_reason(message, target_user_id):
    """ثبت امن دلیل گزارش؛ فقط از پیام خصوصیِ خود گزارش‌دهنده."""
    reporter_user = getattr(message, 'from_user', None)
    reporter_id = getattr(reporter_user, 'id', None)
    chat = getattr(message, 'chat', None)

    try:
        reporter_id = int(reporter_id)
        target_user_id = int(target_user_id)
    except (TypeError, ValueError):
        try:
            bot.send_message(getattr(chat, 'id', reporter_id), '❌ اطلاعات گزارش نامعتبر است؛ لطفاً دوباره اقدام کنید.')
        except Exception:
            logger.warning('Could not notify about an invalid report request', exc_info=True)
        return

    if reporter_id <= 0 or target_user_id <= 0 or target_user_id == reporter_id:
        try:
            bot.send_message(reporter_id, '❌ اطلاعات گزارش نامعتبر است.')
        except Exception:
            logger.warning('Could not notify about an invalid report target', exc_info=True)
        return

    # گزارش فقط باید در گفتگوی خصوصیِ همان گزارش‌دهنده دریافت شود.
    if getattr(chat, 'type', None) != 'private' or getattr(chat, 'id', None) != reporter_id:
        try:
            bot.clear_step_handler_by_chat_id(getattr(chat, 'id', reporter_id))
        except Exception:
            pass
        try:
            bot.send_message(reporter_id, '❌ ثبت گزارش فقط از گفتگوی خصوصی ربات امکان‌پذیر است.')
        except Exception:
            logger.warning('Could not notify about a non-private report message', exc_info=True)
        return

    update_last_seen(reporter_id)

    if getattr(message, 'content_type', None) != 'text':
        try:
            bot.clear_step_handler_by_chat_id(reporter_id)
            bot.send_message(reporter_id, '❌ لطفاً فقط متن ارسال کنید. گزارش لغو شد.')
        except Exception:
            logger.warning('Could not handle a non-text report reason', exc_info=True)
        return

    reason = (getattr(message, 'text', None) or '').strip()
    if reason in ('/cancel', '/start', 'بازگشت', 'لغو'):
        try:
            bot.clear_step_handler_by_chat_id(reporter_id)
            bot.send_message(reporter_id, '❌ گزارش لغو شد.')
        except Exception:
            logger.warning('Could not cancel report flow cleanly', exc_info=True)
        return

    if not reason:
        try:
            bot.send_message(reporter_id, '❌ دلیل گزارش نمی‌تواند خالی باشد. گزارش لغو شد؛ برای شروع دوباره از دکمهٔ گزارش استفاده کنید.')
            bot.clear_step_handler_by_chat_id(reporter_id)
        except Exception:
            logger.warning('Could not notify about an empty report reason', exc_info=True)
        return

    reason = reason[:1000]
    now = datetime.now()
    now_str = now.strftime('%Y-%m-%d %H:%M:%S')
    since = (now - timedelta(hours=24)).strftime('%Y-%m-%d %H:%M:%S')

    status = 'ok'
    target_name, target_username = 'ناشناس', None
    distinct_count = 0
    total_count = 0

    try:
        # BEGIN IMMEDIATE باعث می‌شود کنترل سقف روزانه و درج گزارش در یک
        # تراکنش سریالی انجام شوند و دو درخواست هم‌زمان از سقف عبور نکنند.
        with get_db_connection() as conn:
            conn.execute('BEGIN IMMEDIATE')
            cur = conn.cursor()

            cur.execute(
                'SELECT display_name, username FROM users WHERE user_id = ?',
                (target_user_id,)
            )
            target_row = cur.fetchone()

            if not target_row:
                status = 'nouser'
            else:
                target_name = target_row[0] or 'ناشناس'
                target_username = target_row[1]

                cur.execute(
                    '''SELECT 1 FROM chats
                       WHERE (user1_id = ? AND user2_id = ?)
                          OR (user1_id = ? AND user2_id = ?)
                       LIMIT 1''',
                    (reporter_id, target_user_id, target_user_id, reporter_id)
                )
                if not cur.fetchone():
                    status = 'nohistory'

                if status == 'ok':
                    cur.execute(
                        'SELECT 1 FROM user_reports WHERE reporter_id = ? AND target_id = ?',
                        (reporter_id, target_user_id)
                    )
                    if cur.fetchone():
                        status = 'dup'

                if status == 'ok':
                    cur.execute(
                        'SELECT COUNT(*) FROM user_reports WHERE reporter_id = ? AND created_at >= ?',
                        (reporter_id, since)
                    )
                    if cur.fetchone()[0] >= MAX_REPORTS_PER_DAY:
                        status = 'limit'

                if status == 'ok':
                    cur.execute(
                        '''INSERT OR IGNORE INTO user_reports
                           (reporter_id, target_id, reason, created_at)
                           VALUES (?, ?, ?, ?)''',
                        (reporter_id, target_user_id, reason, now_str)
                    )
                    if cur.rowcount == 0:
                        status = 'dup'
                    else:
                        cur.execute(
                            'UPDATE users SET reports_count = COALESCE(reports_count, 0) + 1 WHERE user_id = ?',
                            (target_user_id,)
                        )
                        cur.execute(
                            'SELECT COUNT(*) FROM user_reports WHERE target_id = ?',
                            (target_user_id,)
                        )
                        distinct_count = cur.fetchone()[0]
                        cur.execute(
                            'SELECT COALESCE(reports_count, 0) FROM users WHERE user_id = ?',
                            (target_user_id,)
                        )
                        count_row = cur.fetchone()
                        total_count = count_row[0] if count_row else 0

            if status == 'ok':
                conn.commit()
            else:
                conn.rollback()
    except Exception:
        logger.error('process_report_reason database operation failed', exc_info=True)
        try:
            bot.send_message(reporter_id, '❌ هنگام ثبت گزارش خطایی رخ داد. لطفاً کمی بعد دوباره تلاش کنید.')
        except Exception:
            logger.warning('Could not notify reporter about database failure', exc_info=True)
        return

    if status == 'nouser':
        bot.send_message(reporter_id, '❌ کاربر مورد نظر یافت نشد.')
        return
    if status == 'nohistory':
        bot.send_message(reporter_id, '❌ سابقهٔ چت معتبری با این کاربر پیدا نشد؛ گزارش ثبت نشد.')
        return
    if status == 'limit':
        bot.send_message(reporter_id, '⚠️ سقف گزارش‌های روزانهٔ شما پر شده است. فردا دوباره تلاش کنید.')
        return
    if status == 'dup':
        bot.send_message(reporter_id, '⚠️ شما قبلاً این کاربر را گزارش کرده‌اید.')
        return

    target_mention = f'<a href="tg://user?id={target_user_id}">{escape_html(target_name)}</a>'
    if target_username:
        target_mention += f' (@{escape_html(target_username)})'

    reporter_first_name = getattr(reporter_user, 'first_name', None) or 'کاربر'
    reporter_mention = f'<a href="tg://user?id={reporter_id}">{escape_html(reporter_first_name)}</a>'
    reporter_username = getattr(reporter_user, 'username', None)
    if reporter_username:
        reporter_mention += f' (@{escape_html(reporter_username)})'

    review_banner = ''
    if distinct_count >= REPORTS_FOR_ADMIN_REVIEW:
        review_banner = (
            f'\n🔴 <b>نیازمند بررسی:</b> این کاربر از {distinct_count} نفر مختلف گزارش گرفته است.\n'
            '(مسدودسازی خودکار نیست؛ تصمیم با شماست.)\n'
        )
    try:
        if is_blocked(target_user_id):
            review_banner += '\n🚫 <i>این کاربر هم‌اکنون مسدود است.</i>\n'
    except Exception:
        logger.warning('Could not read target blocked state while preparing report', exc_info=True)

    report_text = (
        '🚨 <b>گزارش تخلف جدید ثبت شد!</b>\n\n'
        f'👤 <b>شخص گزارش شده:</b> {target_mention}\n'
        f'   ├ <b>آیدی عددی:</b> <code>{target_user_id}</code>\n'
        f'   ├ <b>گزارش‌دهنده‌های متفاوت:</b> {distinct_count}\n'
        f'   └ <b>مجموع تاریخی گزارش‌ها:</b> {total_count}\n'
        f'{review_banner}\n'
        f'🙋 <b>گزارش‌دهنده:</b> {reporter_mention}\n'
        f'   └ <b>آیدی عددی:</b> <code>{reporter_id}</code>\n\n'
        f'💬 <b>دلیل گزارش:</b>\n{escape_html(reason)}'
    )

    markup = types.InlineKeyboardMarkup(row_width=2)
    markup.add(
        types.InlineKeyboardButton(
            '🚫 مسدود کردن کاربر',
            callback_data=f'admin_block_target_{target_user_id}'
        ),
        types.InlineKeyboardButton(
            '🔓 آزادسازی / نادیده گرفتن',
            callback_data=f'admin_ignore_rep_{target_user_id}'
        )
    )

    delivered = 0
    for admin in ADMINS:
        try:
            bot.send_message(admin, report_text, reply_markup=markup, parse_mode='HTML')
            delivered += 1
        except Exception:
            logger.warning('Could not deliver report to admin %s', admin, exc_info=True)

    if delivered == 0:
        logger.error('Report %s->%s saved but could not be delivered to any admin', reporter_id, target_user_id)
        bot.send_message(reporter_id, '✅ گزارش شما در سامانه ثبت شد؛ اما اعلان آن برای ادمین‌ها ارسال نشد.')
    else:
        bot.send_message(reporter_id, '✅ گزارش تخلف شما با موفقیت برای ادمین‌ها ارسال شد. از همکاری شما سپاسگزاریم.')

def escape_html(text):
    if not text: return ""
    return str(text).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")

# 🌟 متغیر سراسری و تابع یکپارچه ارسال کارت پروفایل با کش عکس پیش‌فرض
DEFAULT_PHOTO_FILE_ID = None

def _send_profile_card(user_id, text, markup, photo_id=None):
    """ارسال کارت پروفایل با عکس اختصاصی، عکس پیش‌فرض کش‌شده یا متن جایگزین."""
    global DEFAULT_PHOTO_FILE_ID
    if photo_id:
        try:
            bot.send_photo(user_id, photo_id, caption=text, reply_markup=markup, parse_mode='HTML')
            return True
        except Exception:
            logger.warning('Could not send user profile photo for user=%s; falling back', user_id, exc_info=True)

    if DEFAULT_PHOTO_FILE_ID:
        try:
            bot.send_photo(user_id, DEFAULT_PHOTO_FILE_ID, caption=text, reply_markup=markup, parse_mode='HTML')
            return True
        except Exception:
            logger.warning('Could not send cached default profile photo; falling back', exc_info=True)

    if not DEFAULT_PHOTO_FILE_ID:
        try:
            with open('default_profile.jpg', 'rb') as default_photo:
                photo_message = bot.send_photo(
                    user_id, default_photo, caption=text, reply_markup=markup, parse_mode='HTML'
                )
            DEFAULT_PHOTO_FILE_ID = photo_message.photo[-1].file_id
            return True
        except Exception:
            logger.warning('Could not send default profile photo; falling back to text', exc_info=True)

    bot.send_message(user_id, text, reply_markup=markup, parse_mode='HTML')
    return True

@bot.message_handler(func=lambda message: message.text == 'مشاهده پروفایل مخاطب 👤')
@_chat_message_serialized
def show_partner_profile(message):

    user = getattr(message, 'from_user', None)
    chat = getattr(message, 'chat', None)
    user_id = getattr(user, 'id', None)

    if not isinstance(user_id, int) or user_id <= 0:
        return

    # Profile data must only be disclosed in the user's own private chat with the bot.
    if chat is None or getattr(chat, 'type', None) != 'private' or getattr(chat, 'id', None) != user_id:
        try:
            bot.send_message(user_id, '⚠️ مشاهده پروفایل مخاطب فقط در گفتگوی خصوصی با ربات امکان‌پذیر است.')
        except Exception:
            logger.warning('Could not notify user about private-only profile viewing', exc_info=True)
        return

    update_last_seen(user_id)

    pair = active_pairs.get(user_id)
    if not pair or len(pair) < 3:
        bot.send_message(user_id, '⚠️ شما در حال حاضر در چت فعالی نیستید.')
        return

    try:
        partner_id, chat_id, via_link = pair
        partner_id = int(partner_id)
        chat_id = int(chat_id)
    except (TypeError, ValueError):
        bot.send_message(user_id, '⚠️ وضعیت چت معتبر نیست؛ لطفاً دوباره تلاش کنید.')
        return

    # The lifecycle decorator holds this chat's lock. Recheck both sides under
    # queue_lock so a stale/asymmetric Pair cannot expose the wrong profile.
    with queue_lock:
        current_pair = active_pairs.get(user_id)
        partner_pair = active_pairs.get(partner_id)
        pair_is_valid = bool(
            current_pair == pair
            and partner_id != user_id
            and partner_pair
            and len(partner_pair) >= 3
            and partner_pair[0] == user_id
            and partner_pair[1] == chat_id
            and partner_pair[2] == via_link
        )
        is_game_chat = chat_id in game_sessions
        already_viewed = (user_id, chat_id) in profile_viewed_tracker

    if not pair_is_valid:
        bot.send_message(user_id, '⚠️ این ارتباط دیگر فعال نیست. برای شروع چت جدید اقدام کنید.')
        return

    if is_game_chat:
        bot.send_message(
            user_id,
            '❌ <b>در طول بازی گروهی امکان مشاهده پروفایل وجود ندارد!</b>\n'
            'برای حفظ حریم خصوصی و ناشناس بودن، این بخش غیرفعال است 🥷',
            parse_mode='HTML'
        )
        return

    if already_viewed:
        bot.send_message(user_id, '❌ شما در این چت یک‌بار پروفایل مخاطب را دیده‌اید و امکان مشاهده مجدد وجود ندارد.')
        return

    try:
        partner_row = get_user(partner_id)
        if not partner_row:
            bot.send_message(user_id, '❌ اطلاعات مخاطب یافت نشد.')
            return

        profile_id = get_or_create_profile_id(partner_id)
        if not profile_id:
            bot.send_message(user_id, '❌ شناسه پروفایل مخاطب در دسترس نیست؛ لطفاً دوباره تلاش کنید.')
            return

        display_name = escape_html(partner_row[4] or 'ناشناس')
        age = escape_html(str(partner_row[6]) if partner_row[6] else 'نامشخص')
        education = escape_html(partner_row[7] or 'نامشخص')
        location = escape_html(partner_row[8] or 'نامشخص')
        gender_key = partner_row[9]
        gender_fa = GENDER_MAP.get(gender_key, (gender_key, 'O'))[0] if gender_key else 'نامشخص'
        gender_fa = escape_html(gender_fa)
        last_seen_str = partner_row[10]
        karma = partner_row[11] if partner_row[11] is not None else 0
        photo_id = partner_row[13]
        bio = escape_html(partner_row[14] or 'بیوگرافی ثبت نشده است.')

        position_key = get_user_position(partner_id)
        position_fa = POS_MAP.get(position_key, 'نامشخص')
        position_fa = escape_html(position_fa)
        last_seen_text = escape_html(format_last_seen(partner_id, last_seen_str))
        likes, dislikes = get_user_vote_stats(partner_id)
        satisfaction_text = escape_html(get_satisfaction_display(likes, dislikes))

        with get_db_connection() as conn:
            row = conn.execute(
                'SELECT COUNT(*) FROM chats WHERE user1_id = ? OR user2_id = ?',
                (partner_id, partner_id)
            ).fetchone()
            chat_count = row[0] if row else 0

        profile_text = (
            f'👤 <b>پروفایل مخاطب ناشناس</b>\n'
            f'━━━━━━━━━━━━━━━━━━━\n'
            f'🆔 <b>آیدی چت:</b> /user_{profile_id}\n'
            f'🔸 <b>نام نمایشی:</b> {display_name}\n'
            f'🔹 <b>سن:</b> {age} سال  |  <b>جنسیت:</b> {gender_fa}\n'
            f'🔹 <b>پوزیشن:</b> {position_fa}  |  <b>تحصیلات:</b> {education}\n'
            f'📍 <b>موقعیت:</b> {location}\n'
            f'✨ <b>کاریزما:</b> {karma}  |  📊 <b>رضایت:</b> {satisfaction_text}\n'
            f'👍 <b>لایک:</b> {likes}  |  👎 <b>دیس‌لایک:</b> {dislikes}\n'
            f'💬 <b>تعداد کل چت‌ها:</b> {chat_count}\n'
            f'👁‍🗨 <b>وضعیت فعالیت:</b> {last_seen_text}\n'
            f'━━━━━━━━━━━━━━━━━━━\n'
            f'❞ <b>درباره من و توضیحات:</b>\n'
            f'{bio}'
        )

        inline_markup = types.InlineKeyboardMarkup(row_width=2)
        inline_markup.add(
            types.InlineKeyboardButton(f'👍 لایک ({likes})', callback_data=f'vote_like_{partner_id}'),
            types.InlineKeyboardButton(f'👎 دیس‌لایک ({dislikes})', callback_data=f'vote_dislike_{partner_id}')
        )

        if _send_profile_card(user_id, profile_text, inline_markup, photo_id=photo_id):
            # Record the one-time view only after the profile was actually delivered.
            with queue_lock:
                if active_pairs.get(user_id) == pair:
                    profile_viewed_tracker.add((user_id, chat_id))

            try:
                bot.send_message(partner_id, '👀 <b>مخاطب شما در حال مشاهده پروفایل شماست!</b>', parse_mode='HTML')
            except Exception:
                logger.info('Could not send partner profile-view notification for chat %s', chat_id)

    except Exception:
        logger.error('show_partner_profile failed for user=%s chat=%s', user_id, chat_id, exc_info=True)
        try:
            bot.send_message(user_id, '⚠️ هنگام دریافت پروفایل خطایی رخ داد؛ لطفاً دوباره تلاش کنید.')
        except Exception:
            pass

@bot.message_handler(func=lambda message: message.text == 'ارسال آیدی من👻')
def ask_send_id(message):
    user_id = int(message.from_user.id)
    update_last_seen(user_id)

    # This action is only available from the user's own private chat with the bot.
    if getattr(getattr(message, 'chat', None), 'type', None) != 'private' or message.chat.id != user_id:
        try:
            bot.send_message(user_id, '🚫 این گزینه فقط در گفت‌وگوی خصوصی خود ربات قابل استفاده است.')
        except Exception:
            logger.warning('could not send private-chat requirement for identity share', exc_info=True)
        return

    # Take a consistent snapshot of the active pair without making network calls
    # while queue_lock is held.
    with queue_lock:
        pair_snapshot = active_pairs.get(user_id)
        if not pair_snapshot:
            partner_id = None
            chat_id = None
            pair_valid = False
        else:
            partner_id, chat_id, _via_link = pair_snapshot
            partner_snapshot = active_pairs.get(partner_id)
            pair_valid = bool(
                partner_id != user_id
                and partner_snapshot
                and partner_snapshot[0] == user_id
                and partner_snapshot[1] == chat_id
            )

    if not pair_valid:
        bot.send_message(user_id, '⚠️ شما در حال حاضر چت فعال و معتبری ندارید.')
        return

    lifecycle_lock = chat_lifecycle_locks[int(chat_id) % len(chat_lifecycle_locks)]
    with lifecycle_lock:
        # Re-check both sides under the lifecycle lock. A simultaneous end/re-pair
        # must not produce an identity-sharing prompt for the wrong partner.
        with queue_lock:
            current_pair = active_pairs.get(user_id)
            current_partner_pair = active_pairs.get(partner_id)
            still_same_pair = bool(
                current_pair == pair_snapshot
                and current_partner_pair
                and current_partner_pair[0] == user_id
                and current_partner_pair[1] == chat_id
            )

        if not still_same_pair:
            bot.send_message(user_id, '⚠️ وضعیت چت تغییر کرده است؛ لطفاً دوباره تلاش کنید.')
            return

        # Identity sharing is disabled unless BOTH profiles explicitly have
        # a recorded age of 18 or above. Unknown age is not treated as adult.
        try:
            sender_row = get_user(user_id)
            partner_row = get_user(partner_id)
            sender_age = int(sender_row[6]) if sender_row and sender_row[6] is not None else None
            partner_age = int(partner_row[6]) if partner_row and partner_row[6] is not None else None
        except Exception:
            logger.error(
                'could not validate profile ages before identity-share prompt user=%s partner=%s',
                user_id, partner_id,
                exc_info=True
            )
            sender_age = None
            partner_age = None

        if sender_age is None or partner_age is None or sender_age < 18 or partner_age < 18:
            bot.send_message(
                user_id,
                '🔒 برای حفظ حریم خصوصی، درخواست تبادل آیدی فقط زمانی فعال است که سن ثبت‌شدهٔ هر دو حساب ۱۸ سال یا بیشتر باشد.'
            )
            return

        markup = types.InlineKeyboardMarkup(row_width=2)
        markup.add(
            types.InlineKeyboardButton(
                'بله، ارسال کن ✅',
                callback_data=f'sendid_yes_{chat_id}_{partner_id}'
            ),
            types.InlineKeyboardButton(
                'خیر، لغو ❌',
                callback_data=f'sendid_no_{chat_id}_{partner_id}'
            )
        )

        try:
            bot.send_message(
                user_id,
                '⚠️ <b>آیا مطمئن هستید؟</b>\n'
                'با تأیید این درخواست، آیدی تلگرام یا لینک حساب شما برای مخاطب همین چت ارسال می‌شود و ناشناس‌بودن شما از بین می‌رود.\n\n'
                'این دکمه فقط برای همین چت معتبر است.',
                reply_markup=markup,
                parse_mode='HTML'
            )
        except Exception:
            logger.error(
                'could not send identity-share confirmation prompt user=%s chat=%s',
                user_id, chat_id,
                exc_info=True
            )
            try:
                bot.send_message(user_id, '❌ نمایش پیام تأیید ارسال آیدی ناموفق بود؛ هیچ اطلاعاتی ارسال نشد.')
            except Exception:
                pass

@bot.callback_query_handler(func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('sendid_'))
def confirm_send_id(call):
    """Share account information only after consent for the exact live adult pair."""
    user_id = int(call.from_user.id)
    parts = (getattr(call, 'data', None) or '').split('_')

    if len(parts) != 4 or parts[0] != 'sendid' or parts[1] not in ('yes', 'no'):
        try:
            bot.answer_callback_query(call.id, '🚫 این دکمه قدیمی، نامعتبر یا منقضی است.', show_alert=True)
        except Exception:
            pass
        return

    action = parts[1]
    try:
        expected_chat_id = int(parts[2])
        expected_partner_id = int(parts[3])
        if expected_chat_id <= 0 or expected_partner_id <= 0 or expected_partner_id == user_id:
            raise ValueError('invalid pair identifiers')
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 اطلاعات دکمه معتبر نیست.', show_alert=True)
        return

    # Confirmation must come from the user's private conversation with the bot.
    callback_message = getattr(call, 'message', None)
    callback_chat = getattr(callback_message, 'chat', None)
    if (
        callback_chat is None
        or getattr(callback_chat, 'type', None) != 'private'
        or callback_chat.id != user_id
    ):
        bot.answer_callback_query(
            call.id,
            '🚫 این دکمه فقط از گفت‌وگوی خصوصی خودتان قابل استفاده است.',
            show_alert=True
        )
        return

    # Verify the IDs embedded in this button against BOTH sides of the current pair.
    with queue_lock:
        pair_snapshot = active_pairs.get(user_id)
        partner_snapshot = active_pairs.get(expected_partner_id)
        pair_valid = bool(
            pair_snapshot
            and pair_snapshot[0] == expected_partner_id
            and pair_snapshot[1] == expected_chat_id
            and partner_snapshot
            and partner_snapshot[0] == user_id
            and partner_snapshot[1] == expected_chat_id
        )

    if not pair_valid:
        bot.answer_callback_query(
            call.id,
            '⚠️ این درخواست مربوط به چت قبلی است یا چت فعلی تغییر کرده است.',
            show_alert=True
        )
        try:
            bot.delete_message(callback_chat.id, callback_message.message_id)
        except Exception:
            pass
        return

    lifecycle_lock = chat_lifecycle_locks[int(expected_chat_id) % len(chat_lifecycle_locks)]
    with lifecycle_lock:
        # Final re-check prevents a race with termination or a new pairing.
        with queue_lock:
            current_pair = active_pairs.get(user_id)
            current_partner_pair = active_pairs.get(expected_partner_id)
            still_same_pair = bool(
                current_pair == pair_snapshot
                and current_partner_pair
                and current_partner_pair[0] == user_id
                and current_partner_pair[1] == expected_chat_id
            )

        if not still_same_pair:
            bot.answer_callback_query(
                call.id,
                '⚠️ چت تغییر کرده یا پایان یافته است؛ هیچ اطلاعاتی ارسال نشد.',
                show_alert=True
            )
            try:
                bot.delete_message(callback_chat.id, callback_message.message_id)
            except Exception:
                pass
            return

        try:
            bot.delete_message(callback_chat.id, callback_message.message_id)
        except Exception:
            pass

        if action == 'no':
            bot.answer_callback_query(call.id, '❌ ارسال آیدی لغو شد.')
            return

        # Both profiles must have a recorded adult age. Unknown age fails closed.
        try:
            sender_row = get_user(user_id)
            partner_row = get_user(expected_partner_id)
            sender_age = int(sender_row[6]) if sender_row and sender_row[6] is not None else None
            partner_age = int(partner_row[6]) if partner_row and partner_row[6] is not None else None
        except Exception:
            logger.error(
                'could not validate profile ages for identity share user=%s partner=%s',
                user_id, expected_partner_id,
                exc_info=True
            )
            sender_age = None
            partner_age = None

        if sender_age is None or partner_age is None or sender_age < 18 or partner_age < 18:
            bot.answer_callback_query(
                call.id,
                '🔒 به دلیل سن ثبت‌شده یا نامشخص بودن سن یکی از دو حساب، آیدی ارسال نشد.',
                show_alert=True
            )
            try:
                bot.send_message(user_id, '🔒 تبادل آیدی برای این چت در دسترس نیست.')
            except Exception:
                pass
            return

        sender = call.from_user
        username = getattr(sender, 'username', None)
        first_name = getattr(sender, 'first_name', None) or 'کاربر'
        if username:
            contact_info = f'@{escape_html(username)}'
        else:
            safe_name = escape_html(first_name)
            contact_info = f'<a href="tg://user?id={user_id}">👤 {safe_name} (کلیک کنید)</a>'

        text_for_partner = (
            '👻 <b>مخاطب شما با تأیید خودش، اطلاعات حسابش را ارسال کرد:</b>\n\n'
            f'👉 {contact_info}'
        )

        is_secure = expected_chat_id in secure_chats
        try:
            sent_to_partner = bot.send_message(
                expected_partner_id,
                text_for_partner,
                parse_mode='HTML',
                protect_content=is_secure
            )

            if is_secure:
                chat_msg_tracker[expected_chat_id][expected_partner_id].append(sent_to_partner.message_id)

            bot.answer_callback_query(
                call.id,
                '✅ آیدی شما با موفقیت برای فرد مقابل ارسال شد.',
                show_alert=True
            )

            confirmation = bot.send_message(
                user_id,
                '✅ آیدی/اکانت شما در چت ارسال شد.',
                protect_content=is_secure
            )
            if is_secure:
                chat_msg_tracker[expected_chat_id][user_id].append(confirmation.message_id)

        except SecureTrackerPersistenceError:
            logger.critical(
                'secure identity-share tracking failed user=%s partner=%s chat=%s',
                user_id, expected_partner_id, expected_chat_id,
                exc_info=True
            )
            try:
                bot.answer_callback_query(
                    call.id,
                    '⚠️ خطای ذخیره‌سازی رخ داد؛ چت امن برای حفظ حریم خصوصی پایان می‌یابد.',
                    show_alert=True
                )
            except Exception:
                pass

        except Exception:
            logger.error(
                'identity share failed user=%s partner=%s chat=%s',
                user_id, expected_partner_id, expected_chat_id,
                exc_info=True
            )
            try:
                bot.answer_callback_query(
                    call.id,
                    '❌ ارسال آیدی ناموفق بود؛ وضعیت چت تغییر نکرد.',
                    show_alert=True
                )
            except Exception:
                pass


# ============================================================================
# 🤝 سیستم پیشرفته «دوستیابی دوطرفه ناشناس (Friend List)» بدون افشای آیدی تلگرام
# ============================================================================
FRIEND_REQUEST_TTL_SECONDS = 1800  # ۳۰ دقیقه اعتبار برای تایید درخواست دوستی
FRIEND_CHAT_COOLDOWN_SECONDS = 60  # کول‌داون درخواست چت رایگان بین دوستان
FRIEND_DM_COOLDOWN_SECONDS = 15    # کول‌داون ارسال پیام دایرکت رایگان بین دوستان
FRIENDS_PER_PAGE = 8

_friend_action_lock = threading.Lock()
_friend_dm_last_sent = {}
_friend_chat_last_sent = {}


def _canonical_friend_pair(u1, u2):
    u1, u2 = int(u1), int(u2)
    if u1 == u2:
        raise ValueError('cannot form friendship with self')
    return (u1, u2) if u1 < u2 else (u2, u1)


def are_users_friends(user1_id, user2_id):
    """بررسی سریع دوستی دوطرفه بین دو کاربر."""
    try:
        u1, u2 = _canonical_friend_pair(user1_id, user2_id)
    except (TypeError, ValueError):
        return False
    try:
        with get_db_connection() as conn:
            row = conn.execute(
                'SELECT 1 FROM friendships WHERE user1_id = ? AND user2_id = ?',
                (u1, u2)
            ).fetchone()
            return row is not None
    except Exception:
        logger.error('are_users_friends query failed', exc_info=True)
        return False


def get_friend_alias(viewer_id, friend_id):
    """دریافت نام مستعار/یادداشت شخصی که viewer برای friend تنظیم کرده است."""
    try:
        u1, u2 = _canonical_friend_pair(viewer_id, friend_id)
        col = 'alias_by_user1' if int(viewer_id) == u1 else 'alias_by_user2'
        with get_db_connection() as conn:
            row = conn.execute(
                f'SELECT {col} FROM friendships WHERE user1_id = ? AND user2_id = ?',
                (u1, u2)
            ).fetchone()
            return (row[0] or '').strip() if row and row[0] else None
    except Exception:
        logger.error('get_friend_alias failed', exc_info=True)
        return None


def set_friend_alias(viewer_id, friend_id, alias_text):
    """ذخیره یا حذف نام مستعار شخصی برای یک دوست بدون تغییر نام اصلی او."""
    try:
        u1, u2 = _canonical_friend_pair(viewer_id, friend_id)
    except (TypeError, ValueError):
        return False

    clean_alias = (alias_text or '').strip()[:40] or None
    col = 'alias_by_user1' if int(viewer_id) == u1 else 'alias_by_user2'
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute(
            f'UPDATE friendships SET {col} = ? WHERE user1_id = ? AND user2_id = ?',
            (clean_alias, u1, u2)
        )
        conn.commit()
        return cur.rowcount == 1


def remove_friendship(user1_id, user2_id):
    """حذف دوطرفه رابطه دوستی و لغو درخواست‌های دوستی معلق بین دو کاربر."""
    try:
        u1, u2 = _canonical_friend_pair(user1_id, user2_id)
    except (TypeError, ValueError):
        return False

    now_ts = time.time()
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')
        cur.execute(
            'DELETE FROM friendships WHERE user1_id = ? AND user2_id = ?',
            (u1, u2)
        )
        removed = cur.rowcount == 1
        cur.execute(
            '''UPDATE friend_requests
               SET status = 'cancelled', updated_at = ?
               WHERE ((sender_id = ? AND receiver_id = ?) OR (sender_id = ? AND receiver_id = ?))
                 AND status = 'pending' ''',
            (now_ts, u1, u2, u2, u1)
        )
        conn.commit()
        return removed


def _validate_same_age_band(user1_row, user2_row):
    """اطمینان از اینکه هر دو کاربر پروفایل کامل و رده سنی یکسان (زیر ۱۸ یا بالای ۱۸) دارند."""
    if not user1_row or not user2_row:
        return False, '❌ اطلاعات یکی از کاربران یافت نشد.'
    try:
        age1 = int(user1_row[6])
        age2 = int(user2_row[6])
    except (TypeError, ValueError):
        return False, '⚠️ سن پروفایل یکی از طرفین معتبر نیست.'
    if (age1 >= 18) != (age2 >= 18):
        return False, '🔒 امکان برقراری ارتباط یا دوستی بین دو رده سنی متفاوت وجود ندارد.'
    return True, None


def _notify_friendship_established(u1, u2):
    """ارسال اعلان دوستی دوطرفه بدون افشای آیدی تلگرام به هر دو کاربر."""
    for target_uid, other_uid in ((u1, u2), (u2, u1)):
        try:
            other_row = get_user(other_uid)
            other_name = escape_html((other_row[4] if other_row else None) or 'کاربر ناشناس')
            other_prof_id = get_or_create_profile_id(other_uid)
            markup = types.InlineKeyboardMarkup(row_width=1)
            markup.add(
                types.InlineKeyboardButton(
                    f'🤝 مشاهده کارت دوستی ({other_name})',
                    callback_data=f'friend_view_{other_uid}_0'
                )
            )
            text = (
                '🎉🤝 <b>تبریک! شما به لیست دوستان یکدیگر اضافه شدید!</b>\n'
                '━━━━━━━━━━━━━━━━━━━\n'
                f'👤 <b>دوست جدید شما:</b> {other_name}\n'
                f'🆔 <b>کد پروفایل در ربات:</b> /user_{other_prof_id}\n\n'
                '🔒 <b>حریم خصوصی کامل:</b> آیدی یا شماره تلگرام هیچ‌کدام از شما فاش نشده است.\n'
                '✨ از این پس هر زمان بخواهید می‌توانید از منوی اصلی روی <b>«دوستان من 🤝»</b> بزنید و:\n'
                '• وضعیت آنلاین/چت همدیگر را ببینید\n'
                '• <b>درخواست چت رایگان</b> برای هم بفرستید\n'
                '• <b>پیام دایرکت رایگان</b> داخل ربات ارسال کنید\n'
                '• برای دوستتان نام مستعار شخصی بگذارید'
            )
            bot.send_message(target_uid, text, reply_markup=markup, parse_mode='HTML')
        except Exception:
            logger.warning('could not send friendship celebration notice to user=%s', target_uid, exc_info=True)


def _initiate_friend_request(sender_id, receiver_id, chat_id, *, is_in_chat=True):
    """
    ثبت امن درخواست دوستی یا تکمیل خودکار در صورت وجود درخواست متقابل.
    خروجی: (status_code, message_text)
    status_code in {'error', 'already_friends', 'pending_exists', 'auto_matched', 'sent'}
    """
    sender_id = int(sender_id)
    receiver_id = int(receiver_id)
    chat_id = int(chat_id)

    if sender_id <= 0 or receiver_id <= 0 or sender_id == receiver_id:
        return 'error', '❌ درخواست نامعتبر است.'

    sender_row = get_user(sender_id)
    receiver_row = get_user(receiver_id)
    if (
        not sender_row
        or not receiver_row
        or is_blocked(sender_id)
        or is_blocked(receiver_id)
        or are_users_blocked_mutually(sender_id, receiver_id)
    ):
        return 'error', '❌ امکان ارسال درخواست دوستی برای این کاربر وجود ندارد.'

    if not is_profile_complete(sender_id) or not is_profile_complete(receiver_id):
        return 'error', '⚠️ برای استفاده از سیستم دوستان، پروفایل هر دو نفر باید کامل باشد.'

    age_ok, age_err = _validate_same_age_band(sender_row, receiver_row)
    if not age_ok:
        return 'error', age_err

    u_min, u_max = _canonical_friend_pair(sender_id, receiver_id)
    now_ts = time.time()
    now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    outcome = None

    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')

        # ۱. بررسی اینکه آیا از قبل دوست هستند؟
        cur.execute(
            'SELECT 1 FROM friendships WHERE user1_id = ? AND user2_id = ?',
            (u_min, u_max)
        )
        if cur.fetchone():
            conn.rollback()
            return 'already_friends', '🤝 این مخاطب همین الان هم در لیست دوستان شما قرار دارد!'

        # ۲. بررسی اینکه آیا طرف مقابل قبلاً در همین چت (یا اخیراً) درخواست دوستی معلق برای ما فرستاده است؟
        cur.execute(
            '''SELECT chat_id, created_at FROM friend_requests
               WHERE sender_id = ? AND receiver_id = ? AND status = 'pending'
               ORDER BY updated_at DESC LIMIT 1''',
            (receiver_id, sender_id)
        )
        reverse_req = cur.fetchone()
        if reverse_req and (now_ts - float(reverse_req[1] or 0) <= FRIEND_REQUEST_TTL_SECONDS):
            # تایید دوطرفه خودکار!
            cur.execute(
                '''UPDATE friend_requests
                   SET status = 'accepted', updated_at = ?
                   WHERE ((sender_id = ? AND receiver_id = ?) OR (sender_id = ? AND receiver_id = ?))
                     AND status = 'pending' ''',
                (now_ts, sender_id, receiver_id, receiver_id, sender_id)
            )
            cur.execute(
                '''INSERT OR IGNORE INTO friendships (user1_id, user2_id, created_at, origin_chat_id)
                   VALUES (?, ?, ?, ?)''',
                (u_min, u_max, now_str, chat_id)
            )
            conn.commit()
            outcome = 'auto_matched'
        else:
            # ۳. بررسی درخواست قبلی خود فرستنده در این چت
            cur.execute(
                '''SELECT status, updated_at FROM friend_requests
                   WHERE chat_id = ? AND sender_id = ? AND receiver_id = ?''',
                (chat_id, sender_id, receiver_id)
            )
            existing = cur.fetchone()
            if existing:
                ex_status, ex_updated = existing[0], float(existing[1] or 0)
                if ex_status == 'pending' and (now_ts - ex_updated < FRIEND_REQUEST_TTL_SECONDS):
                    conn.rollback()
                    return 'pending_exists', '⏳ درخواست دوستی شما قبلاً برای مخاطب ارسال شده و منتظر تأیید اوست.'
                if ex_status == 'rejected':
                    conn.rollback()
                    return 'error', '❌ مخاطب قبلاً درخواست دوستی در این چت را رد کرده است.'

            cur.execute(
                '''INSERT INTO friend_requests (chat_id, sender_id, receiver_id, status, created_at, updated_at)
                   VALUES (?, ?, ?, 'pending', ?, ?)
                   ON CONFLICT(chat_id, sender_id, receiver_id)
                   DO UPDATE SET status = 'pending', updated_at = excluded.updated_at''',
                (chat_id, sender_id, receiver_id, now_ts, now_ts)
            )
            conn.commit()
            outcome = 'sent'

    if outcome == 'auto_matched':
        _notify_friendship_established(sender_id, receiver_id)
        return 'auto_matched', '🎉 درخواست دوستی متقابل تایید شد و به لیست دوستان هم اضافه شدید!'

    # ارسال کارت تایید دوستی برای طرف مقابل
    sender_name = escape_html(sender_row[4] or 'مخاطب ناشناس')
    markup = types.InlineKeyboardMarkup(row_width=2)
    markup.add(
        types.InlineKeyboardButton(
            'قبول دوستی 🤝✅',
            callback_data=f'friendreq_yes_{chat_id}_{sender_id}'
        ),
        types.InlineKeyboardButton(
            'رد درخواست ❌',
            callback_data=f'friendreq_no_{chat_id}_{sender_id}'
        )
    )

    context_line = (
        f'مخاطب فعلی شما (<b>{sender_name}</b>) در این چت پیشنهاد داده که به <b>«لیست دوستان 🤝»</b> هم اضافه شوید.'
        if is_in_chat else
        f'مخاطب چت قبلی شما (<b>{sender_name}</b>) پیشنهاد داده که به <b>«لیست دوستان 🤝»</b> هم اضافه شوید.'
    )
    prompt_text = (
        '🤝 <b>درخواست دوستی ناشناس (بدون افشای آیدی تلگرام)</b>\n'
        '━━━━━━━━━━━━━━━━━━━\n'
        f'{context_line}\n\n'
        '🔒 <b>حریم خصوصی شما ۱۰۰٪ محفوظ است:</b>\n'
        'با پذیرش این درخواست، <b>آیدی یا اکانت تلگرام هیچ‌کدام از شما لو نمی‌رود!</b> '
        'فقط به لیست دوستان داخل ربات اضافه می‌شوید تا بعداً بتوانید بدون هزینه به هم درخواست چت یا پیام بدهید.\n\n'
        '👇 آیا تمایل دارید به لیست دوستان هم اضافه شوید؟'
    )

    is_secure = is_in_chat and (chat_id in secure_chats)
    try:
        sent_prompt = bot.send_message(
            receiver_id,
            prompt_text,
            reply_markup=markup,
            parse_mode='HTML',
            protect_content=is_secure
        )
        if is_secure and sent_prompt:
            chat_msg_tracker[chat_id][receiver_id].append(sent_prompt.message_id)
    except SecureTrackerPersistenceError:
        logger.critical(
            'secure friend-request prompt tracking failed sender=%s receiver=%s chat=%s',
            sender_id, receiver_id, chat_id,
            exc_info=True
        )
        return 'error', '⚠️ خطای ذخیره‌سازی در چت امن رخ داد؛ چت برای حفظ حریم خصوصی بسته می‌شود.'
    except Exception:
        logger.warning('could not deliver friend request prompt %s -> %s', sender_id, receiver_id, exc_info=True)
        with get_db_connection() as conn:
            conn.execute(
                '''UPDATE friend_requests SET status = 'cancelled', updated_at = ?
                   WHERE chat_id = ? AND sender_id = ? AND receiver_id = ? AND status = 'pending' ''',
                (time.time(), chat_id, sender_id, receiver_id)
            )
            conn.commit()
        return 'error', '❌ ارسال درخواست دوستی ممکن نشد (مخاطب در دسترس نیست).'

    return (
        'sent',
        '🤝✅ <b>درخواست دوستی ناشناس برای مخاطب ارسال شد!</b>\n'
        '🔒 در صورت تایید طرف مقابل، بدون فاش شدن آیدی تلگرام به لیست <b>«دوستان من 🤝»</b> اضافه خواهید شد.'
    )


@bot.message_handler(func=lambda message: message.text == 'افزودن به لیست دوستان 🤝')
def request_add_friend_in_chat(message):
    """ارسال درخواست دوستی دوطرفه در حین چت فعال."""
    user = getattr(message, 'from_user', None)
    chat = getattr(message, 'chat', None)
    user_id = getattr(user, 'id', None)
    if not isinstance(user_id, int) or user_id <= 0:
        return

    if chat is None or getattr(chat, 'type', None) != 'private' or getattr(chat, 'id', None) != user_id:
        try:
            bot.send_message(user_id, '🚫 این گزینه فقط در گفت‌وگوی خصوصی با ربات قابل استفاده است.')
        except Exception:
            pass
        return

    update_last_seen(user_id)

    with queue_lock:
        pair_snapshot = active_pairs.get(user_id)
        if not pair_snapshot:
            partner_id = None
            chat_id = None
            pair_valid = False
        else:
            partner_id, chat_id, _via_link = pair_snapshot
            partner_snapshot = active_pairs.get(partner_id)
            pair_valid = bool(
                partner_id != user_id
                and partner_snapshot
                and partner_snapshot[0] == user_id
                and partner_snapshot[1] == chat_id
            )

    if not pair_valid:
        bot.send_message(user_id, '⚠️ شما در حال حاضر چت فعالی ندارید.')
        return

    lifecycle_lock = chat_lifecycle_locks[int(chat_id) % len(chat_lifecycle_locks)]
    with lifecycle_lock:
        with queue_lock:
            current_pair = active_pairs.get(user_id)
            current_partner_pair = active_pairs.get(partner_id)
            still_same_pair = bool(
                current_pair == pair_snapshot
                and current_partner_pair
                and current_partner_pair[0] == user_id
                and current_partner_pair[1] == chat_id
            )

        if not still_same_pair:
            bot.send_message(user_id, '⚠️ وضعیت چت تغییر کرده است؛ لطفاً دوباره تلاش کنید.')
            return

        status_code, reply_text = _initiate_friend_request(
            user_id, partner_id, chat_id, is_in_chat=True
        )
        if status_code != 'auto_matched':
            is_secure = int(chat_id) in secure_chats
            try:
                ack_msg = bot.send_message(
                    user_id,
                    reply_text,
                    parse_mode='HTML',
                    protect_content=is_secure
                )
                if is_secure and ack_msg:
                    chat_msg_tracker[int(chat_id)][user_id].append(ack_msg.message_id)
            except SecureTrackerPersistenceError:
                logger.critical('secure friend request ack tracking failed chat=%s', chat_id, exc_info=True)
            except Exception:
                logger.warning('could not send friend request ack to user=%s', user_id, exc_info=True)


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('friend_postchat_')
)
def request_add_friend_postchat(call):
    """ارسال درخواست دوستی از روی کارت نظرسنجی بلافاصله پس از پایان چت."""
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    if (
        not isinstance(user_id, int)
        or user_id <= 0
        or chat is None
        or getattr(chat, 'type', None) != 'private'
        or getattr(chat, 'id', None) != user_id
    ):
        bot.answer_callback_query(call.id, '🚫 این دکمه فقط در پیام خصوصی خودتان معتبر است.', show_alert=True)
        return

    parts = (call.data or '').split('_')
    if len(parts) != 4:
        bot.answer_callback_query(call.id, '🚫 دکمه نامعتبر است.', show_alert=True)
        return

    try:
        chat_id = int(parts[2])
        target_id = int(parts[3])
        if chat_id <= 0 or target_id <= 0 or target_id == user_id:
            raise ValueError('invalid ids')
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 اطلاعات دکمه معتبر نیست.', show_alert=True)
        return

    update_last_seen(user_id)

    # اعتبارسنجی اینکه این دو کاربر واقعاً طرفین همین chat_id بوده‌اند
    with get_db_connection() as conn:
        row = conn.execute(
            '''SELECT 1 FROM chats
               WHERE chat_id = ?
                 AND ((user1_id = ? AND user2_id = ?) OR (user1_id = ? AND user2_id = ?))''',
            (chat_id, user_id, target_id, target_id, user_id)
        ).fetchone()
    if not row:
        bot.answer_callback_query(call.id, '🚫 این دکمه متعلق به چت شما نیست.', show_alert=True)
        return

    lifecycle_lock = chat_lifecycle_locks[int(chat_id) % len(chat_lifecycle_locks)]
    with lifecycle_lock:
        status_code, reply_text = _initiate_friend_request(
            user_id, target_id, chat_id, is_in_chat=False
        )

    clean_alert = re.sub(r'<[^>]+>', '', reply_text)
    bot.answer_callback_query(call.id, clean_alert[:190], show_alert=True)


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('friendreq_')
)
def handle_friend_request_decision(call):
    """پاسخ به درخواست دوستی (قبول یا رد) با بررسی کامل امنیتی و اتمیک."""
    receiver_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None

    if (
        not isinstance(receiver_id, int)
        or receiver_id <= 0
        or chat is None
        or getattr(chat, 'type', None) != 'private'
        or getattr(chat, 'id', None) != receiver_id
    ):
        bot.answer_callback_query(call.id, '🚫 این دکمه فقط از گفت‌وگوی خصوصی خودتان قابل استفاده است.', show_alert=True)
        return

    parts = (call.data or '').split('_')
    if len(parts) != 4 or parts[0] != 'friendreq' or parts[1] not in ('yes', 'no'):
        bot.answer_callback_query(call.id, '🚫 دکمه نامعتبر است.', show_alert=True)
        return

    action = parts[1]
    try:
        chat_id = int(parts[2])
        sender_id = int(parts[3])
        if chat_id <= 0 or sender_id <= 0 or sender_id == receiver_id:
            raise ValueError('invalid friendreq params')
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 اطلاعات درخواست معتبر نیست.', show_alert=True)
        return

    update_last_seen(receiver_id)

    sender_row = get_user(sender_id)
    receiver_row = get_user(receiver_id)
    if (
        not sender_row
        or not receiver_row
        or is_blocked(sender_id)
        or is_blocked(receiver_id)
        or are_users_blocked_mutually(sender_id, receiver_id)
    ):
        bot.answer_callback_query(call.id, '❌ امکان برقراری دوستی با این کاربر وجود ندارد.', show_alert=True)
        try:
            bot.delete_message(chat.id, message.message_id)
        except Exception:
            pass
        return

    age_ok, age_err = _validate_same_age_band(sender_row, receiver_row)
    if not age_ok:
        bot.answer_callback_query(call.id, age_err, show_alert=True)
        try:
            bot.delete_message(chat.id, message.message_id)
        except Exception:
            pass
        return

    u_min, u_max = _canonical_friend_pair(sender_id, receiver_id)
    now_ts = time.time()
    now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    result_state = None

    lifecycle_lock = chat_lifecycle_locks[int(chat_id) % len(chat_lifecycle_locks)]
    with lifecycle_lock:
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('BEGIN IMMEDIATE')

            cur.execute(
                '''SELECT status, updated_at FROM friend_requests
                   WHERE chat_id = ? AND sender_id = ? AND receiver_id = ?''',
                (chat_id, sender_id, receiver_id)
            )
            req_row = cur.fetchone()
            if not req_row or req_row[0] != 'pending':
                conn.rollback()
                result_state = 'not_pending'
            elif now_ts - float(req_row[1] or 0) > FRIEND_REQUEST_TTL_SECONDS:
                cur.execute(
                    '''UPDATE friend_requests SET status = 'expired', updated_at = ?
                       WHERE chat_id = ? AND sender_id = ? AND receiver_id = ? AND status = 'pending' ''',
                    (now_ts, chat_id, sender_id, receiver_id)
                )
                conn.commit()
                result_state = 'expired'
            elif action == 'no':
                cur.execute(
                    '''UPDATE friend_requests SET status = 'rejected', updated_at = ?
                       WHERE chat_id = ? AND sender_id = ? AND receiver_id = ? AND status = 'pending' ''',
                    (now_ts, chat_id, sender_id, receiver_id)
                )
                conn.commit()
                result_state = 'rejected'
            else:
                cur.execute(
                    '''UPDATE friend_requests
                       SET status = 'accepted', updated_at = ?
                       WHERE ((sender_id = ? AND receiver_id = ?) OR (sender_id = ? AND receiver_id = ?))
                         AND status = 'pending' ''',
                    (now_ts, sender_id, receiver_id, receiver_id, sender_id)
                )
                cur.execute(
                    '''INSERT OR IGNORE INTO friendships (user1_id, user2_id, created_at, origin_chat_id)
                       VALUES (?, ?, ?, ?)''',
                    (u_min, u_max, now_str, chat_id)
                )
                conn.commit()
                result_state = 'accepted'

    try:
        bot.delete_message(chat.id, message.message_id)
    except Exception:
        pass

    if result_state == 'not_pending':
        bot.answer_callback_query(call.id, '⚠️ این درخواست قبلاً بررسی یا منقضی شده است.', show_alert=True)
        return

    if result_state == 'expired':
        bot.answer_callback_query(call.id, '⏳ مهلت پاسخ به این درخواست دوستی تمام شده است.', show_alert=True)
        return

    if result_state == 'rejected':
        bot.answer_callback_query(call.id, '❌ درخواست دوستی رد شد.')
        try:
            bot.send_message(sender_id, 'ℹ️ مخاطب تمایلی به اضافه شدن به لیست دوستان نداشت.')
        except Exception:
            pass
        return

    bot.answer_callback_query(call.id, '🤝✅ درخواست دوستی پذیرفته شد!')
    _notify_friendship_established(sender_id, receiver_id)


def _friend_status_badge(friend_id, last_seen_str):
    """نمایش وضعیت لحظه‌ای دوست برای لیست دوستان."""
    if friend_id in active_pairs:
        return '💬 در حال چت', '💬'
    if is_user_waiting(friend_id):
        return '🔎 در صف جستجو', '🔎'
    if not last_seen_str:
        return '⚪ آفلاین', '⚪'
    try:
        last_seen_dt = datetime.strptime(last_seen_str, '%Y-%m-%d %H:%M:%S')
        diff_sec = (datetime.now() - last_seen_dt).total_seconds()
        if diff_sec < 300:
            return '🟢 آنلاین', '🟢'
        if diff_sec < 3600:
            return f'🟡 {max(1, int(diff_sec // 60))} دقیقه پیش', '🟡'
        if diff_sec < 86400:
            return f'🟠 {max(1, int(diff_sec // 3600))} ساعت پیش', '🟠'
        return f'⚪ {max(1, int(diff_sec // 86400))} روز پیش', '⚪'
    except Exception:
        return '⚪ نامشخص', '⚪'


def _render_friends_list_view(user_id, page=0):
    """ساخت متن و کیبورد شیشه‌ای صفحه لیست دوستان کاربر."""
    page = max(0, int(page))
    with get_db_connection() as conn:
        rows = conn.execute(
            '''SELECT
                   CASE WHEN f.user1_id = ? THEN f.user2_id ELSE f.user1_id END AS friend_id,
                   CASE WHEN f.user1_id = ? THEN f.alias_by_user1 ELSE f.alias_by_user2 END AS my_alias,
                   f.created_at,
                   u.display_name,
                   u.age,
                   u.gender,
                   u.last_seen,
                   u.profile_id
               FROM friendships AS f
               JOIN users AS u
                 ON u.user_id = (CASE WHEN f.user1_id = ? THEN f.user2_id ELSE f.user1_id END)
               WHERE (f.user1_id = ? OR f.user2_id = ?)
                 AND NOT EXISTS (
                     SELECT 1 FROM blocked_users AS b WHERE b.user_id = u.user_id
                 )
                 AND NOT EXISTS (
                     SELECT 1 FROM user_blocks AS ub
                     WHERE (ub.blocker_id = ? AND ub.blocked_id = u.user_id)
                        OR (ub.blocker_id = u.user_id AND ub.blocked_id = ?)
                 )
               ORDER BY u.last_seen DESC, f.created_at DESC''',
            (user_id, user_id, user_id, user_id, user_id, user_id, user_id)
        ).fetchall()

    total_friends = len(rows)
    markup = types.InlineKeyboardMarkup(row_width=1)

    if total_friends == 0:
        text = (
            '🤝 <b>لیست دوستان ناشناس شما</b>\n'
            '━━━━━━━━━━━━━━━━━━━\n'
            '📭 لیست دوستان شما در حال حاضر خالی است!\n\n'
            '💡 <b>چطور دوست اضافه کنم؟</b>\n'
            'هنگام چت با هر مخاطب (یا بلافاصله بعد از پایان چت)، کافیست دکمه <b>«افزودن به لیست دوستان 🤝»</b> را بزنید.\n'
            'اگر هر دو نفر تایید کنید، بدون اینکه آیدی تلگرامتان لو برود، به این لیست اضافه می‌شوید و می‌توانید هر زمان خواستید رایگان با هم چت کنید یا پیام بدهید!'
        )
        markup.add(
            types.InlineKeyboardButton('🔄 بروزرسانی لیست', callback_data='friends_page_0')
        )
        return text, markup

    total_pages = max(1, (total_friends + FRIENDS_PER_PAGE - 1) // FRIENDS_PER_PAGE)
    if page >= total_pages:
        page = total_pages - 1

    online_count = 0
    in_chat_count = 0
    for r in rows:
        f_uid = int(r[0])
        _status_txt, icon = _friend_status_badge(f_uid, r[6])
        if icon == '🟢':
            online_count += 1
        elif icon == '💬':
            in_chat_count += 1

    start_idx = page * FRIENDS_PER_PAGE
    page_rows = rows[start_idx:start_idx + FRIENDS_PER_PAGE]

    for r in page_rows:
        f_uid = int(r[0])
        alias = (r[1] or '').strip()
        dname = (r[3] or 'ناشناس').strip()
        age_str = f'{r[4]} ساله' if r[4] else ''
        status_txt, icon = _friend_status_badge(f_uid, r[6])
        label_name = f'{dname} ({alias})' if alias else dname
        btn_text = f'{icon} {label_name}'
        if age_str:
            btn_text += f' | {age_str}'
        btn_text += f' — {status_txt.split(" ", 1)[1] if " " in status_txt else status_txt}'
        markup.add(
            types.InlineKeyboardButton(
                btn_text[:64],
                callback_data=f'friend_view_{f_uid}_{page}'
            )
        )

    nav_buttons = []
    if page > 0:
        nav_buttons.append(
            types.InlineKeyboardButton('⬅️ صفحه قبل', callback_data=f'friends_page_{page - 1}')
        )
    if page + 1 < total_pages:
        nav_buttons.append(
            types.InlineKeyboardButton('صفحه بعد ➡️', callback_data=f'friends_page_{page + 1}')
        )
    if nav_buttons:
        markup.row(*nav_buttons)

    markup.add(
        types.InlineKeyboardButton('🔄 بروزرسانی وضعیت آنلاین', callback_data=f'friends_page_{page}')
    )

    text = (
        '🤝 <b>لیست دوستان ناشناس شما</b>\n'
        '━━━━━━━━━━━━━━━━━━━\n'
        f'👥 <b>تعداد کل دوستان:</b> <code>{total_friends} نفر</code>\n'
        f'🟢 <b>آنلاین:</b> <code>{online_count}</code>  |  💬 <b>در حال چت:</b> <code>{in_chat_count}</code>\n'
        f'📄 <b>صفحه:</b> <code>{page + 1} از {total_pages}</code>\n'
        '━━━━━━━━━━━━━━━━━━━\n'
        '🔒 <i>هویت تلگرام شما و دوستانتان کاملاً مخفی است.</i>\n'
        '👇 برای مشاهده پروفایل، ارسال <b>درخواست چت رایگان</b> یا <b>پیام دایرکت رایگان</b>، روی نام دوست خود بزنید:'
    )
    return text, markup


@bot.message_handler(func=lambda message: message.text == 'دوستان من 🤝')
def show_friends_menu(message):
    """نمایش منوی دوستان از کیبورد اصلی ربات."""
    user = getattr(message, 'from_user', None)
    chat = getattr(message, 'chat', None)
    user_id = getattr(user, 'id', None)
    if not isinstance(user_id, int) or user_id <= 0:
        return
    if chat is None or getattr(chat, 'type', None) != 'private' or getattr(chat, 'id', None) != user_id:
        return

    update_last_seen(user_id)
    if is_user_in_chat(user_id):
        bot.send_message(user_id, '⚠️ شما در حال چت هستید! ابتدا چت را پایان دهید.')
        return

    text, markup = _render_friends_list_view(user_id, page=0)
    bot.send_message(user_id, text, reply_markup=markup, parse_mode='HTML')


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('friends_page_')
)
def handle_friends_page_callback(call):
    """صفحه‌بندی و بروزرسانی لیست دوستان."""
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    if (
        not isinstance(user_id, int)
        or user_id <= 0
        or chat is None
        or getattr(chat, 'type', None) != 'private'
        or getattr(chat, 'id', None) != user_id
    ):
        bot.answer_callback_query(call.id, '🚫 این دکمه فقط در پیام خصوصی خودتان معتبر است.', show_alert=True)
        return

    try:
        page = int(call.data.split('_')[2])
    except (IndexError, TypeError, ValueError):
        page = 0

    update_last_seen(user_id)
    text, markup = _render_friends_list_view(user_id, page=page)

    try:
        if getattr(message, 'content_type', 'text') == 'text':
            bot.edit_message_text(text, chat.id, message.message_id, reply_markup=markup, parse_mode='HTML')
        else:
            bot.delete_message(chat.id, message.message_id)
            bot.send_message(chat.id, text, reply_markup=markup, parse_mode='HTML')
    except Exception:
        pass
    bot.answer_callback_query(call.id)


def _build_friend_card(user_id, friend_id, page=0):
    """ساخت متن و دکمه‌های کارت مدیریت یک دوست خاص."""
    if not are_users_friends(user_id, friend_id):
        return None, None, None

    friend_row = get_user(friend_id)
    user_row = get_user(user_id)
    if (
        not friend_row
        or not user_row
        or is_blocked(friend_id)
        or are_users_blocked_mutually(user_id, friend_id)
    ):
        return None, None, None

    age_ok, _ = _validate_same_age_band(user_row, friend_row)
    if not age_ok:
        return None, None, None

    prof_id = get_or_create_profile_id(friend_id)
    display_name = escape_html(friend_row[4] or 'ناشناس')
    alias = get_friend_alias(user_id, friend_id)
    alias_display = escape_html(alias) if alias else 'تنظیم نشده 🏷'
    age = escape_html(str(friend_row[6]) if friend_row[6] else 'نامشخص')
    edu = escape_html(friend_row[7] or 'نامشخص')
    loc = escape_html(friend_row[8] or 'نامشخص')
    gender_key = friend_row[9]
    gender_fa = escape_html(GENDER_MAP.get(gender_key, (gender_key, 'O'))[0] if gender_key else 'نامشخص')
    karma = friend_row[11] if friend_row[11] is not None else 0
    photo_id = friend_row[13]
    bio = escape_html(friend_row[14] or 'بیوگرافی ثبت نشده است.')
    pos_fa = escape_html(POS_MAP.get(get_user_position(friend_id), 'نامشخص'))
    status_text, _ = _friend_status_badge(friend_id, friend_row[10])
    likes, dislikes = get_user_vote_stats(friend_id)
    satisfaction_text = escape_html(get_satisfaction_display(likes, dislikes))

    u_min, u_max = _canonical_friend_pair(user_id, friend_id)
    with get_db_connection() as conn:
        f_row = conn.execute(
            'SELECT created_at FROM friendships WHERE user1_id = ? AND user2_id = ?',
            (u_min, u_max)
        ).fetchone()
    since_date = escape_html((f_row[0] or '').split(' ')[0] if f_row and f_row[0] else 'نامشخص')

    card_text = (
        f'🤝 <b>کارت دوست ناشناس شما</b>\n'
        f'━━━━━━━━━━━━━━━━━━━\n'
        f'👤 <b>نام نمایشی:</b> {display_name}\n'
        f'🏷 <b>نام مستعار شخصی شما:</b> {alias_display}\n'
        f'🆔 <b>کد پروفایل:</b> /user_{prof_id}\n'
        f'👁‍🗨 <b>وضعیت فعلی:</b> {status_text}\n'
        f'🔹 <b>سن:</b> {age} سال  |  <b>جنسیت:</b> {gender_fa}\n'
        f'🔹 <b>پوزیشن:</b> {pos_fa}  |  <b>تحصیلات:</b> {edu}\n'
        f'📍 <b>موقعیت:</b> {loc}\n'
        f'✨ <b>کاریزما:</b> {karma}  |  📊 <b>رضایت:</b> {satisfaction_text}\n'
        f'📅 <b>تاریخ دوستی:</b> {since_date}\n'
        f'━━━━━━━━━━━━━━━━━━━\n'
        f'❞ <b>درباره من:</b>\n{bio}'
    )

    markup = types.InlineKeyboardMarkup(row_width=2)
    markup.add(
        types.InlineKeyboardButton('💬 درخواست چت رایگان', callback_data=f'friend_chat_{friend_id}'),
        types.InlineKeyboardButton('✉️ پیام دایرکت رایگان', callback_data=f'friend_dm_{friend_id}')
    )
    markup.add(
        types.InlineKeyboardButton('🏷 تنظیم نام مستعار', callback_data=f'friend_alias_{friend_id}_{page}'),
        types.InlineKeyboardButton('🎁 هدیه سکه', callback_data=f'gift_coins_{friend_id}')
    )
    markup.add(
        types.InlineKeyboardButton(f'👍 لایک ({likes})', callback_data=f'vote_like_{friend_id}'),
        types.InlineKeyboardButton(f'👎 دیس‌لایک ({dislikes})', callback_data=f'vote_dislike_{friend_id}')
    )
    markup.add(
        types.InlineKeyboardButton('💔 حذف از دوستان', callback_data=f'friend_remask_{friend_id}_{page}'),
        types.InlineKeyboardButton('🚫 بلاک کردن', callback_data=f'block_user_{friend_id}')
    )
    markup.add(
        types.InlineKeyboardButton('🔙 بازگشت به لیست دوستان', callback_data=f'friends_page_{page}')
    )
    return card_text, markup, photo_id


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('friend_view_')
)
def handle_friend_view_callback(call):
    """نمایش کارت اختصاصی یک دوست با امکانات رایگان چت، دایرکت و نام مستعار."""
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    if (
        not isinstance(user_id, int)
        or user_id <= 0
        or chat is None
        or getattr(chat, 'type', None) != 'private'
        or getattr(chat, 'id', None) != user_id
    ):
        bot.answer_callback_query(call.id, '🚫 این دکمه فقط در پیام خصوصی خودتان معتبر است.', show_alert=True)
        return

    parts = (call.data or '').split('_')
    if len(parts) < 3:
        bot.answer_callback_query(call.id, '🚫 دکمه نامعتبر است.', show_alert=True)
        return

    try:
        friend_id = int(parts[2])
        page = int(parts[3]) if len(parts) >= 4 else 0
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 اطلاعات دوست معتبر نیست.', show_alert=True)
        return

    update_last_seen(user_id)
    card_text, markup, photo_id = _build_friend_card(user_id, friend_id, page=page)
    if not card_text:
        bot.answer_callback_query(call.id, '⚠️ این کاربر دیگر در لیست دوستان شما نیست.', show_alert=True)
        text, list_markup = _render_friends_list_view(user_id, page=page)
        try:
            bot.edit_message_text(text, chat.id, message.message_id, reply_markup=list_markup, parse_mode='HTML')
        except Exception:
            pass
        return

    bot.answer_callback_query(call.id)
    try:
        bot.delete_message(chat.id, message.message_id)
    except Exception:
        pass

    _send_profile_card(user_id, card_text, markup, photo_id=photo_id)


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('friend_alias_')
)
def handle_friend_alias_start(call):
    """شروع تنظیم نام مستعار شخصی برای یک دوست."""
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    if (
        not isinstance(user_id, int)
        or user_id <= 0
        or chat is None
        or getattr(chat, 'type', None) != 'private'
        or getattr(chat, 'id', None) != user_id
    ):
        bot.answer_callback_query(call.id, '🚫 این دکمه فقط در پیام خصوصی خودتان معتبر است.', show_alert=True)
        return

    parts = (call.data or '').split('_')
    try:
        friend_id = int(parts[2])
        page = int(parts[3]) if len(parts) >= 4 else 0
    except (IndexError, TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 دکمه نامعتبر است.', show_alert=True)
        return

    if not are_users_friends(user_id, friend_id):
        bot.answer_callback_query(call.id, '⚠️ این کاربر در لیست دوستان شما نیست.', show_alert=True)
        return

    bot.answer_callback_query(call.id)
    try:
        bot.delete_message(chat.id, message.message_id)
    except Exception:
        pass

    cancel_markup = types.InlineKeyboardMarkup()
    cancel_markup.add(
        types.InlineKeyboardButton('🗑 حذف نام مستعار فعلی', callback_data=f'friend_aliasclear_{friend_id}_{page}'),
        types.InlineKeyboardButton('🔙 انصراف', callback_data=f'friend_view_{friend_id}_{page}')
    )
    prompt = bot.send_message(
        user_id,
        '🏷 <b>تنظیم نام مستعار یا یادداشت شخصی برای دوست</b>\n\n'
        'یک نام مستعار یا یادداشت کوتاه (حداکثر ۴۰ کاراکتر) ارسال کنید تا همیشه در لیست دوستانتان کنار نام او نمایش داده شود (فقط برای خود شما قابل مشاهده است):',
        reply_markup=cancel_markup,
        parse_mode='HTML'
    )
    bot.register_next_step_handler(prompt, process_friend_alias_input, friend_id, page, prompt.message_id)


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('friend_aliasclear_')
)
def handle_friend_alias_clear(call):
    """پاک کردن نام مستعار شخصی دوست."""
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    if not isinstance(user_id, int) or chat is None or chat.id != user_id:
        return

    parts = (call.data or '').split('_')
    try:
        friend_id = int(parts[2])
        page = int(parts[3]) if len(parts) >= 4 else 0
    except (IndexError, TypeError, ValueError):
        return

    bot.clear_step_handler_by_chat_id(user_id)
    set_friend_alias(user_id, friend_id, None)
    bot.answer_callback_query(call.id, '✅ نام مستعار حذف شد.')
    call.data = f'friend_view_{friend_id}_{page}'
    handle_friend_view_callback(call)


def process_friend_alias_input(message, friend_id, page, prompt_msg_id):
    """ذخیره نام مستعار واردشده توسط کاربر."""
    user_id = message.from_user.id
    try:
        bot.delete_message(message.chat.id, message.message_id)
    except Exception:
        pass
    try:
        bot.delete_message(message.chat.id, prompt_msg_id)
    except Exception:
        pass

    if getattr(message, 'content_type', None) != 'text' or (message.text or '').strip() in (
        '/cancel', '/start', 'بازگشت', 'لغو', 'انصراف'
    ):
        bot.send_message(user_id, '❌ تغییر نام مستعار لغو شد.')
    else:
        alias_val = (message.text or '').strip()[:40]
        if set_friend_alias(user_id, friend_id, alias_val):
            bot.send_message(user_id, f'✅ نام مستعار «{escape_html(alias_val)}» با موفقیت ذخیره شد.', parse_mode='HTML')
        else:
            bot.send_message(user_id, '❌ ذخیره نام مستعار ممکن نشد.')

    card_text, markup, photo_id = _build_friend_card(user_id, friend_id, page=page)
    if card_text:
        if photo_id:
            try:
                bot.send_photo(user_id, photo_id, caption=card_text, reply_markup=markup, parse_mode='HTML')
                return
            except Exception:
                pass
        bot.send_message(user_id, card_text, reply_markup=markup, parse_mode='HTML')


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('friend_remask_')
)
def handle_friend_remove_ask(call):
    """مرحله اول تایید حذف دوست از لیست."""
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    if not isinstance(user_id, int) or chat is None or chat.id != user_id:
        bot.answer_callback_query(call.id, '🚫 دسترسی نامعتبر است.', show_alert=True)
        return

    parts = (call.data or '').split('_')
    try:
        friend_id = int(parts[2])
        page = int(parts[3]) if len(parts) >= 4 else 0
    except (IndexError, TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 دکمه نامعتبر است.', show_alert=True)
        return

    bot.answer_callback_query(call.id)
    try:
        bot.delete_message(chat.id, message.message_id)
    except Exception:
        pass

    friend_row = get_user(friend_id)
    friend_name = escape_html((friend_row[4] if friend_row else None) or 'این کاربر')
    markup = types.InlineKeyboardMarkup(row_width=2)
    markup.add(
        types.InlineKeyboardButton('💔 بله، حذف شود', callback_data=f'friend_remyes_{friend_id}_{page}'),
        types.InlineKeyboardButton('🔙 خیر، بازگشت', callback_data=f'friend_view_{friend_id}_{page}')
    )
    bot.send_message(
        user_id,
        f'⚠️ <b>آیا از حذف «{friend_name}» از لیست دوستان مطمئن هستید؟</b>\n\n'
        'با حذف دوستی، این ارتباط از لیست دوستان هر دو نفر پاک می‌شود.',
        reply_markup=markup,
        parse_mode='HTML'
    )


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('friend_remyes_')
)
def handle_friend_remove_confirm(call):
    """حذف نهایی دوست از لیست دوستان."""
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    if not isinstance(user_id, int) or chat is None or chat.id != user_id:
        bot.answer_callback_query(call.id, '🚫 دسترسی نامعتبر است.', show_alert=True)
        return

    parts = (call.data or '').split('_')
    try:
        friend_id = int(parts[2])
        page = int(parts[3]) if len(parts) >= 4 else 0
    except (IndexError, TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 دکمه نامعتبر است.', show_alert=True)
        return

    removed = remove_friendship(user_id, friend_id)
    if removed:
        bot.answer_callback_query(call.id, '✅ کاربر از لیست دوستان حذف شد.', show_alert=True)
    else:
        bot.answer_callback_query(call.id, 'ℹ️ این کاربر قبلاً از لیست دوستان حذف شده است.', show_alert=True)

    text, markup = _render_friends_list_view(user_id, page=page)
    try:
        bot.edit_message_text(text, chat.id, message.message_id, reply_markup=markup, parse_mode='HTML')
    except Exception:
        bot.send_message(chat.id, text, reply_markup=markup, parse_mode='HTML')


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('friend_chat_')
)
def handle_friend_free_chat_request(call):
    """ارسال درخواست چت رایگان (۰ سکه) به دوستی که در لیست دوستان قرار دارد."""
    sender_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    if (
        not isinstance(sender_id, int)
        or sender_id <= 0
        or chat is None
        or getattr(chat, 'type', None) != 'private'
        or getattr(chat, 'id', None) != sender_id
    ):
        bot.answer_callback_query(call.id, '🚫 این دکمه فقط در پیام خصوصی خودتان معتبر است.', show_alert=True)
        return

    try:
        receiver_id = int(call.data.split('_')[2])
    except (IndexError, TypeError, ValueError):
        bot.answer_callback_query(call.id, '❌ درخواست نامعتبر است.', show_alert=True)
        return

    if sender_id == receiver_id or not are_users_friends(sender_id, receiver_id):
        bot.answer_callback_query(call.id, '⚠️ این کاربر در لیست دوستان شما قرار ندارد.', show_alert=True)
        return

    sender_user = get_user(sender_id)
    receiver_user = get_user(receiver_id)
    if (
        not sender_user
        or not receiver_user
        or is_blocked(sender_id)
        or is_blocked(receiver_id)
        or are_users_blocked_mutually(sender_id, receiver_id)
    ):
        bot.answer_callback_query(call.id, '❌ امکان برقراری چت با این کاربر وجود ندارد.', show_alert=True)
        return

    if not is_profile_complete(sender_id) or not is_profile_complete(receiver_id):
        bot.answer_callback_query(call.id, '⚠️ پروفایل یکی از طرفین کامل نیست.', show_alert=True)
        return

    age_ok, age_err = _validate_same_age_band(sender_user, receiver_user)
    if not age_ok:
        bot.answer_callback_query(call.id, age_err, show_alert=True)
        return

    with queue_lock:
        if sender_id in active_pairs:
            bot.answer_callback_query(call.id, '⚠️ شما هم‌اکنون در یک چت فعال هستید.', show_alert=True)
            return
        if receiver_id in active_pairs:
            bot.answer_callback_query(call.id, '💬 دوست شما هم‌اکنون مشغول یک چت دیگر است؛ کمی بعد تلاش کنید.', show_alert=True)
            return

    now_ts = time.time()
    with _friend_action_lock:
        last_ts = _friend_chat_last_sent.get((sender_id, receiver_id), 0.0)
        if now_ts - last_ts < FRIEND_CHAT_COOLDOWN_SECONDS:
            rem = int(FRIEND_CHAT_COOLDOWN_SECONDS - (now_ts - last_ts)) + 1
            bot.answer_callback_query(call.id, f'⏳ لطفاً {rem} ثانیه دیگر مجدداً تلاش کنید.', show_alert=True)
            return
        _friend_chat_last_sent[(sender_id, receiver_id)] = now_ts

    now = datetime.now()
    now_str = now.strftime('%Y-%m-%d %H:%M:%S')

    try:
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('BEGIN IMMEDIATE')
            cur.execute(
                '''SELECT status, last_action_time, COALESCE(fee_paid, 0)
                   FROM chat_requests WHERE sender_id = ? AND receiver_id = ?''',
                (sender_id, receiver_id)
            )
            existing = cur.fetchone()

            if existing and existing[0] == 'accepting':
                conn.rollback()
                bot.answer_callback_query(call.id, '⏳ درخواست قبلی در حال پردازش است.', show_alert=True)
                return

            if existing and existing[0] == 'pending':
                try:
                    age_seconds = (now - datetime.strptime(existing[1], '%Y-%m-%d %H:%M:%S')).total_seconds()
                except (TypeError, ValueError):
                    age_seconds = CHAT_REQUEST_TTL_SECONDS + 1
                if age_seconds < CHAT_REQUEST_TTL_SECONDS:
                    conn.rollback()
                    bot.answer_callback_query(call.id, '⏳ یک درخواست چت فعال برای این دوست دارید.', show_alert=True)
                    return
                if existing[2]:
                    cur.execute(
                        'UPDATE users SET coins = COALESCE(coins, 0) + ? WHERE user_id = ?',
                        (CHAT_REQUEST_COST, sender_id)
                    )

            cur.execute(
                '''INSERT INTO chat_requests(sender_id, receiver_id, last_action_time, status, fee_paid)
                   VALUES (?, ?, ?, 'pending', 0)
                   ON CONFLICT(sender_id, receiver_id)
                   DO UPDATE SET last_action_time = excluded.last_action_time,
                                 status = 'pending',
                                 fee_paid = 0''',
                (sender_id, receiver_id, now_str)
            )
            conn.commit()
    except Exception:
        logger.error('friend free chat request DB error', exc_info=True)
        bot.answer_callback_query(call.id, '❌ ثبت درخواست چت با خطا مواجه شد.', show_alert=True)
        return

    try:
        sender_prof_id = get_or_create_profile_id(sender_id)
        sender_name = escape_html(sender_user[4] or 'دوست شما')
        receiver_alias = get_friend_alias(receiver_id, sender_id)
        alias_note = f' ({escape_html(receiver_alias)})' if receiver_alias else ''
        markup = types.InlineKeyboardMarkup(row_width=2)
        markup.add(
            types.InlineKeyboardButton('✅ شروع چت با دوست', callback_data=f'req_accept_{sender_id}'),
            types.InlineKeyboardButton('❌ فعلاً موقعیتش نیست', callback_data=f'req_reject_{sender_id}')
        )
        bot.send_message(
            receiver_id,
            f'🤝💬 <b>درخواست چت دوستانه جدید!</b>\n'
            f'━━━━━━━━━━━━━━━━━━━\n'
            f'دوست شما <b>{sender_name}{alias_note}</b> (/user_{sender_prof_id}) می‌خواهد همین الان با شما وارد چت شود.\n\n'
            f'👇 برای اتصال فوری یکی از گزینه‌های زیر را انتخاب کنید:',
            reply_markup=markup,
            parse_mode='HTML'
        )
    except Exception:
        logger.warning('friend chat request delivery failed %s -> %s', sender_id, receiver_id, exc_info=True)
        _finish_chat_request(sender_id, receiver_id, 'delivery_failed', refund_if_paid=False)
        bot.answer_callback_query(call.id, '❌ ارسال درخواست ممکن نشد (دوست شما در دسترس نیست).', show_alert=True)
        return

    bot.answer_callback_query(call.id, '✅ درخواست چت رایگان برای دوستتان ارسال شد!', show_alert=True)


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('friend_dm_')
)
def handle_friend_free_dm_start(call):
    """شروع ارسال پیام دایرکت رایگان به دوست بدون افشای آیدی تلگرام."""
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    if (
        not isinstance(user_id, int)
        or user_id <= 0
        or chat is None
        or getattr(chat, 'type', None) != 'private'
        or getattr(chat, 'id', None) != user_id
    ):
        bot.answer_callback_query(call.id, '🚫 این دکمه فقط در پیام خصوصی خودتان معتبر است.', show_alert=True)
        return

    try:
        friend_id = int(call.data.split('_')[2])
    except (IndexError, TypeError, ValueError):
        bot.answer_callback_query(call.id, '❌ درخواست نامعتبر است.', show_alert=True)
        return

    if user_id == friend_id or not are_users_friends(user_id, friend_id):
        bot.answer_callback_query(call.id, '⚠️ این کاربر در لیست دوستان شما قرار ندارد.', show_alert=True)
        return

    sender_row = get_user(user_id)
    friend_row = get_user(friend_id)
    if (
        not sender_row
        or not friend_row
        or is_blocked(user_id)
        or is_blocked(friend_id)
        or are_users_blocked_mutually(user_id, friend_id)
    ):
        bot.answer_callback_query(call.id, '❌ امکان ارسال پیام به این کاربر وجود ندارد.', show_alert=True)
        return

    age_ok, age_err = _validate_same_age_band(sender_row, friend_row)
    if not age_ok:
        bot.answer_callback_query(call.id, age_err, show_alert=True)
        return

    now_ts = time.time()
    with _friend_action_lock:
        last_ts = _friend_dm_last_sent.get(user_id, 0.0)
        if now_ts - last_ts < FRIEND_DM_COOLDOWN_SECONDS:
            rem = int(FRIEND_DM_COOLDOWN_SECONDS - (now_ts - last_ts)) + 1
            bot.answer_callback_query(call.id, f'⏳ لطفاً {rem} ثانیه صبر کنید سپس پیام بعدی را بفرستید.', show_alert=True)
            return

    bot.answer_callback_query(call.id)
    friend_name = escape_html(friend_row[4] or 'دوست')
    cancel_markup = types.InlineKeyboardMarkup()
    cancel_markup.add(types.InlineKeyboardButton('❌ انصراف', callback_data=f'friend_dmcancel_{friend_id}'))

    prompt = bot.send_message(
        user_id,
        f'✉️🤝 <b>ارسال پیام دایرکت رایگان به «{friend_name}»</b>\n\n'
        'لطفاً پیام خود را (متن، عکس، ویدیو، ویس یا استیکر) ارسال کنید:\n'
        '🔒 <i>ارسال لینک، شماره تماس و آیدی تلگرام در پیام دایرکت مجاز نیست.</i>',
        reply_markup=cancel_markup,
        parse_mode='HTML'
    )
    bot.register_next_step_handler(prompt, process_friend_free_dm_send, friend_id, prompt.message_id)


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('friend_dmcancel_')
)
def handle_friend_dm_cancel(call):
    """لغو مرحله ارسال پیام دایرکت به دوست."""
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    if not isinstance(user_id, int) or chat is None or chat.id != user_id:
        return

    bot.clear_step_handler_by_chat_id(user_id)
    try:
        bot.delete_message(chat.id, message.message_id)
    except Exception:
        pass
    bot.answer_callback_query(call.id, '❌ ارسال پیام لغو شد.')


def process_friend_free_dm_send(message, friend_id, prompt_msg_id):
    """ارسال امن پیام دایرکت رایگان بین دوستان با فیلتر اطلاعات هویتی."""
    user_id = message.from_user.id
    try:
        bot.delete_message(message.chat.id, prompt_msg_id)
    except Exception:
        pass

    if message.content_type == 'text' and (message.text or '').strip() in (
        '/cancel', '/start', 'بازگشت', 'لغو', 'انصراف'
    ):
        bot.send_message(user_id, '❌ ارسال پیام به دوست لغو شد.')
        return

    if not are_users_friends(user_id, friend_id):
        bot.send_message(user_id, '⚠️ این کاربر دیگر در لیست دوستان شما نیست.')
        return

    sender_row = get_user(user_id)
    friend_row = get_user(friend_id)
    if (
        not sender_row
        or not friend_row
        or is_blocked(user_id)
        or is_blocked(friend_id)
        or are_users_blocked_mutually(user_id, friend_id)
    ):
        bot.send_message(user_id, '❌ امکان ارسال پیام به این کاربر وجود ندارد.')
        return

    age_ok, age_err = _validate_same_age_band(sender_row, friend_row)
    if not age_ok:
        bot.send_message(user_id, age_err)
        return

    if message.content_type in ('contact', 'location', 'venue', 'poll'):
        bot.send_message(user_id, '🚫 ارسال مخاطب، موقعیت مکانی و نظرسنجی در پیام ناشناس مجاز نیست.')
        return

    raw_text = message.text if message.content_type == 'text' else (message.caption or '')
    entities = (message.entities if message.content_type == 'text' else message.caption_entities) or []
    if contains_sensitive_contact(raw_text, entities) or (BLOCK_LINKS_IN_DIRECT and contains_link(raw_text)):
        bot.send_message(
            user_id,
            '🚫 <b>اخطار حریم خصوصی:</b> ارسال لینک، آیدی تلگرام، ایمیل یا شماره تماس در پیام دایرکت مجاز نیست.',
            parse_mode='HTML'
        )
        return

    now_ts = time.time()
    with _friend_action_lock:
        last_ts = _friend_dm_last_sent.get(user_id, 0.0)
        if now_ts - last_ts < FRIEND_DM_COOLDOWN_SECONDS:
            bot.send_message(user_id, '⏳ لطفاً چند ثانیه صبر کنید و دوباره تلاش کنید.')
            return
        _friend_dm_last_sent[user_id] = now_ts

    my_prof_id = get_or_create_profile_id(user_id)
    my_name = escape_html(sender_row[4] or 'دوست')
    receiver_alias = get_friend_alias(friend_id, user_id)
    alias_suffix = f' ({escape_html(receiver_alias)})' if receiver_alias else ''

    header = (
        f'💌🤝 <b>پیام جدید از دوست شما: {my_name}{alias_suffix}</b>\n'
        f'🆔 کد پروفایل: /user_{my_prof_id}\n'
        f'━━━━━━━━━━━━━━━━━━━\n'
    )

    reply_markup = types.InlineKeyboardMarkup(row_width=2)
    reply_markup.add(
        types.InlineKeyboardButton('↩️ پاسخ سریع رایگان', callback_data=f'friend_dm_{user_id}'),
        types.InlineKeyboardButton('💬 درخواست چت رایگان', callback_data=f'friend_chat_{user_id}')
    )
    reply_markup.add(
        types.InlineKeyboardButton('🤝 مشاهده کارت دوست', callback_data=f'friend_view_{user_id}_0')
    )

    try:
        if message.content_type == 'text':
            bot.send_message(
                friend_id,
                header + escape_html(raw_text[:3400]),
                reply_markup=reply_markup,
                parse_mode='HTML'
            )
        elif message.content_type in DIRECT_CAPTION_TYPES:
            caption = header + escape_html(raw_text[:750])
            bot.copy_message(
                chat_id=friend_id,
                from_chat_id=user_id,
                message_id=message.message_id,
                caption=caption,
                parse_mode='HTML',
                reply_markup=reply_markup
            )
        else:
            bot.send_message(friend_id, header.rstrip() + ' 👇', parse_mode='HTML')
            bot.copy_message(
                chat_id=friend_id,
                from_chat_id=user_id,
                message_id=message.message_id,
                reply_markup=reply_markup
            )

        bot.send_message(user_id, '✅ پیام شما به‌صورت رایگان برای دوستتان ارسال شد.')
    except Exception:
        logger.warning('friend free DM failed %s -> %s', user_id, friend_id, exc_info=True)
        bot.send_message(user_id, '❌ ارسال پیام ممکن نشد (احتمالاً دوست شما ربات را مسدود کرده است).')

@bot.callback_query_handler(
    func=lambda call: (call.data or '').startswith('admin_block_target_')
    or (call.data or '').startswith('admin_ignore_rep_')
)
@admin_only
def handle_admin_report_action(call):
    """اجرای امن تصمیم ادمین درباره گزارش؛ فقط از پیام خصوصی ادمین."""
    admin = getattr(call, 'from_user', None)
    admin_id = getattr(admin, 'id', None)

    # دفاع تکمیلی؛ admin_only نیز همین مجوز را پیش از ورود بررسی می‌کند.
    if admin_id not in ADMINS:
        try:
            bot.answer_callback_query(
                call.id,
                "⛔ شما دسترسی ادمین ندارید.",
                show_alert=True
            )
        except Exception:
            pass
        return

    # دکمه‌های گزارش فقط باید در پیام خصوصی ارسال‌شده برای همان ادمین باشند.
    callback_message = getattr(call, 'message', None)
    callback_chat = getattr(callback_message, 'chat', None)
    if (
        callback_message is None
        or callback_chat is None
        or getattr(callback_chat, 'type', None) != 'private'
        or getattr(callback_chat, 'id', None) != admin_id
    ):
        try:
            bot.answer_callback_query(
                call.id,
                "🚫 این دکمه فقط از پیام خصوصی گزارش برای ادمین قابل استفاده است.",
                show_alert=True
            )
        except Exception:
            pass
        return

    data = getattr(call, 'data', '') or ''
    if data.startswith('admin_block_target_'):
        action = 'block'
        target_text = data[len('admin_block_target_'):]
    elif data.startswith('admin_ignore_rep_'):
        action = 'ignore'
        target_text = data[len('admin_ignore_rep_'):]
    else:
        try:
            bot.answer_callback_query(
                call.id, "🚫 درخواست نامعتبر است.", show_alert=True
            )
        except Exception:
            pass
        return

    # فقط شناسه عددی مثبت پذیرفته می‌شود؛ split با تعداد قطعات ثابت استفاده نمی‌شود.
    if not target_text.isdecimal():
        try:
            bot.answer_callback_query(
                call.id, "🚫 شناسه کاربر نامعتبر است.", show_alert=True
            )
        except Exception:
            pass
        return

    target_user_id = int(target_text)
    if target_user_id <= 0 or target_user_id == admin_id:
        try:
            bot.answer_callback_query(
                call.id, "🚫 شناسه کاربر نامعتبر است.", show_alert=True
            )
        except Exception:
            pass
        return

    try:
        target_user = get_user(target_user_id)
    except Exception:
        logger.error('Could not validate admin report target', exc_info=True)
        try:
            bot.answer_callback_query(
                call.id,
                "❌ بررسی کاربر با خطا مواجه شد؛ دوباره تلاش کنید.",
                show_alert=True
            )
        except Exception:
            pass
        return

    if not target_user:
        try:
            bot.answer_callback_query(
                call.id, "❌ کاربر موردنظر پیدا نشد.", show_alert=True
            )
        except Exception:
            pass
        return

    if action == 'block':
        try:
            block_user(target_user_id)
        except Exception:
            logger.error(
                'Admin report block action failed for target=%s',
                target_user_id,
                exc_info=True
            )
            try:
                bot.answer_callback_query(
                    call.id,
                    "❌ مسدودسازی انجام نشد؛ خطا ثبت شد.",
                    show_alert=True
                )
            except Exception:
                pass
            return
        answer_text = f"✅ کاربر {target_user_id} مسدود شد."
    else:
        answer_text = "✅ گزارش نادیده گرفته شد."

    try:
        bot.answer_callback_query(call.id, answer_text, show_alert=True)
    except Exception:
        logger.warning('Could not answer admin report callback', exc_info=True)

    # پس از اجرای موفق تصمیم، دکمه‌های همان گزارش غیرفعال می‌شوند.
    try:
        bot.edit_message_reply_markup(
            callback_chat.id,
            callback_message.message_id,
            reply_markup=None
        )
    except Exception:
        logger.warning('Could not remove handled report buttons', exc_info=True)

@bot.message_handler(func=lambda message: message.text == 'شروع چت ناشناس 🎭')
def start_anonymous_chat(message):
    user_id = message.from_user.id
    update_last_seen(user_id)
    
    current_time = time.time()
    with spam_lock:
        if user_id in user_timeouts:
            if current_time < user_timeouts[user_id]:
                remaining = int(user_timeouts[user_id] - current_time)
                bot.send_message(user_id, f"🚫 شما به دلیل ارسال اسپم، به مدت {remaining} ثانیه از جستجوی جدید محروم هستید.")
                return
            else:
                user_timeouts.pop(user_id, None)
    
    if is_blocked(user_id):
        bot.send_message(user_id, "حساب شما مسدود است.")
        return
    
    if is_user_in_chat(user_id):
        bot.send_message(user_id, "شما هم اکنون در چت هستید! برای پایان چت از /cancel استفاده کنید.")
        return
    
    if not is_profile_complete(user_id):
        bot.send_message(user_id, "⚠️ پروفایل شما ناقص است! برای شروع چت ابتدا مراحل ثبت‌نام را تکمیل کنید.")
        start_questionnaire(message)
        return
    
    gender = get_user_gender(user_id)
    if not gender:
        bot.send_message(user_id, "لطفا پروفایل خود را کامل کنید.")
        return
        
    # 🌟 بررسی سهمیه روزانه قبل از نمایش منو
    if not has_chat_quota(user_id):
        bot.send_message(
            user_id, 
            "❌ <b>سقف درخواست‌های چت امروز شما پر شده است!</b>\n\n"
            "🚀 برای افزایش ظرفیت روزانه، از منوی اصلی روی دکمه <b>سکه 🪙</b> کلیک کنید و سطح خود را ارتقا دهید.",
            parse_mode='HTML'
        )
        return
    
    # 🌟 بازگرداندن منوی ترتمیز و ۴ دکمه‌ای دقیقاً مطابق با عکس شما 🌟
    markup = types.InlineKeyboardMarkup(row_width=2)
    
    # ردیف اول: شانسی و اطراف
    markup.add(
        types.InlineKeyboardButton("(رایگان)جستجوی شانسی 🎲", callback_data="search_general"),
        types.InlineKeyboardButton("(5 سکه)جستجوی اطراف 🛰", callback_data="search_nearby")
    )
    
    # ردیف دوم: جستجوی پیشرفته
    markup.add(
        types.InlineKeyboardButton("جستجوی پیشرفته(10 سکه) 🎯", callback_data="advanced_search_start")
    )
    
    # ردیف سوم: منوی گرایش‌های خاص
    markup.add(
        types.InlineKeyboardButton("(5 سکه)جستجوی گرایش خاص 🚻", callback_data="menu_select_gender")
    )
    
    bot.send_message(user_id, "👇 به کی وصلت کنم؟ انتخاب کن:", reply_markup=markup)

# ============================================================================
# 7) Search handlers (قفل‌گذاری امن برای جلوگیری از هدر رفتن سکه‌ها)
# ============================================================================
@bot.callback_query_handler(func=lambda call: call.data.startswith('search_'))
def process_search_type(call):
    user_id = call.from_user.id
    update_last_seen(user_id)

    current_time = time.time()
    with spam_lock:
        if user_id in user_timeouts:
            if current_time < user_timeouts[user_id]:
                bot.answer_callback_query(call.id, '🚫 کمی صبر کنید.', show_alert=True)
                return
            user_timeouts.pop(user_id, None)

    if is_user_in_chat(user_id):
        bot.answer_callback_query(call.id, '❌ شما داخل چت هستید.', show_alert=True)
        return
    if is_user_waiting(user_id):
        bot.answer_callback_query(call.id, '⚠️ شما همین حالا در صف هستید.', show_alert=True)
        return

    search_type = call.data.split('_', 1)[1]

    if search_type == 'nearby':
        user_row = get_user(user_id)
        loc = user_row[8] if user_row else None
        if not loc or loc == 'نامشخص':
            bot.answer_callback_query(call.id, '❌ موقعیت شما ثبت نشده است.', show_alert=True)
            return

        markup = types.InlineKeyboardMarkup(row_width=2)
        buttons = [types.InlineKeyboardButton(
            g_val[0].split(' (')[0] if '(' in g_val[0] else g_val[0],
            callback_data=f'nearbyg_{g_key}'
        ) for g_key, g_val in GENDER_MAP.items()]
        markup.add(*buttons)
        markup.add(types.InlineKeyboardButton('🔄 هر گرایشی', callback_data='nearbyg_any'))
        markup.add(types.InlineKeyboardButton('لغو ❌', callback_data='cancel_search'))
        try: bot.edit_message_text('📍 <b>جستجوی افراد نزدیک</b>\n\nگرایش هدف را انتخاب کنید.\n(هزینه: ۵ سکه)', call.message.chat.id, call.message.message_id, reply_markup=markup, parse_mode='HTML')
        except Exception: pass
        return

    cost = 5 if search_type in GENDER_MAP else 0
    ok, reservation = reserve_search_charge(user_id, cost)
    if not ok:
        bot.answer_callback_query(call.id, '❌ سهمیه چت یا موجودی سکه شما کافی نیست.', show_alert=True)
        return

    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except Exception: pass

    cancel_markup = types.InlineKeyboardMarkup()
    cancel_markup.add(types.InlineKeyboardButton('لغو جستجو ❌', callback_data='cancel_search'))
    fee_text = f' ({cost} سکه کسر شد)' if cost else ''

    try:
        if search_type == 'general':
            msg = bot.send_message(user_id, f'🎲 در حال جستجوی شانسی...{fee_text}', reply_markup=cancel_markup)
            with queue_lock:
                search_message_ids[user_id] = msg.message_id
            try_pairing(user_id, 'general')
        elif search_type in GENDER_MAP:
            gender_name = GENDER_MAP[search_type][0].split(' (')[0] if '(' in GENDER_MAP[search_type][0] else GENDER_MAP[search_type][0]
            msg = bot.send_message(user_id, f'🔍 در حال جستجوی {gender_name}{fee_text}...', reply_markup=cancel_markup)
            with queue_lock:
                search_message_ids[user_id] = msg.message_id
            try_pairing(user_id, search_type)
        else:
            release_search_reservation(user_id)
            bot.answer_callback_query(call.id, '❌ نوع جستجو نامعتبر است.', show_alert=True)
    except Exception:
        release_search_reservation(user_id)
        bot.send_message(user_id, '❌ شروع جستجو با خطا مواجه شد. هزینه بازگردانده شد.')

@bot.callback_query_handler(func=lambda call: call.data.startswith('nearbyg_'))
def process_nearby_gender_selection(call):
    user_id = call.from_user.id
    update_last_seen(user_id)
    target_gender = call.data.split('_', 1)[1]

    if is_user_in_chat(user_id) or is_user_waiting(user_id):
        bot.answer_callback_query(call.id, '⚠️ وضعیت چت/صف شما تغییر کرده است.', show_alert=True)
        return

    user_row = get_user(user_id)
    loc = user_row[8] if user_row else None
    if not loc or loc == 'نامشخص':
        bot.answer_callback_query(call.id, '❌ موقعیت شما ثبت نشده است.', show_alert=True)
        return

    ok, reservation = reserve_search_charge(user_id, 5)
    if not ok:
        bot.answer_callback_query(call.id, '❌ سهمیه چت یا ۵ سکه کافی نیست.', show_alert=True)
        return

    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except Exception: pass

    city = loc.split(' - ')[1] if ' - ' in loc else loc
    province = loc.split(' - ')[0] if ' - ' in loc else loc
    gender_fa = 'هر گرایشی' if target_gender == 'any' else GENDER_MAP.get(target_gender, ('نامشخص',))[0].split(' (')[0]

    cancel_markup = types.InlineKeyboardMarkup()
    cancel_markup.add(types.InlineKeyboardButton('لغو جستجو ❌', callback_data='cancel_search'))
    try:
        msg = bot.send_message(
            user_id,
            f'🛰 در حال جستجوی همشهری‌ها در <b>{escape_html(city)}</b> / <b>{escape_html(province)}</b>...\n'
            f'🚻 هدف: <b>{escape_html(gender_fa)}</b> (۵ سکه کسر شد)',
            reply_markup=cancel_markup, parse_mode='HTML'
        )
        with queue_lock:
            search_message_ids[user_id] = msg.message_id
        try_pairing(user_id, f'nearby_{target_gender}')
    except Exception:
        release_search_reservation(user_id)
        bot.send_message(user_id, '❌ شروع جستجو با خطا مواجه شد؛ ۵ سکه و سهمیه بازگردانده شد.')

@bot.callback_query_handler(func=lambda call: call.data.startswith('searchpos_'))
def process_search_pos_type(call):
    user_id = call.from_user.id
    update_last_seen(user_id)
    
    with spam_lock:
        if user_id in user_timeouts and time.time() < user_timeouts[user_id]:
            bot.answer_callback_query(call.id, "🚫 محرومیت اسپم!", show_alert=True)
            return
            
    if is_user_in_chat(user_id) or is_user_waiting(user_id): 
        bot.answer_callback_query(call.id, '⚠️ شما در وضعیت چت/صف هستید.', show_alert=True)
        return
        
    target_pos = call.data.split('_')[1]
    target_name = POS_MAP.get(target_pos, "نامشخص")
    
    ok, reservation = reserve_search_charge(user_id, 0)
    if not ok:
        bot.answer_callback_query(call.id, '❌ سهمیه چت روزانه شما تمام شده است.', show_alert=True)
        return

    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except Exception: pass
    
    cancel_markup = types.InlineKeyboardMarkup()
    cancel_markup.add(types.InlineKeyboardButton("لغو جستجو ❌", callback_data="cancel_search"))
    
    try:
        msg = bot.send_message(user_id, f"🔍 در حال جستجوی {target_name}... لطفا صبر کنید.", reply_markup=cancel_markup)
        with queue_lock:
            search_message_ids[user_id] = msg.message_id
        try_pairing(user_id, f'pos_{target_pos}')
    except Exception:
        release_search_reservation(user_id)
        bot.send_message(user_id, '❌ شروع جستجو با خطا مواجه شد.')

@bot.callback_query_handler(func=lambda call: call.data == 'adv_execute_search')
def adv_execute_search(call):
    user_id = call.from_user.id
    update_last_seen(user_id)

    with spam_lock:
        if user_id in user_timeouts and time.time() < user_timeouts[user_id]:
            bot.answer_callback_query(call.id, '🚫 کمی صبر کنید.', show_alert=True)
            return
    if is_user_in_chat(user_id) or is_user_waiting(user_id):
        bot.answer_callback_query(call.id, '⚠️ شما در وضعیت چت/صف هستید.', show_alert=True)
        return

    ok, reservation = reserve_search_charge(user_id, 10)
    if not ok:
        bot.answer_callback_query(call.id, '❌ برای این جستجو ۱۰ سکه و یک سهمیه چت لازم است.', show_alert=True)
        return

    filters = advanced_search_filters.get(user_id, {'gender': 'any', 'city': 'any', 'age': 'any', 'pos': 'any'})
    t_gender = filters.get('gender', 'any')
    t_city = filters.get('city', 'any')
    t_age = filters.get('age', 'any')
    t_pos = filters.get('pos', 'any')

    gender_fa = GENDER_MAP[t_gender][0].split(' (')[0] if t_gender in GENDER_MAP else 'بدون محدودیت'
    city_fa = t_city.split(' - ')[1] if ' - ' in t_city else 'بدون محدودیت'
    age_fa = {'12-18': '۱۲ تا ۱۸ سال', '19-25': '۱۹ تا ۲۵ سال', '26-35': '۲۶ تا ۳۵ سال', '36-99': '۳۶ سال به بالا'}.get(t_age, 'بدون محدودیت')
    pos_fa = POS_MAP.get(t_pos, 'بدون محدودیت') if t_pos != 'any' else 'بدون محدودیت'

    cancel_markup = types.InlineKeyboardMarkup()
    cancel_markup.add(types.InlineKeyboardButton('لغو جستجو ❌', callback_data='cancel_search'))

    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except Exception: pass

    try:
        msg = bot.send_message(
            user_id,
            f'🎯 <b>در حال جستجوی پیشرفته...</b>\n\n'
            f'├ گرایش: <b>{escape_html(gender_fa)}</b>\n'
            f'├ پوزیشن: <b>{escape_html(pos_fa)}</b>\n'
            f'├ شهر: <b>{escape_html(city_fa)}</b>\n'
            f'└ سن: <b>{escape_html(age_fa)}</b>\n\n⏳ لطفاً صبر کنید...',
            reply_markup=cancel_markup, parse_mode='HTML'
        )
        with queue_lock:
            search_message_ids[user_id] = msg.message_id
        try_pairing(user_id, 'advanced')
    except Exception:
        release_search_reservation(user_id)
        bot.send_message(user_id, '❌ شروع جستجوی پیشرفته با خطا مواجه شد؛ ۱۰ سکه و سهمیه بازگردانده شد.')

@bot.callback_query_handler(func=lambda call: call.data == 'cancel_search')
def cancel_search_callback(call):
    user_id = call.from_user.id
    update_last_seen(user_id)
    
    with queue_lock:
        removed = remove_from_waiting(user_id)
        released = release_search_reservation(user_id)
    
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except Exception: pass
        
    if removed or released:
        bot.send_message(user_id, "❌ جستجوی شما لغو شد (سهمیه و هزینه پرداخت شده، دقیقاً به حساب شما بازگردانده شد).")
        show_main_menu(user_id, user_id in ADMINS)
    else:
        if is_user_in_chat(user_id):
            bot.answer_callback_query(call.id, "شما به یک کاربر متصل شده‌اید و نمی‌توانید جستجو را لغو کنید!", show_alert=True)
        else:
            show_main_menu(user_id, user_id in ADMINS)

@bot.callback_query_handler(func=lambda call: call.data == 'menu_select_gender')
def show_selected_genders_menu(call):
    user_id = call.from_user.id
    update_last_seen(user_id)
    
    markup = types.InlineKeyboardMarkup(row_width=2)
    
    # 🌟 استفاده از کل GENDER_MAP برای نمایش تمام گرایش‌ها و جنسیت‌ها به صورت پویا
    gender_buttons = []
    for g_key, g_val in GENDER_MAP.items():
        clean_name = g_val[0]
        # مرتب‌سازی متن دکمه برای حذف عبارات اضافی اگر لازم باشد
        if '(' in clean_name:
            clean_name = clean_name.split(' (')[0] + " " + clean_name.split()[-1]
        gender_buttons.append(types.InlineKeyboardButton(f"جستجوی {clean_name}", callback_data=f"search_{g_key}"))
        
    markup.add(*gender_buttons)
    markup.add(types.InlineKeyboardButton("🔙 بازگشت به منوی قبل", callback_data="back_to_main_search"))

    text = "🚻 <b>انتخاب گرایش برای جستجو:</b>\n\nلطفاً گرایش مورد نظر خود را از لیست زیر انتخاب کنید:"
    
    try:
        bot.edit_message_text(text, call.message.chat.id, call.message.message_id, reply_markup=markup, parse_mode='HTML')
    except:
        bot.send_message(call.message.chat.id, text, reply_markup=markup, parse_mode='HTML')

@bot.callback_query_handler(func=lambda call: call.data == 'back_to_main_search')
def back_to_main_search(call):
    user_id = call.from_user.id
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
    
    # شبیه‌سازی بازگشت به منوی اصلی جستجو
    fake_msg = call.message
    fake_msg.from_user.id = user_id
    start_anonymous_chat(fake_msg)

# ----------------- 🌟 سیستم جستجوی پیشرفته (آپدیت جدید) 🌟 -----------------

@bot.callback_query_handler(func=lambda call: call.data == 'advanced_search_start')
def advanced_search_menu(call):
    user_id = call.from_user.id
    update_last_seen(user_id)
    
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT COALESCE(coins, 0) FROM users WHERE user_id = ?', (user_id,))
        row = cur.fetchone()
        if not row: return
        coins = row[0]
        
        if coins < 10:
            bot.answer_callback_query(
                call.id, 
                "❌ برای جستجوی پیشرفته به ۱۰ سکه نیاز دارید!\nموجودی سکه شما کافی نیست.", 
                show_alert=True
            )
            return

    if user_id not in advanced_search_filters:
        advanced_search_filters[user_id] = {'gender': 'any', 'city': 'any', 'age': 'any', 'pos': 'any'}

    filters = advanced_search_filters[user_id]
    
    t_gender = filters.get('gender', 'any')
    gender_fa = GENDER_MAP[t_gender][0].split(' (')[0] if t_gender != 'any' and t_gender in GENDER_MAP else "بدون محدودیت"
    
    t_city = filters.get('city', 'any')
    if t_city == 'any': city_fa = "بدون محدودیت"
    else: city_fa = t_city.split(' - ')[1] if ' - ' in t_city else t_city
        
    t_age = filters.get('age', 'any')
    age_fa_map = {'12-18': '۱۲ تا ۱۸ سال', '19-25': '۱۹ تا ۲۵ سال', '26-35': '۲۶ تا ۳۵ سال', '36-99': '۳۶ سال به بالا', 'any': 'بدون محدودیت'}
    age_fa = age_fa_map.get(t_age, "بدون محدودیت")

    # 🌟 اضافه شدن نمایش پوز
    t_pos = filters.get('pos', 'any')
    pos_fa = POS_MAP[t_pos] if t_pos != 'any' and t_pos in POS_MAP else "بدون محدودیت"

    markup = types.InlineKeyboardMarkup(row_width=2)
    markup.add(
        types.InlineKeyboardButton("🚻 گرایش/جنسیت", callback_data="adv_set_gender"),
        types.InlineKeyboardButton("🎭 پوزیشن", callback_data="adv_set_position") # دکمه جدید
    )
    markup.add(
        types.InlineKeyboardButton("📍 استان و شهر", callback_data="adv_set_city"),
        types.InlineKeyboardButton("🎂 محدوده سنی", callback_data="adv_set_age")
    )
    markup.add(types.InlineKeyboardButton("🚀 شروع جستجو با فیلترها (۱۰ سکه)", callback_data="adv_execute_search"))
    markup.add(types.InlineKeyboardButton("❌ انصراف", callback_data="cancel_search"))
    
    text = (
        "🎯 <b>سیستم جستجوی پیشرفته</b>\n\n"
        "در این بخش می‌توانید مشخصات فرد مورد نظر خود را فیلتر کنید.\n"
        "• هزینه این جستجو: <b>۱۰ سکه</b>\n\n"
        "📋 <b>فیلترهای انتخاب‌شده شما:</b>\n"
        f" ├ 🚻 گرایش: <code>{gender_fa}</code>\n"
        f" ├ 🎭 پوزیشن: <code>{pos_fa}</code>\n"
        f" ├ 📍 شهر: <code>{city_fa}</code>\n"
        f" └ 🎂 سن: <code>{age_fa}</code>\n\n"
        "👇 برای تغییر فیلترها از دکمه‌های زیر استفاده کنید:"
    )
    
    try: bot.edit_message_text(text, call.message.chat.id, call.message.message_id, reply_markup=markup, parse_mode='HTML')
    except: bot.send_message(call.message.chat.id, text, reply_markup=markup, parse_mode='HTML')

# ----------------- تنظیم گرایش -----------------
@bot.callback_query_handler(func=lambda call: call.data == 'adv_set_gender')
def adv_set_gender(call):
    user_id = call.from_user.id
    markup = types.InlineKeyboardMarkup(row_width=2)
    
    # دکمه‌های انتخاب جنسیت هدف
    for g_key, g_val in GENDER_MAP.items():
        markup.add(types.InlineKeyboardButton(g_val[0], callback_data=f"adv_sel_gender_{g_key}"))
    markup.add(types.InlineKeyboardButton("🔄 هر جنسیتی (بدون محدودیت)", callback_data="adv_sel_gender_any"))
    markup.add(types.InlineKeyboardButton("🔙 بازگشت", callback_data="advanced_search_start"))

    bot.edit_message_text("🚻 لطفاً جنسیت مخاطب مورد نظر خود را انتخاب کنید:", call.message.chat.id, call.message.message_id, reply_markup=markup)

@bot.callback_query_handler(func=lambda call: call.data.startswith('adv_sel_gender_'))
def adv_save_gender(call):
    user_id = call.from_user.id
    gender_choice = call.data.split('_')[3]
    
    if user_id not in advanced_search_filters:
        advanced_search_filters[user_id] = {}
    advanced_search_filters[user_id]['gender'] = gender_choice
    
    bot.answer_callback_query(call.id, "✅ گرایش مورد نظر ذخیره شد.")
    advanced_search_menu(call)

# ----------------- تنظیم پوزیشن در پیشرفته -----------------
@bot.callback_query_handler(func=lambda call: call.data == 'adv_set_position')
def adv_set_position(call):
    markup = types.InlineKeyboardMarkup(row_width=3)
    markup.add(
        types.InlineKeyboardButton("تاپ 🔝", callback_data="adv_sel_pos_top"),
        types.InlineKeyboardButton("ورس ↔️", callback_data="adv_sel_pos_vers"),
        types.InlineKeyboardButton("بات 🔽", callback_data="adv_sel_pos_bottom")
    )
    markup.add(types.InlineKeyboardButton("🔄 هر پوزیشنی (بدون محدودیت)", callback_data="adv_sel_pos_any"))
    markup.add(types.InlineKeyboardButton("🔙 بازگشت", callback_data="advanced_search_start"))
    bot.edit_message_text("🎭 لطفاً گرایش نقشی (پوزیشن) مخاطب مورد نظر خود را انتخاب کنید:", call.message.chat.id, call.message.message_id, reply_markup=markup)

@bot.callback_query_handler(func=lambda call: call.data.startswith('adv_sel_pos_'))
def adv_save_position(call):
    user_id = call.from_user.id
    pos_choice = call.data.split('_')[3]
    if user_id not in advanced_search_filters:
        advanced_search_filters[user_id] = {'gender': 'any', 'city': 'any', 'age': 'any', 'pos': 'any'}
    advanced_search_filters[user_id]['pos'] = pos_choice
    bot.answer_callback_query(call.id, "✅ پوزیشن مورد نظر ذخیره شد.")
    advanced_search_menu(call)

# ----------------- 🌟 سیستم دو مرحله‌ای انتخاب شهر 🌟 -----------------
@bot.callback_query_handler(func=lambda call: call.data == 'adv_set_city')
def adv_set_city_step1(call):
    markup = types.InlineKeyboardMarkup(row_width=3)
    buttons = [types.InlineKeyboardButton(p, callback_data=f"adv_prov_{provinces.index(p)}") for p in provinces]
    markup.add(*buttons)
    markup.add(types.InlineKeyboardButton("🔄 هر شهری (بدون محدودیت)", callback_data="adv_save_city_any"))
    markup.add(types.InlineKeyboardButton("🔙 بازگشت", callback_data="advanced_search_start"))
    bot.edit_message_text("📍 برای انتخاب شهر، ابتدا استان آن را مشخص کنید:", call.message.chat.id, call.message.message_id, reply_markup=markup)

@bot.callback_query_handler(func=lambda call: call.data.startswith('adv_prov_'))
def adv_set_city_step2(call):
    prov_index = int(call.data.split('_')[2])
    province = provinces[prov_index]
    cities = iran_cities.get(province, ['مرکز استان', 'سایر'])
    
    markup = types.InlineKeyboardMarkup(row_width=3)
    buttons = [types.InlineKeyboardButton(c, callback_data=f"adv_city_{prov_index}_{cities.index(c)}") for c in cities]
    markup.add(*buttons)
    markup.add(types.InlineKeyboardButton("🔙 بازگشت به لیست استان‌ها", callback_data="adv_set_city"))
    
    bot.edit_message_text(f"🏙 حالا شهر مورد نظر در استان {province} را انتخاب کنید:", call.message.chat.id, call.message.message_id, reply_markup=markup)

@bot.callback_query_handler(func=lambda call: call.data.startswith('adv_city_') or call.data == 'adv_save_city_any')
def adv_save_city(call):
    user_id = call.from_user.id
    if user_id not in advanced_search_filters:
        advanced_search_filters[user_id] = {'gender': 'any', 'city': 'any', 'age': 'any'}

    if call.data == 'adv_save_city_any':
        advanced_search_filters[user_id]['city'] = 'any'
    else:
        parts = call.data.split('_')
        prov_index = int(parts[2])
        city_index = int(parts[3])
        province = provinces[prov_index]
        city = iran_cities.get(province, ['مرکز استان', 'سایر'])[city_index]
        # فرمت استاندارد برای تطابق در دیتابیس
        advanced_search_filters[user_id]['city'] = f"{province} - {city}"

    bot.answer_callback_query(call.id, "✅ شهر ذخیره شد.")
    advanced_search_menu(call) # بازگشت و آپدیت متن اصلی

# ----------------- تنظیم سن -----------------
@bot.callback_query_handler(func=lambda call: call.data == 'adv_set_age')
def adv_set_age(call):
    markup = types.InlineKeyboardMarkup(row_width=2)
    markup.add(
        types.InlineKeyboardButton("۱۲ تا ۱۸ سال", callback_data="adv_sel_age_12-18"),
        types.InlineKeyboardButton("۱۹ تا ۲۵ سال", callback_data="adv_sel_age_19-25"),
        types.InlineKeyboardButton("۲۶ تا ۳۵ سال", callback_data="adv_sel_age_26-35"),
        types.InlineKeyboardButton("۳۶ سال به بالا", callback_data="adv_sel_age_36-99")
    )
    markup.add(types.InlineKeyboardButton("🔄 هر سنی (بدون محدودیت)", callback_data="adv_sel_age_any"))
    markup.add(types.InlineKeyboardButton("🔙 بازگشت", callback_data="advanced_search_start"))
    bot.edit_message_text("🎂 لطفاً محدوده سنی مخاطب مورد نظر خود را انتخاب کنید:", call.message.chat.id, call.message.message_id, reply_markup=markup)

@bot.callback_query_handler(func=lambda call: call.data.startswith('adv_sel_age_'))
def adv_save_age(call):
    user_id = call.from_user.id
    age_choice = call.data.split('_')[3]
    if user_id not in advanced_search_filters:
        advanced_search_filters[user_id] = {'gender': 'any', 'city': 'any', 'age': 'any'}
    advanced_search_filters[user_id]['age'] = age_choice
    bot.answer_callback_query(call.id, "✅ محدوده سنی ذخیره شد.")
    advanced_search_menu(call)

def format_last_seen(user_id, last_seen_str):
    # بررسی اینکه آیا کاربر همین الان در حال چت با کسی است
    if user_id in active_pairs:
        return "🟢 در حال چت"
        
    if not last_seen_str:
        return "نامشخص ⚪"
        
    try:
        last_seen_date = datetime.strptime(last_seen_str, "%Y-%m-%d %H:%M:%S")
        now = datetime.now()
        diff = now - last_seen_date
        
        if diff.total_seconds() < 60:
            return "همین الان آنلاین بود 🟢"
        elif diff.total_seconds() < 3600:
            minutes = int(diff.total_seconds() / 60)
            return f"{minutes} دقیقه پیش 🟡"
        elif diff.total_seconds() < 86400:
            hours = int(diff.total_seconds() / 3600)
            return f"{hours} ساعت پیش 🟠"
        else:
            days = int(diff.total_seconds() / 86400)
            return f"{days} روز پیش ⚪"
    except:
        return "نامشخص ⚪"

@bot.message_handler(func=lambda message: message.text == 'پروفایل من 👤')
def show_profile(message):
    user_id = message.from_user.id
    update_last_seen(user_id)

    if is_user_in_chat(user_id):
        bot.send_message(user_id, '⚠️ شما در حال چت هستید! ابتدا چت را پایان دهید.')
        return

    user_row = get_user(user_id)
    if not user_row:
        return

    prof_id = get_or_create_profile_id(user_id)
    display_name = escape_html(user_row[4] or 'نامشخص')
    age = escape_html(str(user_row[6]) if user_row[6] else 'نامشخص')
    edu = escape_html(user_row[7] or 'نامشخص')
    loc = escape_html(user_row[8] or 'نامشخص')
    gender_key = user_row[9]
    gender_fa = GENDER_MAP.get(gender_key, (gender_key, 'O'))[0] if gender_key else 'نامشخص'
    last_seen_text = format_last_seen(user_id, user_row[10])
    karma = user_row[11] if user_row[11] is not None else 0
    photo_id = user_row[13]
    original_bio = user_row[14]
    bio = escape_html(original_bio or 'ثبت نشده')
    pos_fa = POS_MAP.get(get_user_position(user_id), 'نامشخص')

    likes, dislikes = get_user_vote_stats(user_id)
    satisfaction_text = get_satisfaction_display(likes, dislikes)
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT COUNT(*) FROM chats WHERE user1_id = ? OR user2_id = ?', (user_id, user_id))
        chat_count = cur.fetchone()[0]

    profile_text = (
        f'👤 <b>پروفایل کاربری شما</b>\n'
        f'━━━━━━━━━━━━━━━━━━━\n'
        f'🆔 <b>آیدی چت:</b> /user_{prof_id}\n'
        f'🔸 <b>نام نمایشی:</b> {display_name}\n'
        f'🔹 <b>سن:</b> {age} سال  |  <b>جنسیت:</b> {gender_fa}\n'
        f'🔹 <b>پوزیشن:</b> {pos_fa}  |  <b>تحصیلات:</b> {edu}\n'
        f'📍 <b>موقعیت:</b> {loc}\n'
        f'✨ <b>کاریزما:</b> {karma}  |  📊 <b>رضایت:</b> {satisfaction_text}\n'
        f'👍 <b>لایک:</b> {likes}  |  👎 <b>دیس‌لایک:</b> {dislikes}\n'
        f'💬 <b>تعداد کل چت‌ها:</b> {chat_count}\n'
        f'👁‍🗨 <b>وضعیت فعالیت:</b> {last_seen_text}\n'
        f'━━━━━━━━━━━━━━━━━━━\n'
        f'❞ <b>درباره من و توضیحات:</b>\n{bio}'
    )

    markup = types.InlineKeyboardMarkup(row_width=2)
    markup.add(types.InlineKeyboardButton('ویرایش نام ✏️', callback_data='edit_name'),
               types.InlineKeyboardButton('ویرایش سن 🎂', callback_data='edit_age'),
               types.InlineKeyboardButton('ویرایش استان 📍', callback_data='edit_location'),
               types.InlineKeyboardButton('ویرایش تحصیلات 🎓', callback_data='edit_edu'))
    markup.add(types.InlineKeyboardButton('ویرایش جنسیت 🌈', callback_data='edit_gender'),
               types.InlineKeyboardButton('ویرایش پوز 🎭', callback_data='edit_pos'))
    markup.add(types.InlineKeyboardButton('ویرایش بیوگرافی 📝', callback_data='edit_bio'),
               types.InlineKeyboardButton('📷 ثبت/ویرایش عکس', callback_data='edit_photo'))
    markup.add(types.InlineKeyboardButton('🗑 حذف عکس', callback_data='delete_photo'))

    _send_profile_card(user_id, profile_text, markup, photo_id=photo_id)

    # یادآوری مستقل از مسیر ارسال عکس اجرا می‌شود.
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT profile_reward_claimed FROM users WHERE user_id = ?', (user_id,))
        reward_row = cur.fetchone()
    reward_claimed = reward_row[0] if reward_row else 1

    if reward_claimed == 0 and (not photo_id or not original_bio):
        missing = []
        if not photo_id:
            missing.append('«عکس»')
        if not original_bio:
            missing.append('«بیوگرافی»')
        missing_text = ' و '.join(missing)
        reminder = (
            '🎁 <b>یک جایزه در انتظار شماست!</b>\n'
            f'با اضافه کردن {missing_text} از طریق دکمه‌های بالا، ۱۵ سکه دریافت کنید.'
        )
        try:
            bot.send_message(user_id, reminder, parse_mode='HTML')
        except Exception:
            logger.warning('profile reward reminder failed for user %s', user_id, exc_info=True)

# 🌟 تابع کمکی برای رفرش کردن خودکار صفحه پروفایل بدون نیاز به دکمه
def force_show_profile(user_id):
    class FakeUser:
        def __init__(self, uid): self.id = uid
    class FakeMessage:
        def __init__(self, uid): self.from_user = FakeUser(uid)
    show_profile(FakeMessage(user_id))

# 🌟 هندلر دکمه انصراف اختصاصی برای پروفایل
@bot.callback_query_handler(func=lambda call: call.data == 'cancel_profile_edit')
def cancel_profile_edit_step(call):
    user_id = call.from_user.id
    bot.clear_step_handler_by_chat_id(call.message.chat.id)
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
    bot.answer_callback_query(call.id, "❌ ویرایش لغو شد.")
    force_show_profile(user_id)

# ============================================================================
# 13) Profile completion reward / return reward atomicity
# ============================================================================
def check_and_reward_profile_completion(user_id):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('''UPDATE users
                       SET coins = COALESCE(coins, 0) + 15,
                           profile_reward_claimed = 1
                       WHERE user_id = ?
                         AND profile_reward_claimed = 0
                         AND photo_file_id IS NOT NULL AND photo_file_id <> ''
                         AND bio IS NOT NULL AND TRIM(bio) <> '' ''', (user_id,))
        if cur.rowcount != 1:
            return False
        conn.commit()

    try:
        bot.send_message(user_id,
                         '🎉 <b>تبریک!</b> پروفایل شما کامل شد و <b>۱۵ سکه</b> جایزه گرفتید! 🎁',
                         parse_mode='HTML')
    except Exception:
        pass
    return True

@bot.callback_query_handler(func=lambda call: call.data == 'claim_return_reward')
def claim_return_reward_callback(call):
    user_id = call.from_user.id
    update_last_seen(user_id)
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('''UPDATE users
                       SET coins = COALESCE(coins, 0) + 10,
                           pending_return_reward = 0
                       WHERE user_id = ? AND pending_return_reward = 1''', (user_id,))
        if cur.rowcount != 1:
            conn.rollback()
            bot.answer_callback_query(call.id, '❌ این هدیه قبلاً دریافت شده یا منقضی شده است.', show_alert=True)
            return
        conn.commit()

    bot.answer_callback_query(call.id, '🎉 ۱۰ سکه هدیه بازگشت واریز شد!', show_alert=True)
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
    bot.send_message(user_id, '🎁 <b>هدیه دریافت شد!</b>', parse_mode='HTML')
    show_main_menu(user_id, user_id in ADMINS)

def process_display_name(message, prompt_msg_id):
    user_id = message.from_user.id
    # پاک کردن پیام ربات و متن کاربر برای خلوت ماندن صفحه
    try: bot.delete_message(message.chat.id, message.message_id)
    except: pass
    try: bot.delete_message(message.chat.id, prompt_msg_id)
    except: pass

    if message.content_type != 'text' or message.text in ['/cancel', 'لغو', 'انصراف', 'بازگشت', '/start']:
        bot.clear_step_handler_by_chat_id(message.chat.id)
        bot.send_message(user_id, "❌ عملیات لغو شد.")
        force_show_profile(user_id)
        return

    update_display_name(user_id, message.text[:50])
    bot.send_message(user_id, "✅ نام نمایشی شما با موفقیت بروزرسانی شد.")
    force_show_profile(user_id)

def process_new_bio(message, prompt_msg_id):
    user_id = message.from_user.id
    try: bot.delete_message(message.chat.id, message.message_id)
    except: pass
    try: bot.delete_message(message.chat.id, prompt_msg_id)
    except: pass

    if message.content_type != 'text' or message.text in ['/cancel', 'لغو', 'انصراف', 'بازگشت', '/start']:
        bot.clear_step_handler_by_chat_id(message.chat.id)
        bot.send_message(user_id, "❌ عملیات لغو شد.")
        force_show_profile(user_id)
        return
        
    update_bio(user_id, message.text[:100])
    bot.send_message(user_id, "✅ بیوگرافی شما با موفقیت بروزرسانی شد.")
    check_and_reward_profile_completion(user_id)
    force_show_profile(user_id)

def process_new_photo(message, prompt_msg_id):
    user_id = message.from_user.id
    try: bot.delete_message(message.chat.id, message.message_id)
    except: pass
    try: bot.delete_message(message.chat.id, prompt_msg_id)
    except: pass

    if message.content_type != 'photo':
        bot.clear_step_handler_by_chat_id(message.chat.id)
        bot.send_message(user_id, "❌ عملیات لغو شد. (لطفاً فقط عکس ارسال کنید)")
        force_show_profile(user_id)
        return
        
    photo_file_id = message.photo[-1].file_id
    update_photo(user_id, photo_file_id)
    bot.send_message(user_id, "✅ عکس پروفایل شما با موفقیت ثبت شد.")
    check_and_reward_profile_completion(user_id)
    force_show_profile(user_id)

@bot.callback_query_handler(func=lambda call: call.data == 'delete_photo')
def process_delete_photo(call):
    user_id = call.from_user.id
    update_last_seen(user_id)
    
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('UPDATE users SET photo_file_id = NULL WHERE user_id = ?', (user_id,))
        conn.commit()
        
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
        
    bot.answer_callback_query(call.id, "✅ عکس پروفایل شما حذف شد.", show_alert=True)
    force_show_profile(user_id)

# ==========================================
# 🌟 هندلرهای لغو (Cancel)
# ==========================================
# 🌟 تابع مدیریت دکمه شیشه‌ای لغو در بخش‌های مدیریتی
@bot.callback_query_handler(func=lambda call: call.data == 'admin_cancel_step')
def admin_cancel_step_callback(call):
    admin_id = call.from_user.id
    if admin_id not in ADMINS: return
    
    bot.clear_step_handler_by_chat_id(call.message.chat.id)
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
        
    bot.answer_callback_query(call.id, "❌ عملیات لغو شد.")
    show_admin_panel(admin_id)

# ۲. هندلر دکمه لغو برای کاربران عادی (مثل آپلود عکس پروفایل)
@bot.callback_query_handler(func=lambda call: call.data == 'cancel_step')
def cancel_any_step(call):
    user_id = call.from_user.id
    bot.clear_step_handler_by_chat_id(call.message.chat.id)
    
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
        
    bot.answer_callback_query(call.id, "❌ عملیات لغو شد.")
    show_main_menu(user_id, user_id in ADMINS)

def update_bio(user_id, bio):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('UPDATE users SET bio = ? WHERE user_id = ?', (bio, user_id))
        conn.commit()

def update_photo(user_id, photo_file_id):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('UPDATE users SET photo_file_id = ? WHERE user_id = ?', (photo_file_id, user_id))
        conn.commit()

@bot.callback_query_handler(func=lambda call: call.data.startswith('edit_'))
def handle_profile_edits(call):
    user_id = call.from_user.id
    update_last_seen(user_id)
    action = call.data
    
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except Exception: pass
    
    cancel_markup = types.InlineKeyboardMarkup()
    cancel_markup.add(types.InlineKeyboardButton("❌ انصراف", callback_data="cancel_profile_edit"))
    
    if action == "edit_name":
        msg = bot.send_message(user_id, "✏️ لطفا نام نمایشی جدید خود را وارد کنید:", reply_markup=cancel_markup)
        bot.register_next_step_handler(msg, process_display_name, msg.message_id)
    elif action == "edit_bio":
        msg = bot.send_message(user_id, "📝 لطفا متن بیوگرافی جدید خود را ارسال کنید (حداکثر ۱۰۰ کاراکتر):", reply_markup=cancel_markup)
        bot.register_next_step_handler(msg, process_new_bio, msg.message_id)
    elif action == "edit_photo":
        msg = bot.send_message(user_id, "📸 لطفاً عکس جدید پروفایل خود را ارسال کنید (فقط Photo):", reply_markup=cancel_markup)
        bot.register_next_step_handler(msg, process_new_photo, msg.message_id)
    elif action == "edit_age":
        markup = types.InlineKeyboardMarkup(row_width=5)
        buttons = [types.InlineKeyboardButton(str(age), callback_data=f"editage_{age}_{user_id}") for age in range(12, 41)]
        markup.add(*buttons)
        markup.add(types.InlineKeyboardButton("❌ انصراف", callback_data="cancel_profile_edit"))
        bot.send_message(user_id, "🎂 سن جدید خود را انتخاب کنید:", reply_markup=markup)
    elif action == "edit_location":
        markup = types.InlineKeyboardMarkup(row_width=3)
        buttons = [types.InlineKeyboardButton(p, callback_data=f"editloc_{provinces.index(p)}_{user_id}") for p in provinces]
        markup.add(*buttons)
        markup.add(types.InlineKeyboardButton("❌ انصراف", callback_data="cancel_profile_edit"))
        bot.send_message(user_id, "📍 استان جدید خود را انتخاب کنید:", reply_markup=markup)
    elif action == "edit_edu":
        markup = types.InlineKeyboardMarkup(row_width=2)
        edus = ['زیر دیپلم', 'دیپلم', 'فوق دیپلم', 'لیسانس', 'فوق لیسانس', 'دکترا']
        buttons = [types.InlineKeyboardButton(e, callback_data=f"editedu_{edus.index(e)}_{user_id}") for e in edus]
        markup.add(*buttons)
        markup.add(types.InlineKeyboardButton("❌ انصراف", callback_data="cancel_profile_edit"))
        bot.send_message(user_id, "🎓 تحصیلات جدید خود را انتخاب کنید:", reply_markup=markup)
    elif action == "edit_gender":
        markup = types.InlineKeyboardMarkup(row_width=2)
        buttons = [types.InlineKeyboardButton(g_val[0], callback_data=f"editgender_{g_key}_{user_id}") for g_key, g_val in GENDER_MAP.items()]
        markup.add(*buttons)
        markup.add(types.InlineKeyboardButton("❌ انصراف", callback_data="cancel_profile_edit"))
        bot.send_message(user_id, "🌈 لطفاً جنسیت/گرایش جدید خود را انتخاب کنید:", reply_markup=markup)
    elif action == "edit_pos":
        markup = types.InlineKeyboardMarkup(row_width=3)
        markup.add(
            types.InlineKeyboardButton("تاپ 🔝", callback_data=f"editpos_top_{user_id}"),
            types.InlineKeyboardButton("ورس ↔️", callback_data=f"editpos_vers_{user_id}"),
            types.InlineKeyboardButton("بات 🔽", callback_data=f"editpos_bottom_{user_id}")
        )
        markup.add(types.InlineKeyboardButton("ترجیح می‌دهم نگویم 👤", callback_data=f"editpos_unknown_{user_id}"))
        markup.add(types.InlineKeyboardButton("❌ انصراف", callback_data="cancel_profile_edit"))
        bot.send_message(user_id, "🎭 لطفاً گرایش نقشی (پوز) جدید خود را انتخاب کنید:", reply_markup=markup)

@bot.callback_query_handler(func=lambda call: call.data.startswith('editage_'))
def process_edit_age_inline(call):
    parts = call.data.split('_')
    age = int(parts[1])
    user_id = int(parts[2])
    
    if call.from_user.id != user_id: return
    update_age(user_id, age)
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
    bot.answer_callback_query(call.id, "✅ سن شما با موفقیت بروزرسانی شد.")
    force_show_profile(user_id)

@bot.callback_query_handler(func=lambda call: call.data.startswith('editedu_'))
def process_edit_edu_inline(call):
    parts = call.data.split('_')
    edus = ['زیر دیپلم', 'دیپلم', 'فوق دیپلم', 'لیسانس', 'فوق لیسانس', 'دکترا']
    edu = edus[int(parts[1])]
    user_id = int(parts[2])
    
    if call.from_user.id != user_id: return
    update_education(user_id, edu)
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
    bot.answer_callback_query(call.id, "✅ تحصیلات شما بروزرسانی شد.")
    force_show_profile(user_id)

@bot.callback_query_handler(func=lambda call: call.data.startswith('editloc_'))
def process_edit_loc_inline(call):
    parts = call.data.split('_')
    prov_index = int(parts[1])
    user_id = int(parts[2])
    
    if call.from_user.id != user_id: return
    province = provinces[prov_index]
    
    # پاک کردن پیام قبلی (لیست استان‌ها)
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
    
    markup = types.InlineKeyboardMarkup(row_width=3)
    cities = iran_cities.get(province, ['مرکز استان', 'سایر'])
    buttons = [types.InlineKeyboardButton(c, callback_data=f"editcity_{prov_index}_{cities.index(c)}_{user_id}") for c in cities]
    markup.add(*buttons)
    
    # 🌟 اضافه شدن دکمه انصراف به مرحله انتخاب شهر تا کاربر گیر نکند
    markup.add(types.InlineKeyboardButton("❌ انصراف", callback_data="cancel_profile_edit"))
    
    bot.send_message(user_id, f"🏙 لطفاً شهر جدید خود را در استان {province} انتخاب کنید:", reply_markup=markup)

@bot.callback_query_handler(func=lambda call: call.data.startswith('editcity_'))
def process_edit_city_inline(call):
    parts = call.data.split('_')
    prov_index = int(parts[1])
    city_index = int(parts[2])
    user_id = int(parts[3])
    
    if call.from_user.id != user_id: return
        
    province = provinces[prov_index]
    cities = iran_cities.get(province, ['مرکز استان', 'سایر'])
    city = cities[city_index]
    full_location = f"{province} - {city}"
    
    update_last_seen(user_id)
    update_location(user_id, full_location)
    
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
        
    bot.answer_callback_query(call.id, f"✅ شهر به {full_location} تغییر یافت.", show_alert=False)
    force_show_profile(user_id)

@bot.callback_query_handler(func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('editgender_'))
def process_edit_gender_inline(call):
    parts = call.data.split('_')
    gender_key = parts[1]
    user_id = int(parts[2])
    
    if call.from_user.id != user_id: return
        
    update_gender(user_id, gender_key)
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
    bot.answer_callback_query(call.id, "✅ جنسیت/گرایش بروزرسانی شد.")
    force_show_profile(user_id)

@bot.callback_query_handler(func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('editpos_'))
def process_edit_pos_inline(call):
    parts = call.data.split('_')
    pos_key = parts[1]
    user_id = int(parts[2])
    
    if call.from_user.id != user_id: return
        
    update_position(user_id, pos_key)
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
    
    bot.answer_callback_query(call.id, "✅ پوزیشن شما بروزرسانی شد.")
    force_show_profile(user_id)

@bot.message_handler(func=lambda message: message.text == '🔗 لینک ناشناس من')
def generate_private_link(message):
    user_id = message.from_user.id
    update_last_seen(user_id)

    try:
        link_token = get_or_create_user_link(user_id)
        if not link_token or not re.fullmatch(r'chat_[0-9a-fA-F]{32}', str(link_token)):
            raise RuntimeError('private link token is missing or invalid')

        bot_username = get_bot_username()
        if not bot_username:
            raise RuntimeError('bot username is unavailable')

        user_link = f'https://t.me/{bot_username}?start={link_token}'
        share_text = (
            'سلام! از طریق لینک زیر می‌تونی به‌صورت ناشناس با من چت کنی 👇\n'
            + user_link
        )
        markup = types.InlineKeyboardMarkup(row_width=1)
        markup.add(
            types.InlineKeyboardButton('اشتراک‌گذاری 📤', switch_inline_query=share_text),
            types.InlineKeyboardButton('🔄 تغییر لینک (باطل کردن قبلی)', callback_data='reset_link')
        )

        bot.send_message(
            message.chat.id,
            f'🔗 <b>لینک ناشناس اختصاصی شما:</b>\n\n<code>{escape_html(user_link)}</code>\n\n'
            '⚠️ با تغییر لینک، لینک قبلی دیگر کار نمی‌کند.',
            reply_markup=markup,
            parse_mode='HTML'
        )
    except Exception:
        logger.error('generate_private_link failed user=%s', user_id, exc_info=True)
        bot.send_message(user_id, '❌ ساخت لینک ناشناس فعلاً ممکن نیست؛ کمی بعد دوباره تلاش کنید.')

@bot.callback_query_handler(func=lambda call: call.data == 'reset_link')
def reset_private_link(call):
    user_id = call.from_user.id
    try:
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('BEGIN IMMEDIATE')
            cur.execute('SELECT 1 FROM users WHERE user_id = ?', (user_id,))
            if cur.fetchone() is None:
                conn.rollback()
                bot.answer_callback_query(call.id, '❌ حساب کاربری پیدا نشد.', show_alert=True)
                return

            new_link = _generate_private_link_token(cur)
            cur.execute('UPDATE users SET unique_link = ? WHERE user_id = ?', (new_link, user_id))
            if cur.rowcount != 1:
                conn.rollback()
                bot.answer_callback_query(call.id, '❌ ساخت لینک جدید ناموفق بود.', show_alert=True)
                return
            conn.commit()

        bot_username = get_bot_username()
        if not bot_username:
            bot.answer_callback_query(call.id, '❌ نام کاربری ربات دریافت نشد.', show_alert=True)
            return

        user_link = f'https://t.me/{bot_username}?start={new_link}'
        share_text = 'سلام! از طریق لینک زیر می‌تونی کاملاً ناشناس بهم پیام بدی یا باهام چت کنی 👇\n' + user_link
        markup = types.InlineKeyboardMarkup(row_width=1)
        markup.add(
            types.InlineKeyboardButton('اشتراک‌گذاری 📤', switch_inline_query=share_text),
            types.InlineKeyboardButton('🔄 تغییر لینک (باطل کردن قبلی)', callback_data='reset_link')
        )

        bot.answer_callback_query(call.id, '✅ لینک قبلی باطل و لینک جدید ساخته شد!', show_alert=True)
        bot.edit_message_text(
            f'🔗 *لینک ناشناس اختصاصی جدید شما:*\n\n`{user_link}`\n\n'
            '⚠️ *لینک‌های قبلی دیگر کار نمی‌کنند؛ لینک جدید را دوباره به اشتراک بگذارید.*',
            call.message.chat.id,
            call.message.message_id,
            reply_markup=markup,
            parse_mode='Markdown'
        )
    except Exception:
        logger.error('reset_private_link failed for user=%s', user_id, exc_info=True)
        try:
            bot.answer_callback_query(call.id, '❌ خطا در ساخت لینک جدید.', show_alert=True)
        except Exception:
            pass

@bot.message_handler(func=lambda message: message.text == 'پنل مدیریت 👨‍💻')
def admin_panel_button(message):
    if message.from_user.id in ADMINS:
        show_admin_panel(message.chat.id)
    else:
        bot.send_message(message.chat.id, "⚠️ شما دسترسی به این بخش را ندارید!")

@bot.callback_query_handler(func=lambda call: call.data == 'admin_close_panel')
@admin_only
def close_admin_panel_callback(call):
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
    bot.answer_callback_query(call.id, "✅ پنل مدیریت بسته شد.")

def show_admin_panel(chat_id):
    markup = types.InlineKeyboardMarkup(row_width=2)

    markup.add(
        types.InlineKeyboardButton("📊 آمار ربات", callback_data="admin_stats"),
        types.InlineKeyboardButton("🚦 وضعیت صف‌ها", callback_data="admin_queues")
    )
    markup.add(
        types.InlineKeyboardButton("🟢 کاربران آنلاین", callback_data="admin_online"),
        types.InlineKeyboardButton("👥 لیست کاربران", callback_data="admin_users")
    )
    markup.add(
        types.InlineKeyboardButton("📢 ارسال همگانی", callback_data="admin_broadcast"),
        types.InlineKeyboardButton("📣 مدیریت کانال‌ها", callback_data="admin_channels")
    )
    markup.add(
        types.InlineKeyboardButton("🚫 مسدود کردن", callback_data="admin_block"),
        types.InlineKeyboardButton("📋 لیست مسدود", callback_data="admin_blocked")
    )
    markup.add(
        types.InlineKeyboardButton("👤 مدیریت تک‌کاربر (CRM)", callback_data="admin_manageuser"),
        types.InlineKeyboardButton("💫 پرداخت‌ها / ریفاند Stars", callback_data="sadm_list")
    )
    markup.add(
        types.InlineKeyboardButton("🎧 تیکت‌های پشتیبانی", callback_data="admin_tickets"),
        types.InlineKeyboardButton("🎁 هدیه سکه همگانی 🪙", callback_data="admin_global_gift")
    )
    markup.add(types.InlineKeyboardButton("🔙 بستن پنل", callback_data="admin_close_panel"))

    bot.send_message(chat_id, "🔐 پنل مدیریت:", reply_markup=markup)

# ==========================================
# 🌟 سیستم پیشرفته مدیریت تک‌کاربر (CRM ادمین)
# ==========================================
@bot.callback_query_handler(func=lambda call: call.data == 'admin_manageuser')
def admin_manage_user_start(call):
    admin_id = call.from_user.id
    if admin_id not in ADMINS: return
    bot.answer_callback_query(call.id)

    markup = types.InlineKeyboardMarkup()
    markup.add(types.InlineKeyboardButton("❌ لغو", callback_data="admin_cancel_step"))
    
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
    
    msg = bot.send_message(admin_id, "🔢 لطفاً <b>آیدی عددی (User ID)</b> کاربر مورد نظر را بفرستید:", reply_markup=markup, parse_mode='HTML')
    bot.register_next_step_handler(msg, process_manage_user_id)

@admin_only
def process_manage_user_id(message):
    admin_id = message.from_user.id
    if message.content_type == 'text' and message.text.strip() in ['لغو', 'cancel', 'بازگشت', '/start']:
        bot.clear_step_handler_by_chat_id(message.chat.id)
        show_admin_panel(admin_id)
        return

    try:
        target_id = int(message.text.strip())
        show_user_management_panel(admin_id, target_id)
    except ValueError:
        bot.clear_step_handler_by_chat_id(message.chat.id) # 🌟 فیکس باگ گیر کردن
        bot.send_message(admin_id, "❌ آیدی نامعتبر است! لطفاً فقط عدد ارسال کنید.")
        show_admin_panel(admin_id)  

def show_user_management_panel(admin_id, target_id, message_id_to_edit=None):
    user_row = get_user(target_id)
    if not user_row:
        bot.send_message(admin_id, f"❌ کاربری با آیدی <code>{target_id}</code> در ربات یافت نشد!", parse_mode='HTML')
        show_admin_panel(admin_id)
        return

    # دریافت اطلاعات دقیق از دیتابیس
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT display_name, username, last_seen, coins, level, join_date FROM users WHERE user_id = ?', (target_id,))
        user_data = cur.fetchone()
        cur.execute('SELECT COUNT(*) FROM chats WHERE user1_id = ? OR user2_id = ?', (target_id, target_id))
        chat_count = cur.fetchone()[0]

    display_name, username, last_seen_str, coins, level, join_date = user_data
    is_blocked_status = is_blocked(target_id)

    # قالب‌بندی وضعیت آنلاین بودن با تابع هوشمند ربات
    online_status = format_last_seen(target_id, last_seen_str)
    username_txt = f"@{username}" if username else "ندارد"

    text = (
        f"👤 <b>گزارش وضعیت کاربر</b>\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
        f"🆔 <b>آیدی عددی:</b> <code>{target_id}</code>\n"
        f"👤 <b>نام:</b> {escape_html(display_name)} | <b>یوزرنیم:</b> {username_txt}\n"
        f"📅 <b>تاریخ عضویت:</b> <i>{join_date[:10]}</i>\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
        f"💰 <b>موجودی سکه:</b> {coins} سکه\n"
        f"🏅 <b>سطح فعلی:</b> Level {level}\n"
        f"💬 <b>تعداد کل چت‌ها:</b> {chat_count} بار\n"
        f"👁‍🗨 <b>وضعیت آنلاین:</b> {online_status}\n"
        f"🚫 <b>وضعیت حساب:</b> {'مسدود است 🔴' if is_blocked_status else 'آزاد 🟢'}\n\n"
        f"👇 <i>عملیات مورد نظر را انتخاب کنید:</i>"
    )

    markup = types.InlineKeyboardMarkup(row_width=2)
    
    # دکمه مسدود/آزادسازی داینامیک در پنل شیشه‌ای
    block_btn_txt = "🔓 رفع مسدودی" if is_blocked_status else "🔒 مسدود کردن"
    
    markup.add(
        types.InlineKeyboardButton("➕ افزودن سکه", callback_data=f"um_addcoin_{target_id}"),
        types.InlineKeyboardButton("➖ کسر سکه", callback_data=f"um_subcoin_{target_id}")
    )
    markup.add(
        types.InlineKeyboardButton("✉️ ارسال پیام شخصی", callback_data=f"um_sendmsg_{target_id}"),
        types.InlineKeyboardButton(block_btn_txt, callback_data=f"um_block_{target_id}")
    )
    # 🌟 دکمه جدید برای مشاهده پروفایل فرد
    markup.add(
        types.InlineKeyboardButton("👁 مشاهده کامل پروفایل فرد", callback_data=f"um_viewprof_{target_id}")
    )
    markup.add(types.InlineKeyboardButton("🔙 بازگشت به پنل اصلی", callback_data="admin_back"))

    if message_id_to_edit:
        try: bot.edit_message_text(text, admin_id, message_id_to_edit, reply_markup=markup, parse_mode='HTML')
        except: pass
    else:
        bot.send_message(admin_id, text, reply_markup=markup, parse_mode='HTML')

# ==========================================
# 🎁 سیستم هدیه سکه همگانی به تمام ممبرها
# ==========================================
@bot.callback_query_handler(func=lambda call: call.data == 'admin_global_gift')
@admin_only
def admin_global_gift_start(call):
    admin_id = call.from_user.id
    bot.answer_callback_query(call.id)

    markup = types.InlineKeyboardMarkup()
    markup.add(types.InlineKeyboardButton("❌ لغو", callback_data="admin_cancel_step"))
    
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except Exception: pass
    
    msg = bot.send_message(
        admin_id, 
        "🎁 لطفاً تعداد سکه‌ای که می‌خواهید به <b>تمامی ممبرهای ربات</b> هدیه دهید را به صورت عدد ارسال کنید:", 
        reply_markup=markup, 
        parse_mode='HTML'
    )
    bot.register_next_step_handler(msg, process_global_gift_amount)

@admin_only
def process_global_gift_amount(message):
    admin_id = message.from_user.id
    
    if message.content_type != 'text' or not message.text:
        bot.clear_step_handler_by_chat_id(message.chat.id)
        bot.send_message(admin_id, "❌ عملیات لغو شد! لطفاً فقط عدد ارسال کنید.")
        show_admin_panel(admin_id)
        return

    if message.text.strip() in ['لغو', 'cancel', 'بازگشت', '/start', 'انصراف']:
        bot.clear_step_handler_by_chat_id(message.chat.id)
        bot.send_message(admin_id, "❌ عملیات هدیه همگانی لغو شد.")
        show_admin_panel(admin_id)
        return

    try:
        amount = int(message.text.strip())
        if amount <= 0:
            raise ValueError

        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('BEGIN IMMEDIATE')
            cur.execute('UPDATE users SET coins = COALESCE(coins, 0) + ?', (amount,))
            cur.execute('SELECT COUNT(*) FROM users')
            total_users = cur.fetchone()[0]
            conn.commit()

        bot.send_message(
            admin_id, 
            f"✅ عملیات با موفقیت انجام شد!\n\n"
            f"🎁 مقدار <b>{amount} سکه</b> به حساب <b>{total_users} کاربر</b> افزوده شد. 🪙", 
            parse_mode='HTML'
        )

    except ValueError:
        bot.clear_step_handler_by_chat_id(message.chat.id)
        bot.send_message(admin_id, "❌ مقدار نامعتبر است! لطفاً فقط یک عدد صحیح و مثبت ارسال کنید.")
    
    show_admin_panel(admin_id)

# ----------------- هندلر دکمه‌های شیشه‌ای مدیریت کاربر -----------------
@bot.callback_query_handler(func=lambda call: bool(call.data) and call.data.startswith('um_'))
@admin_only
def handle_user_management_actions(call):
    """پردازش امن دکمه‌های پنل مدیریت یک کاربر."""
    global DEFAULT_PHOTO_FILE_ID

    admin_id = call.from_user.id
    call_message = getattr(call, 'message', None)
    call_chat = getattr(call_message, 'chat', None)

    # این پنل فقط باید از پیام خصوصی خود ادمین اجرا شود.
    if (
        call_chat is None
        or getattr(call_chat, 'type', None) != 'private'
        or getattr(call_chat, 'id', None) != admin_id
    ):
        try:
            bot.answer_callback_query(
                call.id,
                '⛔ این عملیات فقط از پنل خصوصی مدیریت قابل اجراست.',
                show_alert=True
            )
        except Exception:
            logger.warning('Could not answer non-private user-management callback', exc_info=True)
        return

    parts = (call.data or '').split('_')
    if len(parts) != 3 or parts[0] != 'um':
        bot.answer_callback_query(call.id, '🚫 دکمه نامعتبر یا منقضی است.', show_alert=True)
        return

    action = parts[1]
    if action not in {'block', 'addcoin', 'subcoin', 'sendmsg', 'viewprof', 'backto', 'cancel'}:
        bot.answer_callback_query(call.id, '🚫 نوع عملیات معتبر نیست.', show_alert=True)
        return

    try:
        target_id = int(parts[2])
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 شناسه کاربر نامعتبر است.', show_alert=True)
        return

    if target_id <= 0:
        bot.answer_callback_query(call.id, '🚫 شناسه کاربر نامعتبر است.', show_alert=True)
        return

    try:
        target_row = get_user(target_id)
    except Exception:
        logger.error('User-management target lookup failed: target=%s', target_id, exc_info=True)
        bot.answer_callback_query(call.id, '⚠️ بررسی اطلاعات کاربر ناموفق بود.', show_alert=True)
        return

    if not target_row:
        bot.answer_callback_query(call.id, '❌ کاربر مورد نظر پیدا نشد.', show_alert=True)
        return

    if action == 'block':
        try:
            if is_blocked(target_id):
                unblock_user(target_id)
                result_text = '✅ کاربر از مسدودی خارج شد.'
            else:
                block_user(target_id)
                result_text = '🚫 کاربر با موفقیت مسدود شد.'
        except Exception:
            logger.error('User-management block toggle failed: target=%s', target_id, exc_info=True)
            bot.answer_callback_query(call.id, '❌ تغییر وضعیت مسدودی انجام نشد.', show_alert=True)
            return

        bot.answer_callback_query(call.id, result_text, show_alert=True)
        show_user_management_panel(admin_id, target_id, call_message.message_id)
        return

    if action in ('addcoin', 'subcoin'):
        amount_mode = 'add' if action == 'addcoin' else 'sub'
        prompt_text = (
            '➕ مقدار سکه‌ای که می‌خواهید به این کاربر اضافه کنید را بفرستید:'
            if amount_mode == 'add'
            else '➖ مقدار سکه‌ای که می‌خواهید از این کاربر کسر کنید را بفرستید:'
        )
        markup = types.InlineKeyboardMarkup()
        markup.add(types.InlineKeyboardButton('❌ لغو این مرحله', callback_data=f'um_cancel_{target_id}'))
        try:
            bot.answer_callback_query(call.id)
            msg = bot.send_message(admin_id, prompt_text, reply_markup=markup)
            bot.register_next_step_handler(msg, process_um_modify_coin, target_id, amount_mode)
        except Exception:
            logger.error('Could not start user coin modification: target=%s', target_id, exc_info=True)
            try:
                bot.send_message(admin_id, '❌ شروع عملیات تغییر سکه ناموفق بود؛ دوباره تلاش کنید.')
            except Exception:
                pass
        return

    if action == 'sendmsg':
        markup = types.InlineKeyboardMarkup()
        markup.add(types.InlineKeyboardButton('❌ لغو این مرحله', callback_data=f'um_cancel_{target_id}'))
        try:
            bot.answer_callback_query(call.id)
            msg = bot.send_message(
                admin_id,
                '✉️ پیام خود را برای ارسال به دایرکت کاربر بنویسید (متن، عکس و...):',
                reply_markup=markup
            )
            bot.register_next_step_handler(msg, process_um_send_msg, target_id)
        except Exception:
            logger.error('Could not start admin direct-message step: target=%s', target_id, exc_info=True)
            try:
                bot.send_message(admin_id, '❌ شروع ارسال پیام ناموفق بود؛ دوباره تلاش کنید.')
            except Exception:
                pass
        return

    if action == 'viewprof':
        try:
            bot.answer_callback_query(call.id)
            # اطلاعات پروفایل از رکورد تازه خوانده می‌شود تا داده قدیمی نمایش داده نشود.
            target_row = get_user(target_id)
            if not target_row:
                bot.send_message(admin_id, '❌ کاربر دیگر در دیتابیس وجود ندارد.')
                return

            prof_id = get_or_create_profile_id(target_id)
            display_name = escape_html(target_row[4] or 'ناشناس')
            age = escape_html(str(target_row[6]) if target_row[6] else 'نامشخص')
            edu = escape_html(target_row[7] or 'نامشخص')
            loc = escape_html(target_row[8] or 'نامشخص')
            gender_key = target_row[9]
            gender_fa = GENDER_MAP.get(gender_key, (gender_key, 'O'))[0] if gender_key else 'نامشخص'
            last_seen_str = target_row[10]
            karma = target_row[11] if target_row[11] is not None else 0
            photo_id = target_row[13]
            bio = escape_html(target_row[14] or 'بیوگرافی ثبت نشده است.')

            pos_key = get_user_position(target_id)
            pos_fa = POS_MAP.get(pos_key, 'نامشخص')
            last_seen_text = format_last_seen(target_id, last_seen_str)
            likes, dislikes = get_user_vote_stats(target_id)
            satisfaction_text = get_satisfaction_display(likes, dislikes)

            with get_db_connection() as conn:
                cur = conn.cursor()
                cur.execute(
                    'SELECT COUNT(*) FROM chats WHERE user1_id = ? OR user2_id = ?',
                    (target_id, target_id)
                )
                chat_count = cur.fetchone()[0]

            profile_text = (
                f'👤 <b>پروفایل کاربر</b>\n'
                f'━━━━━━━━━━━━━━━━━━━\n'
                f'🆔 <b>آیدی چت:</b> /user_{escape_html(prof_id or "نامشخص")}\n'
                f'🔸 <b>نام نمایشی:</b> {display_name}\n'
                f'🔹 <b>سن:</b> {age} سال  |  <b>جنسیت:</b> {escape_html(gender_fa)}\n'
                f'🔹 <b>پوزیشن:</b> {escape_html(pos_fa)}  |  <b>تحصیلات:</b> {edu}\n'
                f'📍 <b>موقعیت:</b> {loc}\n'
                f'✨ <b>کاریزما:</b> {karma}  |  📊 <b>رضایت:</b> {escape_html(satisfaction_text)}\n'
                f'👍 <b>لایک:</b> {likes}  |  👎 <b>دیس‌لایک:</b> {dislikes}\n'
                f'💬 <b>تعداد کل چت‌ها:</b> {chat_count}\n'
                f'👁‍🗨 <b>وضعیت فعالیت:</b> {escape_html(last_seen_text)}\n'
                f'━━━━━━━━━━━━━━━━━━━\n'
                f'❞ <b>درباره کاربر و توضیحات:</b>\n{bio}'
            )

            markup = types.InlineKeyboardMarkup()
            markup.add(
                types.InlineKeyboardButton(
                    '🔙 بازگشت به مدیریت این کاربر',
                    callback_data=f'um_backto_{target_id}'
                )
            )
            try:
                bot.delete_message(call_chat.id, call_message.message_id)
            except Exception:
                pass

            try:
                if photo_id:
                    bot.send_photo(admin_id, photo_id, caption=profile_text, reply_markup=markup, parse_mode='HTML')
                elif DEFAULT_PHOTO_FILE_ID:
                    bot.send_photo(admin_id, DEFAULT_PHOTO_FILE_ID, caption=profile_text, reply_markup=markup, parse_mode='HTML')
                else:
                    with open('default_profile.jpg', 'rb') as default_photo:
                        sent_photo = bot.send_photo(
                            admin_id, default_photo, caption=profile_text,
                            reply_markup=markup, parse_mode='HTML'
                        )
                    DEFAULT_PHOTO_FILE_ID = sent_photo.photo[-1].file_id
            except Exception:
                logger.warning('Could not send admin profile photo; falling back to text', exc_info=True)
                bot.send_message(admin_id, profile_text, reply_markup=markup, parse_mode='HTML')
        except Exception:
            logger.error('Admin profile view failed: target=%s', target_id, exc_info=True)
            try:
                bot.send_message(admin_id, '❌ نمایش پروفایل کاربر ناموفق بود.')
            except Exception:
                pass
        return

    if action == 'backto':
        bot.answer_callback_query(call.id)
        try:
            bot.delete_message(call_chat.id, call_message.message_id)
        except Exception:
            pass
        show_user_management_panel(admin_id, target_id)
        return

    if action == 'cancel':
        try:
            bot.clear_step_handler_by_chat_id(admin_id)
        except Exception:
            logger.warning('Could not clear admin step handler', exc_info=True)
        bot.answer_callback_query(call.id, '❌ عملیات لغو شد.')
        try:
            bot.delete_message(call_chat.id, call_message.message_id)
        except Exception:
            pass
        show_user_management_panel(admin_id, target_id)
        return
# ----------------- استپ هندلرهای مدیریت کاربر -----------------
@admin_only
def process_um_modify_coin(message, target_id, mode):
    admin_id = message.from_user.id
    if message.content_type == 'text' and message.text.strip() in ['لغو', 'cancel', 'بازگشت', '/start']:
        bot.clear_step_handler_by_chat_id(message.chat.id)
        show_user_management_panel(admin_id, target_id)
        return

    try:
        amount = int(message.text.strip())
        if amount <= 0: raise ValueError

        with get_db_connection() as conn:
            cur = conn.cursor()
            if mode == 'add':
                cur.execute('UPDATE users SET coins = coins + ? WHERE user_id = ?', (amount, target_id))
                bot.send_message(admin_id, f"✅ مقدار {amount} سکه با موفقیت به کاربر اضافه شد.")
                try: bot.send_message(target_id, f"🎁 <b>هدیه مدیریت!</b>\nمقدار {amount} سکه به حساب شما افزوده شد.", parse_mode='HTML')
                except: pass
            else:
                cur.execute('UPDATE users SET coins = MAX(0, coins - ?) WHERE user_id = ?', (amount, target_id))
                bot.send_message(admin_id, f"✅ مقدار {amount} سکه با موفقیت از کاربر کسر شد.")
            conn.commit()

    except ValueError:
        bot.clear_step_handler_by_chat_id(message.chat.id) # 🌟 فیکس باگ گیر کردن
        bot.send_message(admin_id, "❌ مقدار نامعتبر است! لطفاً یک عدد صحیح و مثبت ارسال کنید.")

    show_user_management_panel(admin_id, target_id)

@admin_only
def process_um_send_msg(message, target_id):
    admin_id = message.from_user.id
    if message.content_type == 'text' and message.text.strip() in ['لغو', 'cancel', 'بازگشت', '/start']:
        bot.clear_step_handler_by_chat_id(message.chat.id)
        show_user_management_panel(admin_id, target_id)
        return

    try:
        bot.copy_message(chat_id=target_id, from_chat_id=admin_id, message_id=message.message_id)
        bot.send_message(admin_id, "✅ پیام شما با موفقیت به دایرکت کاربر ارسال شد.")
    except Exception:
        bot.send_message(admin_id, "❌ ارسال پیام شکست خورد. احتمالاً کاربر ربات را بلاک کرده است.")

    show_user_management_panel(admin_id, target_id)

# ============================================================================
# 🎧 سیستم پیشرفته تیکت پشتیبانی دوطرفه (Two-Way Support Ticket System)
# با قفل هم‌زمانی ادمین‌ها، تشخیص پاسخ قبلی و همگام‌سازی زنده پنل مدیریت
# ============================================================================
SUPPORT_CATEGORIES = {
    'payment': '💳 خرید سکه و پرداخت‌ها',
    'bug': '🐞 گزارش باگ و مشکل فنی',
    'report': '🚨 شکایت یا پیگیری حساب',
    'suggestion': '💡 پیشنهادات و انتقادات',
    'general': '❓ سوالات و پشتیبانی عمومی',
}
SUPPORT_ALLOWED_CONTENT_TYPES = (
    'text', 'photo', 'video', 'voice', 'audio', 'document', 'animation', 'sticker'
)
SUPPORT_CAPTION_CONTENT_TYPES = (
    'photo', 'video', 'voice', 'audio', 'document', 'animation'
)
SUPPORT_USER_COOLDOWN_SECONDS = 15
SUPPORT_MAX_PENDING_PER_TICKET = 5
SUPPORT_ADMIN_LOCK_TTL_SECONDS = 180
_support_rate_lock = threading.Lock()
_support_user_last_sent = {}

MAIN_MENU_BUTTON_LABELS = frozenset({
    'شروع چت ناشناس 🎭',
    'پروفایل من 👤',
    'دوستان من 🤝',
    '🔗 لینک ناشناس من',
    'سکه 🪙',
    'Level Up 🚀',
    'پشتیبانی 🎧',
    'پنل مدیریت 👨‍💻',
    '🎲 سوال تصادفی',
    'افزودن به لیست دوستان 🤝',
    'مشاهده پروفایل مخاطب 👤',
    'ارسال آیدی من👻',
    'چت امن 🔒',
    'بازی 🎮',
})


def _support_ticket_status_badge(status):
    if status == 'open':
        return '🟡 در انتظار پاسخ ادمین'
    if status == 'answered':
        return '🟢 پاسخ داده شده'
    if status == 'closed':
        return '🔒 بسته شده'
    return '⚪ نامشخص'


def _extract_support_preview(message):
    ctype = getattr(message, 'content_type', 'text') or 'text'
    raw = (getattr(message, 'text', None) if ctype == 'text' else getattr(message, 'caption', None)) or ''
    raw = raw.strip()
    type_labels = {
        'photo': '🖼 [تصویر]',
        'video': '🎥 [ویدیو]',
        'voice': '🎤 [پیام صوتی]',
        'audio': '🎵 [فایل صوتی]',
        'document': '📎 [فایل/سند]',
        'animation': '🎞 [گیف]',
        'sticker': '😊 [استیکر]',
    }
    prefix = type_labels.get(ctype, '')
    if prefix and raw:
        preview = f'{prefix} {raw}'
    elif prefix:
        preview = prefix
    else:
        preview = raw or '[پیام متنی]'
    return preview[:250]


def _build_admin_ticket_markup(ticket_id, ticket_msg_id, target_user_id, msg_status='pending', ticket_status='open', answered_by_name=None):
    markup = types.InlineKeyboardMarkup(row_width=2)
    if ticket_status == 'closed' or msg_status == 'closed':
        markup.add(
            types.InlineKeyboardButton('🔒 تیکت بسته شده است', callback_data=f'sup_adm_info_{ticket_msg_id}')
        )
        markup.add(
            types.InlineKeyboardButton(f'📜 تاریخچه (#T{ticket_id})', callback_data=f'sup_adm_hist_{ticket_id}'),
            types.InlineKeyboardButton('👤 پنل کاربر (CRM)', callback_data=f'sup_adm_crm_{target_user_id}')
        )
        return markup

    if msg_status == 'answered':
        badge_name = (answered_by_name or 'ادمین')[:20]
        markup.add(
            types.InlineKeyboardButton(f'✅ پاسخ داده شد ({badge_name})', callback_data=f'sup_adm_info_{ticket_msg_id}')
        )
        markup.add(
            types.InlineKeyboardButton('➕ ارسال پاسخ تکمیلی', callback_data=f'sup_adm_followup_{ticket_id}_{ticket_msg_id}'),
            types.InlineKeyboardButton('🔒 بستن تیکت', callback_data=f'sup_adm_close_{ticket_id}_{ticket_msg_id}')
        )
    else:
        markup.add(
            types.InlineKeyboardButton('✍️ پاسخ ادمین', callback_data=f'sup_adm_reply_{ticket_msg_id}')
        )
        markup.add(
            types.InlineKeyboardButton('🔒 بستن تیکت', callback_data=f'sup_adm_close_{ticket_id}_{ticket_msg_id}')
        )

    markup.add(
        types.InlineKeyboardButton(f'📜 تاریخچه (#T{ticket_id})', callback_data=f'sup_adm_hist_{ticket_id}'),
        types.InlineKeyboardButton('👤 پنل کاربر (CRM)', callback_data=f'sup_adm_crm_{target_user_id}')
    )
    return markup


def _sync_admin_ticket_cards(ticket_msg_id):
    """بروزرسانی لحظه‌ای دکمه‌های شیشه‌ای کارت تیکت نزد تمامی ادمین‌ها."""
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute(
            '''SELECT m.ticket_id, m.sender_id, m.status, m.answered_by_name, t.status, t.user_id
               FROM support_ticket_messages m
               JOIN support_tickets t ON m.ticket_id = t.ticket_id
               WHERE m.id = ?''',
            (ticket_msg_id,)
        )
        row = cur.fetchone()
        if not row:
            return
        ticket_id, _sender_id, msg_status, answered_by_name, ticket_status, target_user_id = row
        cur.execute(
            'SELECT admin_id, telegram_message_id FROM support_admin_notifications WHERE ticket_msg_id = ?',
            (ticket_msg_id,)
        )
        notifs = cur.fetchall()

    new_markup = _build_admin_ticket_markup(
        ticket_id=ticket_id,
        ticket_msg_id=ticket_msg_id,
        target_user_id=target_user_id,
        msg_status=msg_status,
        ticket_status=ticket_status,
        answered_by_name=answered_by_name
    )
    for adm_id, tg_msg_id in notifs:
        try:
            bot.edit_message_reply_markup(
                chat_id=adm_id,
                message_id=tg_msg_id,
                reply_markup=new_markup
            )
        except Exception:
            pass


def _render_user_support_menu(user_id):
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute(
            '''SELECT ticket_id, category, status, updated_at
               FROM support_tickets
               WHERE user_id = ? AND status IN ('open', 'answered')
               ORDER BY ticket_id DESC LIMIT 1''',
            (user_id,)
        )
        active_ticket = cur.fetchone()
        cur.execute('SELECT COUNT(*) FROM support_tickets WHERE user_id = ?', (user_id,))
        total_tickets = cur.fetchone()[0]

    markup = types.InlineKeyboardMarkup(row_width=2)
    if active_ticket:
        t_id, t_cat, t_status, t_updated = active_ticket
        cat_title = SUPPORT_CATEGORIES.get(t_cat, SUPPORT_CATEGORIES['general'])
        status_badge = _support_ticket_status_badge(t_status)
        active_section = (
            f"📌 <b>تیکت فعال شما:</b> <code>#T{t_id}</code>\n"
            f"📂 <b>موضوع:</b> {cat_title}\n"
            f"📡 <b>وضعیت:</b> {status_badge}\n"
            f"🕒 <b>آخرین بروزرسانی:</b> <code>{t_updated}</code>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"💡 <i>می‌توانید در تیکت فعلی پیام جدید بفرستید یا اگر مشکلتان حل شده آن را ببندید:</i>"
        )
        markup.add(
            types.InlineKeyboardButton(f'💬 ارسال پیام در تیکت #T{t_id}', callback_data=f'sup_reply_ticket_{t_id}')
        )
        markup.add(
            types.InlineKeyboardButton('📜 مشاهده گفتگوهای تیکت', callback_data=f'sup_user_hist_{t_id}'),
            types.InlineKeyboardButton('✅ بستن تیکت (حل شد)', callback_data=f'sup_user_close_{t_id}')
        )
    else:
        active_section = (
            "💬 <b>در حال حاضر تیکت بازی ندارید.</b>\n"
            "برای ارتباط مستقیم با مدیریت و پشتیبانی، موضوع پیام خود را از دکمه‌های زیر انتخاب کنید تا تیکت شما ثبت شود 👇"
        )
        markup.add(
            types.InlineKeyboardButton('💳 خرید سکه و پرداخت‌ها', callback_data='sup_cat_payment'),
            types.InlineKeyboardButton('🐞 گزارش باگ و مشکل فنی', callback_data='sup_cat_bug')
        )
        markup.add(
            types.InlineKeyboardButton('🚨 شکایت یا پیگیری حساب', callback_data='sup_cat_report'),
            types.InlineKeyboardButton('💡 پیشنهادات و انتقادات', callback_data='sup_cat_suggestion')
        )
        markup.add(
            types.InlineKeyboardButton('❓ سوالات و پشتیبانی عمومی', callback_data='sup_cat_general')
        )

    if total_tickets > 0:
        markup.add(
            types.InlineKeyboardButton(f'🗂 تاریخچه تیکت‌های من ({total_tickets})', callback_data='sup_user_list_0')
        )

    text = (
        "🎧 <b>مرکز پشتیبانی هوشمند و تیکت دوطرفه</b>\n"
        "━━━━━━━━━━━━━━━━━━━\n"
        "⚡️ پیام شما مستقیماً به پنل ادمین‌ها ارسال می‌شود و به محض پاسخگویی، جواب را در همین ربات دریافت خواهید کرد.\n"
        "📎 امکان ارسال <b>متن، عکس، ویدیو، ویس و فایل</b> وجود دارد.\n"
        "━━━━━━━━━━━━━━━━━━━\n"
        f"{active_section}"
    )
    return text, markup


@bot.message_handler(func=lambda message: message.text == 'پشتیبانی 🎧')
def open_support_menu(message):
    user = getattr(message, 'from_user', None)
    chat = getattr(message, 'chat', None)
    user_id = getattr(user, 'id', None)
    if not isinstance(user_id, int) or user_id <= 0:
        return
    if chat is None or getattr(chat, 'type', None) != 'private' or getattr(chat, 'id', None) != user_id:
        return

    update_last_seen(user_id)
    if is_user_in_chat(user_id):
        bot.send_message(user_id, '⚠️ شما در حال چت هستید! ابتدا چت فعلی را پایان دهید.')
        return

    text, markup = _render_user_support_menu(user_id)
    bot.send_message(user_id, text, reply_markup=markup, parse_mode='HTML')


@bot.callback_query_handler(func=lambda call: bool(getattr(call, 'data', None)) and call.data == 'sup_menu')
def handle_support_menu_callback(call):
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    if not isinstance(user_id, int) or chat is None or getattr(chat, 'id', None) != user_id:
        bot.answer_callback_query(call.id)
        return

    bot.clear_step_handler_by_chat_id(user_id)
    update_last_seen(user_id)
    text, markup = _render_user_support_menu(user_id)
    try:
        bot.edit_message_text(text, chat.id, message.message_id, reply_markup=markup, parse_mode='HTML')
    except Exception:
        bot.send_message(user_id, text, reply_markup=markup, parse_mode='HTML')
    bot.answer_callback_query(call.id)


@bot.callback_query_handler(func=lambda call: bool(getattr(call, 'data', None)) and call.data == 'sup_user_cancel')
def handle_support_user_cancel(call):
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    if not isinstance(user_id, int) or chat is None or getattr(chat, 'id', None) != user_id:
        bot.answer_callback_query(call.id)
        return

    bot.clear_step_handler_by_chat_id(user_id)
    bot.answer_callback_query(call.id, '❌ ارسال پیام پشتیبانی لغو شد.')
    text, markup = _render_user_support_menu(user_id)
    try:
        bot.edit_message_text(text, chat.id, message.message_id, reply_markup=markup, parse_mode='HTML')
    except Exception:
        bot.send_message(user_id, text, reply_markup=markup, parse_mode='HTML')


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None))
    and (call.data.startswith('sup_cat_') or call.data.startswith('sup_reply_ticket_'))
)
def handle_support_start_input(call):
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    if not isinstance(user_id, int) or chat is None or getattr(chat, 'type', None) != 'private' or getattr(chat, 'id', None) != user_id:
        bot.answer_callback_query(call.id, '🚫 فقط در چت خصوصی معتبر است.', show_alert=True)
        return

    if is_user_in_chat(user_id):
        bot.answer_callback_query(call.id, '⚠️ ابتدا چت ناشناس فعلی خود را پایان دهید.', show_alert=True)
        return

    data = call.data
    ticket_id = None
    category = 'general'

    if data.startswith('sup_cat_'):
        category = data[len('sup_cat_'):]
        if category not in SUPPORT_CATEGORIES:
            category = 'general'
    else:
        try:
            ticket_id = int(data[len('sup_reply_ticket_'):])
        except (TypeError, ValueError):
            bot.answer_callback_query(call.id, '🚫 شماره تیکت نامعتبر است.', show_alert=True)
            return

        with get_db_connection() as conn:
            row = conn.execute(
                'SELECT category, status FROM support_tickets WHERE ticket_id = ? AND user_id = ?',
                (ticket_id, user_id)
            ).fetchone()
        if not row:
            bot.answer_callback_query(call.id, '❌ این تیکت یافت نشد.', show_alert=True)
            return
        if row[1] == 'closed':
            bot.answer_callback_query(
                call.id,
                '🔒 این تیکت بسته شده است. لطفاً از منوی پشتیبانی یک تیکت جدید باز کنید.',
                show_alert=True
            )
            return
        category = row[0] or 'general'

    cat_title = SUPPORT_CATEGORIES.get(category, SUPPORT_CATEGORIES['general'])
    cancel_markup = types.InlineKeyboardMarkup()
    cancel_markup.add(types.InlineKeyboardButton('❌ انصراف و بازگشت', callback_data='sup_user_cancel'))

    header_line = f"🎫 <b>ارسال پیام در تیکت #T{ticket_id}</b>" if ticket_id else "🎫 <b>ثبت تیکت پشتیبانی جدید</b>"
    prompt_text = (
        f"{header_line}\n"
        f"📂 <b>دسته:</b> {cat_title}\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
        f"✍️ لطفاً پیام، سوال یا مشکل خود را با جزئیات کامل بنویسید.\n"
        f"📎 <i>می‌توانید متن، عکس (اسکرین‌شات)، ویدیو، ویس یا فایل ارسال کنید:</i>"
    )

    bot.clear_step_handler_by_chat_id(user_id)
    bot.answer_callback_query(call.id)
    try:
        if getattr(message, 'content_type', 'text') == 'text' and data.startswith('sup_cat_'):
            bot.edit_message_text(prompt_text, chat.id, message.message_id, reply_markup=cancel_markup, parse_mode='HTML')
            prompt_msg = message
        else:
            prompt_msg = bot.send_message(user_id, prompt_text, reply_markup=cancel_markup, parse_mode='HTML')
    except Exception:
        prompt_msg = bot.send_message(user_id, prompt_text, reply_markup=cancel_markup, parse_mode='HTML')

    bot.register_next_step_handler(
        prompt_msg,
        process_user_support_message,
        category,
        ticket_id,
        getattr(prompt_msg, 'message_id', None)
    )


def _broadcast_ticket_to_admins(ticket_id, ticket_msg_id, user_id, category, message):
    """ارسال کارت تیکت به تمام ادمین‌ها همراه با ذخیره message_id برای همگام‌سازی دوطرفه."""
    user_row = get_user(user_id)
    display_name = escape_html((user_row[4] if user_row else None) or getattr(message.from_user, 'first_name', None) or 'کاربر')
    username = (user_row[1] if user_row else None) or getattr(message.from_user, 'username', None)
    username_str = f"@{escape_html(username)}" if username else "ندارد"
    coins = user_row[16] if user_row and len(user_row) > 16 and user_row[16] is not None else 0
    level = user_row[17] if user_row and len(user_row) > 17 and user_row[17] is not None else 0
    prof_id = get_or_create_profile_id(user_id)
    cat_title = SUPPORT_CATEGORIES.get(category, SUPPORT_CATEGORIES['general'])
    now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

    ctype = getattr(message, 'content_type', 'text') or 'text'
    raw_text = (getattr(message, 'text', None) if ctype == 'text' else getattr(message, 'caption', None)) or ''

    header = (
        f"🎧 <b>تیکت پشتیبانی جدید</b> | <code>#T{ticket_id}</code> (پیام <code>#M{ticket_msg_id}</code>)\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
        f"📂 <b>موضوع:</b> {cat_title}\n"
        f"👤 <b>کاربر:</b> {display_name} ({username_str})\n"
        f"🆔 <b>آیدی عددی:</b> <code>{user_id}</code> | /user_{prof_id}\n"
        f"💰 <b>سکه:</b> <code>{coins}</code> | 🏅 <b>سطح:</b> <code>Level {level}</code>\n"
        f"🕒 <b>زمان:</b> <code>{now_str}</code>\n"
        f"📡 <b>وضعیت:</b> 🟡 در انتظار پاسخ ادمین\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
    )

    markup = _build_admin_ticket_markup(
        ticket_id=ticket_id,
        ticket_msg_id=ticket_msg_id,
        target_user_id=user_id,
        msg_status='pending',
        ticket_status='open'
    )

    notif_rows = []
    for admin_id in ADMINS:
        try:
            if ctype == 'text':
                body = f"💬 <b>متن پیام کاربر:</b>\n{escape_html(raw_text[:3000])}"
                sent = bot.send_message(admin_id, header + body, reply_markup=markup, parse_mode='HTML')
                notif_rows.append((ticket_msg_id, admin_id, sent.message_id, 0))
            elif ctype in SUPPORT_CAPTION_CONTENT_TYPES:
                cap_body = f"💬 <b>توضیحات:</b>\n{escape_html(raw_text[:550])}" if raw_text else "📎 <i>فایل/مدیای پیوست‌شده توسط کاربر</i>"
                sent = bot.copy_message(
                    chat_id=admin_id,
                    from_chat_id=user_id,
                    message_id=message.message_id,
                    caption=header + cap_body,
                    parse_mode='HTML',
                    reply_markup=markup
                )
                notif_rows.append((ticket_msg_id, admin_id, sent.message_id, 1))
            else:
                bot.copy_message(chat_id=admin_id, from_chat_id=user_id, message_id=message.message_id)
                sent = bot.send_message(
                    admin_id,
                    header + "👆 <i>مدیای بالا مربوط به این تیکت است.</i>",
                    reply_markup=markup,
                    parse_mode='HTML'
                )
                notif_rows.append((ticket_msg_id, admin_id, sent.message_id, 0))
        except Exception:
            logger.warning('Could not send support ticket #%s to admin=%s', ticket_id, admin_id, exc_info=True)

    if notif_rows:
        with get_db_connection() as conn:
            conn.executemany(
                '''INSERT OR REPLACE INTO support_admin_notifications
                   (ticket_msg_id, admin_id, telegram_message_id, has_caption)
                   VALUES (?, ?, ?, ?)''',
                notif_rows
            )
            conn.commit()


def process_user_support_message(message, category='general', ticket_id=None, prompt_msg_id=None):
    user = getattr(message, 'from_user', None)
    chat = getattr(message, 'chat', None)
    user_id = getattr(user, 'id', None)
    if not isinstance(user_id, int) or chat is None or getattr(chat, 'id', None) != user_id:
        return

    update_last_seen(user_id)
    ctype = getattr(message, 'content_type', None)
    text_val = (getattr(message, 'text', None) or '').strip()

    if ctype == 'text' and (
        text_val in ('/cancel', '/start', 'لغو', 'انصراف', 'بازگشت')
        or text_val in MAIN_MENU_BUTTON_LABELS
    ):
        bot.send_message(user_id, '❌ ثبت پیام پشتیبانی لغو شد.')
        if text_val not in MAIN_MENU_BUTTON_LABELS:
            show_main_menu(user_id, user_id in ADMINS)
        return

    if ctype not in SUPPORT_ALLOWED_CONTENT_TYPES:
        bot.send_message(
            user_id,
            '🚫 فرمت ارسالی پشتیبانی نمی‌شود. لطفاً متن، عکس، ویدیو، ویس یا فایل ارسال کنید.'
        )
        return

    now_ts = time.time()
    with _support_rate_lock:
        last_sent = _support_user_last_sent.get(user_id, 0.0)
        if now_ts - last_sent < SUPPORT_USER_COOLDOWN_SECONDS:
            wait_s = max(1, int(SUPPORT_USER_COOLDOWN_SECONDS - (now_ts - last_sent)))
            bot.send_message(user_id, f'⏳ لطفاً {wait_s} ثانیه صبر کنید و سپس پیام بعدی را بفرستید.')
            return

    now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    preview = _extract_support_preview(message)

    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')

        if ticket_id is not None:
            cur.execute(
                'SELECT ticket_id, category, status FROM support_tickets WHERE ticket_id = ? AND user_id = ?',
                (ticket_id, user_id)
            )
            t_row = cur.fetchone()
            if not t_row or t_row[2] == 'closed':
                ticket_id = None
            else:
                category = t_row[1] or category

        if ticket_id is None:
            cur.execute(
                '''SELECT ticket_id, category FROM support_tickets
                   WHERE user_id = ? AND status IN ('open', 'answered')
                   ORDER BY ticket_id DESC LIMIT 1''',
                (user_id,)
            )
            existing_open = cur.fetchone()
            if existing_open:
                ticket_id, existing_cat = existing_open
                if category == 'general' and existing_cat:
                    category = existing_cat

        if ticket_id is not None:
            cur.execute(
                '''SELECT COUNT(*) FROM support_ticket_messages
                   WHERE ticket_id = ? AND sender_role = 'user' AND status = 'pending' ''',
                (ticket_id,)
            )
            pending_count = cur.fetchone()[0]
            if pending_count >= SUPPORT_MAX_PENDING_PER_TICKET:
                conn.rollback()
                bot.send_message(
                    user_id,
                    f'⚠️ شما در تیکت <code>#T{ticket_id}</code> دارای {pending_count} پیام در انتظار بررسی هستید.\n'
                    'لطفاً تا دریافت پاسخ ادمین شکیبا باشید 🙏',
                    parse_mode='HTML'
                )
                return

            cur.execute(
                '''UPDATE support_tickets
                   SET status = 'open', category = ?, updated_at = ?
                   WHERE ticket_id = ?''',
                (category, now_str, ticket_id)
            )
        else:
            cur.execute(
                '''INSERT INTO support_tickets (user_id, category, status, created_at, updated_at)
                   VALUES (?, ?, 'open', ?, ?)''',
                (user_id, category, now_str, now_str)
            )
            ticket_id = cur.lastrowid

        cur.execute(
            '''INSERT INTO support_ticket_messages
               (ticket_id, sender_id, sender_role, content_type, text_preview, status, created_at)
               VALUES (?, ?, 'user', ?, ?, 'pending', ?)''',
            (ticket_id, user_id, ctype, preview, now_str)
        )
        ticket_msg_id = cur.lastrowid
        conn.commit()

    with _support_rate_lock:
        _support_user_last_sent[user_id] = now_ts

    if prompt_msg_id:
        try:
            bot.edit_message_reply_markup(user_id, prompt_msg_id, reply_markup=None)
        except Exception:
            pass

    _broadcast_ticket_to_admins(ticket_id, ticket_msg_id, user_id, category, message)

    ack_markup = types.InlineKeyboardMarkup(row_width=2)
    ack_markup.add(
        types.InlineKeyboardButton(f'📜 مشاهده تیکت #T{ticket_id}', callback_data=f'sup_user_hist_{ticket_id}'),
        types.InlineKeyboardButton('🎧 منوی پشتیبانی', callback_data='sup_menu')
    )
    bot.send_message(
        user_id,
        f"✅ <b>پیام شما در تیکت <code>#T{ticket_id}</code> ثبت و برای ادمین‌ها ارسال شد!</b>\n\n"
        f"📂 <b>موضوع:</b> {SUPPORT_CATEGORIES.get(category, SUPPORT_CATEGORIES['general'])}\n"
        f"📡 <b>وضعیت:</b> 🟡 در انتظار پاسخ ادمین\n\n"
        f"🔔 <i>به محض پاسخگویی پشتیبانی، پیام ادمین مستقیماً برای شما ارسال خواهد شد.</i>",
        reply_markup=ack_markup,
        parse_mode='HTML'
    )


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('sup_user_hist_')
)
def handle_support_user_history(call):
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    if not isinstance(user_id, int) or chat is None or getattr(chat, 'id', None) != user_id:
        bot.answer_callback_query(call.id)
        return

    try:
        ticket_id = int(call.data[len('sup_user_hist_'):])
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 شماره تیکت نامعتبر است.', show_alert=True)
        return

    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute(
            'SELECT category, status, created_at, updated_at FROM support_tickets WHERE ticket_id = ? AND user_id = ?',
            (ticket_id, user_id)
        )
        t_row = cur.fetchone()
        if not t_row:
            bot.answer_callback_query(call.id, '❌ تیکت یافت نشد.', show_alert=True)
            return

        cur.execute(
            '''SELECT sender_role, text_preview, created_at, answered_by_name
               FROM support_ticket_messages
               WHERE ticket_id = ?
               ORDER BY id DESC LIMIT 12''',
            (ticket_id,)
        )
        msgs = list(reversed(cur.fetchall()))

    category, status, _created_at, updated_at = t_row
    cat_title = SUPPORT_CATEGORIES.get(category, SUPPORT_CATEGORIES['general'])
    status_badge = _support_ticket_status_badge(status)

    lines = [
        f"📜 <b>تاریخچه تیکت <code>#T{ticket_id}</code></b>",
        f"📂 <b>موضوع:</b> {cat_title}",
        f"📡 <b>وضعیت:</b> {status_badge}",
        f"🕒 <b>آخرین بروزرسانی:</b> <code>{updated_at}</code>",
        "━━━━━━━━━━━━━━━━━━━"
    ]
    if not msgs:
        lines.append("<i>هیچ پیامی ثبت نشده است.</i>")
    else:
        for role, preview, c_at, adm_name in msgs:
            time_short = (c_at or '')[5:16]
            if role == 'user':
                lines.append(f"👤 <b>شما</b> (<code>{time_short}</code>):\n└ {escape_html(preview or '')}\n")
            else:
                lines.append(f"🎧 <b>پشتیبانی</b> (<code>{time_short}</code>):\n└ {escape_html(preview or '')}\n")

    markup = types.InlineKeyboardMarkup(row_width=2)
    if status != 'closed':
        markup.add(
            types.InlineKeyboardButton('💬 ارسال پیام جدید', callback_data=f'sup_reply_ticket_{ticket_id}'),
            types.InlineKeyboardButton('✅ بستن تیکت', callback_data=f'sup_user_close_{ticket_id}')
        )
    markup.add(types.InlineKeyboardButton('🔙 بازگشت به پشتیبانی', callback_data='sup_menu'))

    text = "\n".join(lines)
    try:
        bot.edit_message_text(text, chat.id, message.message_id, reply_markup=markup, parse_mode='HTML')
    except Exception:
        bot.send_message(user_id, text, reply_markup=markup, parse_mode='HTML')
    bot.answer_callback_query(call.id)


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('sup_user_list_')
)
def handle_support_user_list(call):
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    if not isinstance(user_id, int) or chat is None or getattr(chat, 'id', None) != user_id:
        bot.answer_callback_query(call.id)
        return

    with get_db_connection() as conn:
        rows = conn.execute(
            '''SELECT ticket_id, category, status, updated_at
               FROM support_tickets
               WHERE user_id = ?
               ORDER BY ticket_id DESC LIMIT 10''',
            (user_id,)
        ).fetchall()

    markup = types.InlineKeyboardMarkup(row_width=1)
    if not rows:
        text = "🗂 شما هنوز هیچ تیکتی ثبت نکرده‌اید."
    else:
        text = "🗂 <b>۱۰ تیکت اخیر شما:</b>\nبرای مشاهده جزئیات هر تیکت روی آن بزنید 👇"
        for t_id, cat, st, upd in rows:
            icon = '🟡' if st == 'open' else ('🟢' if st == 'answered' else '🔒')
            cat_short = SUPPORT_CATEGORIES.get(cat, 'عمومی').split()[0]
            markup.add(
                types.InlineKeyboardButton(
                    f"{icon} تیکت #T{t_id} | {cat_short} ({upd[:10]})",
                    callback_data=f"sup_user_hist_{t_id}"
                )
            )
    markup.add(types.InlineKeyboardButton('🔙 بازگشت به منوی پشتیبانی', callback_data='sup_menu'))

    try:
        bot.edit_message_text(text, chat.id, message.message_id, reply_markup=markup, parse_mode='HTML')
    except Exception:
        bot.send_message(user_id, text, reply_markup=markup, parse_mode='HTML')
    bot.answer_callback_query(call.id)


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('sup_user_close_')
)
def handle_support_user_close(call):
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message else None
    if not isinstance(user_id, int) or chat is None or getattr(chat, 'id', None) != user_id:
        bot.answer_callback_query(call.id)
        return

    try:
        ticket_id = int(call.data[len('sup_user_close_'):])
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 شماره تیکت نامعتبر است.', show_alert=True)
        return

    now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    msg_ids_to_sync = []
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')
        cur.execute(
            'SELECT status FROM support_tickets WHERE ticket_id = ? AND user_id = ?',
            (ticket_id, user_id)
        )
        row = cur.fetchone()
        if not row:
            conn.rollback()
            bot.answer_callback_query(call.id, '❌ تیکت یافت نشد.', show_alert=True)
            return
        if row[0] == 'closed':
            conn.rollback()
            bot.answer_callback_query(call.id, 'ℹ️ این تیکت قبلاً بسته شده است.', show_alert=True)
            return

        cur.execute(
            '''UPDATE support_tickets
               SET status = 'closed', updated_at = ?, closed_by = ?
               WHERE ticket_id = ?''',
            (now_str, user_id, ticket_id)
        )
        cur.execute(
            '''SELECT id FROM support_ticket_messages
               WHERE ticket_id = ? AND sender_role = 'user' ''',
            (ticket_id,)
        )
        msg_ids_to_sync = [r[0] for r in cur.fetchall()]
        cur.execute(
            '''UPDATE support_ticket_messages
               SET status = CASE WHEN status = 'pending' THEN 'closed' ELSE status END,
                   locked_by_admin_id = NULL, locked_at = 0
               WHERE ticket_id = ?''',
            (ticket_id,)
        )
        conn.commit()

    for mid in msg_ids_to_sync:
        _sync_admin_ticket_cards(mid)

    bot.answer_callback_query(call.id, f'✅ تیکت #T{ticket_id} با موفقیت بسته شد.', show_alert=True)
    text, markup = _render_user_support_menu(user_id)
    try:
        bot.edit_message_text(text, chat.id, message.message_id, reply_markup=markup, parse_mode='HTML')
    except Exception:
        bot.send_message(user_id, text, reply_markup=markup, parse_mode='HTML')


# ----------------- هندلرهای ادمین برای پاسخگویی، بررسی وضعیت و مدیریت تیکت‌ها -----------------

@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('sup_adm_info_')
)
@admin_only
def handle_support_admin_info(call):
    """نمایش اطلاعات کامل ادمین پاسخ‌دهنده در صورت کلیک ادمین دوم روی تیکت پاسخ‌داده‌شده یا بسته‌شده."""
    try:
        ticket_msg_id = int(call.data[len('sup_adm_info_'):])
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 شناسه نامعتبر است.', show_alert=True)
        return

    with get_db_connection() as conn:
        row = conn.execute(
            '''SELECT m.ticket_id, m.status, m.answered_by_admin_id, m.answered_by_name,
                      m.answered_at, m.reply_preview, t.status, t.closed_by
               FROM support_ticket_messages m
               JOIN support_tickets t ON m.ticket_id = t.ticket_id
               WHERE m.id = ?''',
            (ticket_msg_id,)
        ).fetchone()

    if not row:
        bot.answer_callback_query(call.id, '❌ اطلاعات این پیام یافت نشد.', show_alert=True)
        return

    ticket_id, msg_status, ans_admin_id, ans_name, ans_at, reply_prev, ticket_status, closed_by = row
    if msg_status == 'answered':
        preview_short = (reply_prev or 'ثبت شده')[:90]
        alert_text = (
            f"✅ این پیام قبلاً پاسخ داده شده است!\n"
            f"👨‍💻 ادمین: {ans_name or 'ادمین'} ({ans_admin_id or '-'})\n"
            f"🕒 زمان: {ans_at or '-'}\n"
            f"💬 خلاصه پاسخ: {preview_short}"
        )
        bot.answer_callback_query(call.id, alert_text[:200], show_alert=True)
    elif ticket_status == 'closed' or msg_status == 'closed':
        bot.answer_callback_query(
            call.id,
            f"🔒 تیکت #T{ticket_id} بسته شده است (توسط: {closed_by or 'کاربر/ادمین'}).",
            show_alert=True
        )
    else:
        bot.answer_callback_query(call.id, '🟡 این پیام هنوز در انتظار پاسخ است.', show_alert=True)


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None))
    and (call.data.startswith('sup_adm_reply_') or call.data.startswith('sup_adm_followup_'))
)
@admin_only
def handle_support_admin_reply_start(call):
    """شروع پاسخ ادمین با بررسی اتمیک اینکه آیا ادمین دیگری قبلاً پاسخ داده یا در حال نوشتن پاسخ است."""
    admin_id = call.from_user.id
    data = call.data
    is_followup = data.startswith('sup_adm_followup_')

    try:
        if is_followup:
            parts = data.split('_')
            ticket_id_arg = int(parts[3])
            ticket_msg_id = int(parts[4])
        else:
            ticket_msg_id = int(data[len('sup_adm_reply_'):])
            ticket_id_arg = None
    except (IndexError, TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 دکمه نامعتبر است.', show_alert=True)
        return

    now_ts = time.time()
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')
        cur.execute(
            '''SELECT m.ticket_id, m.sender_id, m.text_preview, m.status,
                      m.locked_by_admin_id, COALESCE(m.locked_at, 0),
                      m.answered_by_admin_id, m.answered_by_name, m.answered_at, m.reply_preview,
                      t.status, t.user_id
               FROM support_ticket_messages m
               JOIN support_tickets t ON m.ticket_id = t.ticket_id
               WHERE m.id = ?''',
            (ticket_msg_id,)
        )
        row = cur.fetchone()
        if not row:
            conn.rollback()
            bot.answer_callback_query(call.id, '❌ پیام تیکت در دیتابیس یافت نشد.', show_alert=True)
            return

        (
            ticket_id, _sender_id, user_preview, msg_status,
            locked_by, locked_at,
            ans_admin_id, ans_name, ans_at, reply_prev,
            ticket_status, target_user_id
        ) = row

        # ۱. اگر تیکت بسته شده باشد
        if ticket_status == 'closed' or msg_status == 'closed':
            conn.rollback()
            _sync_admin_ticket_cards(ticket_msg_id)
            bot.answer_callback_query(
                call.id,
                f'🔒 تیکت #T{ticket_id} بسته شده است و امکان پاسخگویی وجود ندارد.',
                show_alert=True
            )
            return

        # ۲. بررسی کلیدی: اگر ادمین دیگری قبلاً به این پیام پاسخ داده باشد!
        if not is_followup and msg_status == 'answered':
            conn.rollback()
            _sync_admin_ticket_cards(ticket_msg_id)
            prev_short = (reply_prev or '')[:70]
            alert_msg = (
                f"⚠️ این پیام قبلاً پاسخ داده شده است!\n"
                f"👨‍💻 پاسخ‌دهنده: {ans_name or 'ادمین'} ({ans_admin_id or ''})\n"
                f"🕒 زمان: {ans_at or '-'}\n"
                f"💬 پاسخ: {prev_short}"
            )
            bot.answer_callback_query(call.id, alert_msg[:200], show_alert=True)
            return

        # ۳. بررسی قفل هم‌زمانی: آیا ادمین دیگری همین الان در حال تایپ پاسخ است؟
        if (
            locked_by is not None
            and int(locked_by) != admin_id
            and (now_ts - float(locked_at)) < SUPPORT_ADMIN_LOCK_TTL_SECONDS
        ):
            conn.rollback()
            rem_lock = max(1, int(SUPPORT_ADMIN_LOCK_TTL_SECONDS - (now_ts - float(locked_at))))
            bot.answer_callback_query(
                call.id,
                f'⏳ ادمین دیگر ({locked_by}) هم‌اکنون در حال نوشتن پاسخ برای این تیکت است!\n'
                f'(قفل محافظتی تا {rem_lock} ثانیه دیگر فعال است)',
                show_alert=True
            )
            return

        # ۴. ثبت قفل پاسخگویی برای این ادمین
        cur.execute(
            '''UPDATE support_ticket_messages
               SET locked_by_admin_id = ?, locked_at = ?
               WHERE id = ?''',
            (admin_id, now_ts, ticket_msg_id)
        )
        conn.commit()

    bot.clear_step_handler_by_chat_id(admin_id)
    bot.answer_callback_query(call.id)

    cancel_markup = types.InlineKeyboardMarkup()
    cancel_markup.add(
        types.InlineKeyboardButton('❌ انصراف از پاسخ (آزادسازی قفل)', callback_data=f'sup_adm_cancel_reply_{ticket_msg_id}')
    )

    mode_title = "ارسال پیام تکمیلی" if is_followup else "پاسخ مستقیم ادمین"
    prompt_text = (
        f"✍️ <b>{mode_title} به تیکت <code>#T{ticket_id}</code> (پیام <code>#M{ticket_msg_id}</code>)</b>\n"
        f"👤 <b>کاربر هدف:</b> <code>{target_user_id}</code>\n"
        f"💬 <b>پیام کاربر:</b> <i>{escape_html((user_preview or '')[:150])}</i>\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
        f"👇 لطفاً پاسخ خود را ارسال کنید (متن، عکس، ویدیو، ویس یا فایل):"
    )
    prompt_msg = bot.send_message(admin_id, prompt_text, reply_markup=cancel_markup, parse_mode='HTML')
    bot.register_next_step_handler(
        prompt_msg,
        process_support_admin_reply,
        ticket_msg_id,
        is_followup,
        getattr(prompt_msg, 'message_id', None)
    )


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('sup_adm_cancel_reply_')
)
@admin_only
def handle_support_admin_cancel_reply(call):
    """لغو مرحله نوشتن پاسخ توسط ادمین و آزادسازی قفل تیکت برای سایر ادمین‌ها."""
    admin_id = call.from_user.id
    try:
        ticket_msg_id = int(call.data[len('sup_adm_cancel_reply_'):])
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id)
        return

    bot.clear_step_handler_by_chat_id(admin_id)
    with get_db_connection() as conn:
        conn.execute(
            '''UPDATE support_ticket_messages
               SET locked_by_admin_id = NULL, locked_at = 0
               WHERE id = ? AND locked_by_admin_id = ?''',
            (ticket_msg_id, admin_id)
        )
        conn.commit()

    try:
        bot.delete_message(call.message.chat.id, call.message.message_id)
    except Exception:
        pass
    bot.answer_callback_query(call.id, '❌ پاسخگویی لغو شد و قفل تیکت آزاد گردید.')


@admin_only
def process_support_admin_reply(message, ticket_msg_id, is_followup=False, prompt_msg_id=None):
    """ارسال پاسخ ادمین به کاربر با بررسی مجدد اتمیک عدم پاسخ قبلی توسط ادمین دیگر."""
    admin_id = message.from_user.id
    admin_name = (getattr(message.from_user, 'first_name', None) or f'Admin {admin_id}').strip()
    ctype = getattr(message, 'content_type', None)
    text_val = (getattr(message, 'text', None) or '').strip()

    if prompt_msg_id:
        try:
            bot.delete_message(admin_id, prompt_msg_id)
        except Exception:
            pass

    if ctype == 'text' and (
        text_val in ('/cancel', '/start', 'لغو', 'انصراف', 'بازگشت')
        or text_val in MAIN_MENU_BUTTON_LABELS
    ):
        with get_db_connection() as conn:
            conn.execute(
                '''UPDATE support_ticket_messages
                   SET locked_by_admin_id = NULL, locked_at = 0
                   WHERE id = ? AND locked_by_admin_id = ?''',
                (ticket_msg_id, admin_id)
            )
            conn.commit()
        bot.send_message(admin_id, '❌ ارسال پاسخ به تیکت لغو شد.')
        return

    if ctype not in SUPPORT_ALLOWED_CONTENT_TYPES:
        with get_db_connection() as conn:
            conn.execute(
                '''UPDATE support_ticket_messages
                   SET locked_by_admin_id = NULL, locked_at = 0
                   WHERE id = ? AND locked_by_admin_id = ?''',
                (ticket_msg_id, admin_id)
            )
            conn.commit()
        bot.send_message(admin_id, '❌ فرمت پیام نامعتبر است. عملیات لغو شد.')
        return

    now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    reply_preview = _extract_support_preview(message)

    # ۱. بررسی و رزرو اتمیک وضعیت پیام تیکت قبل از ارسال به کاربر
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')
        cur.execute(
            '''SELECT m.ticket_id, m.status, m.answered_by_admin_id, m.answered_by_name,
                      m.answered_at, m.reply_preview, t.status, t.user_id
               FROM support_ticket_messages m
               JOIN support_tickets t ON m.ticket_id = t.ticket_id
               WHERE m.id = ?''',
            (ticket_msg_id,)
        )
        row = cur.fetchone()
        if not row:
            conn.rollback()
            bot.send_message(admin_id, '❌ این پیام تیکت در دیتابیس یافت نشد.')
            return

        ticket_id, msg_status, ans_admin_id, ans_name, ans_at, existing_reply_prev, ticket_status, target_user_id = row

        if ticket_status == 'closed' or msg_status == 'closed':
            cur.execute(
                'UPDATE support_ticket_messages SET locked_by_admin_id = NULL, locked_at = 0 WHERE id = ?',
                (ticket_msg_id,)
            )
            conn.commit()
            _sync_admin_ticket_cards(ticket_msg_id)
            bot.send_message(admin_id, f'🔒 تیکت <code>#T{ticket_id}</code> بسته شده است؛ پاسخ شما ارسال نشد.', parse_mode='HTML')
            return

        if not is_followup and msg_status == 'answered':
            cur.execute(
                'UPDATE support_ticket_messages SET locked_by_admin_id = NULL, locked_at = 0 WHERE id = ?',
                (ticket_msg_id,)
            )
            conn.commit()
            _sync_admin_ticket_cards(ticket_msg_id)
            bot.send_message(
                admin_id,
                f"⚠️ <b>جلوگیری از ارسال پاسخ تکراری!</b>\n"
                f"این پیام قبلاً توسط ادمین <b>{escape_html(ans_name or 'ادمین')}</b> (<code>{ans_admin_id}</code>) "
                f"در تاریخ <code>{ans_at}</code> پاسخ داده شده است.\n"
                f"💬 <b>پاسخ ثبت‌شده:</b> <i>{escape_html(existing_reply_prev or '')}</i>",
                parse_mode='HTML'
            )
            return

        conn.commit()

    # ۲. ارسال پاسخ به کاربر همراه با دکمه‌های دوطرفه (پاسخ مجدد کاربر یا بستن تیکت)
    user_markup = types.InlineKeyboardMarkup(row_width=2)
    user_markup.add(
        types.InlineKeyboardButton(f'💬 پاسخ به پشتیبانی (#T{ticket_id})', callback_data=f'sup_reply_ticket_{ticket_id}'),
        types.InlineKeyboardButton('✅ مشکلم حل شد (بستن تیکت)', callback_data=f'sup_user_close_{ticket_id}')
    )

    user_header = (
        f"🎧 <b>پاسخ جدید از پشتیبانی ربات</b>\n"
        f"🎫 <b>شماره تیکت:</b> <code>#T{ticket_id}</code>\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
    )
    raw_text = (getattr(message, 'text', None) if ctype == 'text' else getattr(message, 'caption', None)) or ''

    try:
        if ctype == 'text':
            bot.send_message(
                target_user_id,
                user_header + escape_html(raw_text[:3400]),
                reply_markup=user_markup,
                parse_mode='HTML'
            )
        elif ctype in SUPPORT_CAPTION_CONTENT_TYPES:
            cap_text = user_header + escape_html(raw_text[:700])
            bot.copy_message(
                chat_id=target_user_id,
                from_chat_id=admin_id,
                message_id=message.message_id,
                caption=cap_text,
                parse_mode='HTML',
                reply_markup=user_markup
            )
        else:
            bot.send_message(target_user_id, user_header + "👇 <i>پیام پشتیبانی:</i>", parse_mode='HTML')
            bot.copy_message(
                chat_id=target_user_id,
                from_chat_id=admin_id,
                message_id=message.message_id,
                reply_markup=user_markup
            )
    except Exception:
        logger.warning('Failed to deliver support reply to user=%s ticket=%s', target_user_id, ticket_id, exc_info=True)
        with get_db_connection() as conn:
            conn.execute(
                'UPDATE support_ticket_messages SET locked_by_admin_id = NULL, locked_at = 0 WHERE id = ?',
                (ticket_msg_id,)
            )
            conn.commit()
        bot.send_message(
            admin_id,
            f"❌ ارسال پاسخ به کاربر <code>{target_user_id}</code> ممکن نشد (احتمالاً کاربر ربات را مسدود کرده است).",
            parse_mode='HTML'
        )
        return

    # ۳. ثبت نهایی پاسخ ادمین در دیتابیس و علامت‌گذاری تمام پیام‌های pending این تیکت به‌عنوان پاسخ‌داده‌شده
    resolved_msg_ids = []
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')
        cur.execute(
            '''SELECT id FROM support_ticket_messages
               WHERE ticket_id = ? AND sender_role = 'user' AND (status = 'pending' OR id = ?)''',
            (ticket_id, ticket_msg_id)
        )
        resolved_msg_ids = [r[0] for r in cur.fetchall()]

        cur.execute(
            '''UPDATE support_ticket_messages
               SET status = 'answered',
                   locked_by_admin_id = NULL,
                   locked_at = 0,
                   answered_by_admin_id = COALESCE(answered_by_admin_id, ?),
                   answered_by_name = COALESCE(answered_by_name, ?),
                   answered_at = COALESCE(answered_at, ?),
                   reply_preview = COALESCE(reply_preview, ?)
               WHERE ticket_id = ? AND sender_role = 'user' AND (status = 'pending' OR id = ?)''',
            (admin_id, admin_name, now_str, reply_preview, ticket_id, ticket_msg_id)
        )

        cur.execute(
            '''INSERT INTO support_ticket_messages
               (ticket_id, sender_id, sender_role, content_type, text_preview, status,
                answered_by_admin_id, answered_by_name, answered_at, created_at)
               VALUES (?, ?, 'admin', ?, ?, 'answered', ?, ?, ?, ?)''',
            (ticket_id, admin_id, ctype, reply_preview, admin_id, admin_name, now_str, now_str)
        )

        cur.execute(
            '''UPDATE support_tickets
               SET status = 'answered', updated_at = ?
               WHERE ticket_id = ?''',
            (now_str, ticket_id)
        )
        conn.commit()

    # ۴. همگام‌سازی فوری دکمه‌های کارت تیکت نزد تمامی ادمین‌ها
    for mid in resolved_msg_ids:
        _sync_admin_ticket_cards(mid)

    bot.send_message(
        admin_id,
        f"✅ <b>پاسخ شما با موفقیت برای کاربر (<code>{target_user_id}</code>) در تیکت <code>#T{ticket_id}</code> ارسال شد!</b>\n"
        f"📡 وضعیت تیکت نزد سایر ادمین‌ها نیز به «پاسخ داده شده توسط {escape_html(admin_name)}» بروزرسانی گردید.",
        parse_mode='HTML'
    )


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('sup_adm_close_')
)
@admin_only
def handle_support_admin_close(call):
    """بستن تیکت توسط ادمین با همگام‌سازی کارت‌ها و اطلاع‌رسانی به کاربر."""
    admin_id = call.from_user.id
    admin_name = (getattr(call.from_user, 'first_name', None) or f'Admin {admin_id}').strip()
    parts = (call.data or '').split('_')
    try:
        ticket_id = int(parts[3])
    except (IndexError, TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 دکمه نامعتبر است.', show_alert=True)
        return

    now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    msg_ids_to_sync = []
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE')
        cur.execute('SELECT user_id, status FROM support_tickets WHERE ticket_id = ?', (ticket_id,))
        t_row = cur.fetchone()
        if not t_row:
            conn.rollback()
            bot.answer_callback_query(call.id, '❌ تیکت یافت نشد.', show_alert=True)
            return

        target_user_id, current_status = t_row
        if current_status == 'closed':
            conn.rollback()
            bot.answer_callback_query(call.id, '🔒 این تیکت قبلاً بسته شده است.', show_alert=True)
            return

        cur.execute(
            '''UPDATE support_tickets
               SET status = 'closed', updated_at = ?, closed_by = ?
               WHERE ticket_id = ?''',
            (now_str, admin_id, ticket_id)
        )
        cur.execute(
            '''SELECT id FROM support_ticket_messages
               WHERE ticket_id = ? AND sender_role = 'user' ''',
            (ticket_id,)
        )
        msg_ids_to_sync = [r[0] for r in cur.fetchall()]
        cur.execute(
            '''UPDATE support_ticket_messages
               SET status = CASE WHEN status = 'pending' THEN 'closed' ELSE status END,
                   locked_by_admin_id = NULL, locked_at = 0
               WHERE ticket_id = ?''',
            (ticket_id,)
        )
        conn.commit()

    for mid in msg_ids_to_sync:
        _sync_admin_ticket_cards(mid)

    try:
        bot.send_message(
            target_user_id,
            f"🔒 <b>تیکت پشتیبانی <code>#T{ticket_id}</code> توسط پشتیبانی بسته شد.</b>\n"
            "در صورت داشتن سوال جدید، می‌توانید از بخش «پشتیبانی 🎧» تیکت جدیدی ثبت کنید.",
            parse_mode='HTML'
        )
    except Exception:
        pass

    bot.answer_callback_query(call.id, f'🔒 تیکت #T{ticket_id} توسط {admin_name} بسته شد.', show_alert=True)


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('sup_adm_hist_')
)
@admin_only
def handle_support_admin_history(call):
    """مشاهده تاریخچه کامل مکالمات یک تیکت توسط ادمین."""
    admin_id = call.from_user.id
    try:
        ticket_id = int(call.data[len('sup_adm_hist_'):])
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 شماره تیکت نامعتبر است.', show_alert=True)
        return

    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute(
            'SELECT user_id, category, status, created_at, updated_at FROM support_tickets WHERE ticket_id = ?',
            (ticket_id,)
        )
        t_row = cur.fetchone()
        if not t_row:
            bot.answer_callback_query(call.id, '❌ تیکت یافت نشد.', show_alert=True)
            return

        target_user_id, category, status, _created_at, updated_at = t_row
        cur.execute(
            '''SELECT id, sender_role, text_preview, status, answered_by_name, created_at
               FROM support_ticket_messages
               WHERE ticket_id = ?
               ORDER BY id DESC LIMIT 15''',
            (ticket_id,)
        )
        msgs = list(reversed(cur.fetchall()))

    user_row = get_user(target_user_id)
    dname = escape_html((user_row[4] if user_row else None) or 'کاربر')
    cat_title = SUPPORT_CATEGORIES.get(category, SUPPORT_CATEGORIES['general'])
    status_badge = _support_ticket_status_badge(status)

    lines = [
        f"📜 <b>پرونده کامل تیکت <code>#T{ticket_id}</code></b>",
        f"👤 <b>کاربر:</b> {dname} (<code>{target_user_id}</code>)",
        f"📂 <b>موضوع:</b> {cat_title}",
        f"📡 <b>وضعیت:</b> {status_badge} | 🕒 <code>{updated_at}</code>",
        "━━━━━━━━━━━━━━━━━━━"
    ]
    last_user_msg_id = None
    last_user_msg_status = 'pending'
    last_ans_name = None

    for mid, role, preview, m_st, adm_name, c_at in msgs:
        t_short = (c_at or '')[5:16]
        if role == 'user':
            last_user_msg_id = mid
            last_user_msg_status = m_st
            last_ans_name = adm_name
            st_icon = '🟢' if m_st == 'answered' else ('🟡' if m_st == 'pending' else '🔒')
            lines.append(f"{st_icon} <b>کاربر [#M{mid}]</b> (<code>{t_short}</code>):\n└ {escape_html(preview or '')}\n")
        else:
            lines.append(f"👨‍💻 <b>ادمین ({escape_html(adm_name or 'پشتیبانی')})</b> (<code>{t_short}</code>):\n└ {escape_html(preview or '')}\n")

    markup = types.InlineKeyboardMarkup(row_width=2)
    if status != 'closed' and last_user_msg_id is not None:
        if last_user_msg_status == 'pending':
            markup.add(
                types.InlineKeyboardButton(f'✍️ پاسخ ادمین (#M{last_user_msg_id})', callback_data=f'sup_adm_reply_{last_user_msg_id}')
            )
        else:
            markup.add(
                types.InlineKeyboardButton(f'✅ پاسخ داده شده ({last_ans_name or "ادمین"})', callback_data=f'sup_adm_info_{last_user_msg_id}'),
                types.InlineKeyboardButton('➕ ارسال پیام تکمیلی', callback_data=f'sup_adm_followup_{ticket_id}_{last_user_msg_id}')
            )
        markup.add(
            types.InlineKeyboardButton('🔒 بستن تیکت', callback_data=f'sup_adm_close_{ticket_id}_{last_user_msg_id}'),
            types.InlineKeyboardButton('👤 پنل کاربر (CRM)', callback_data=f'sup_adm_crm_{target_user_id}')
        )
    else:
        markup.add(
            types.InlineKeyboardButton('👤 پنل کاربر (CRM)', callback_data=f'sup_adm_crm_{target_user_id}')
        )
    markup.add(types.InlineKeyboardButton('🎧 لیست تیکت‌های پنل مدیریت', callback_data='admin_tickets'))

    bot.answer_callback_query(call.id)
    bot.send_message(admin_id, "\n".join(lines), reply_markup=markup, parse_mode='HTML')


@bot.callback_query_handler(
    func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('sup_adm_crm_')
)
@admin_only
def handle_support_admin_crm_shortcut(call):
    """ورود مستقیم و یک‌کلیکی از روی کارت تیکت به پنل مدیریت کاربر (CRM)."""
    admin_id = call.from_user.id
    try:
        target_id = int(call.data[len('sup_adm_crm_'):])
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 آیدی کاربر نامعتبر است.', show_alert=True)
        return

    bot.answer_callback_query(call.id)
    show_user_management_panel(admin_id, target_id)

# ============================================================================
# 🎮 سیستم پیشرفته بازی گروهی و بلایند دیت هوشمند (Blind Ship / Blind Date)
# با الگوریتم امتیازدهی تطابق گرایش + پوزیشن (Vers تطبیق‌پذیر با همه)،
# اولویت‌بندی MRV، بهینه‌سازی 2-Opt، جلوگیری از تکرار جفت‌های اخیر و امنیت کامل
# ============================================================================

BLIND_DATE_LOBBY_SECONDS = 180
BLIND_DATE_CHAT_SECONDS = 900
BLIND_DATE_WARNING_SECONDS = 840
BLIND_DATE_REVEAL_SECONDS = 90
_group_lobby_edit_lock = threading.Lock()
_group_lobby_last_edit = {}
_group_lobby_locks = [threading.Lock() for _ in range(64)]


def _is_lobby_message_deleted_error(exc):
    """تشخیص اینکه آیا خطای تلگرام به معنی پاک شدن پیام لابی در گروه است یا خیر."""
    desc = (getattr(exc, 'description', None) or str(exc) or '').lower()
    deleted_markers = (
        'message to edit not found',
        'message not found',
        'message_id_invalid',
        "message can't be edited",
        'message to be replied not found',
        'message to reply not found',
        'replied message not found',
        'chat not found',
    )
    return any(marker in desc for marker in deleted_markers)


def _cancel_group_lobby(group_id, expected_game=None, expected_msg_id=None):
    """لغو کامل لابی گروه، توقف تایمر و پاک‌سازی وضعیت در صورت حذف پیام لابی یا لغو دستی."""
    removed_game = None
    with queue_lock:
        current = active_group_games.get(group_id)
        if not current:
            return None
        if expected_game is not None and current is not expected_game:
            return None
        if expected_msg_id is not None and current.get('msg_id') != expected_msg_id:
            return None
        removed_game = active_group_games.pop(group_id, None)

    if removed_game:
        timer = removed_game.get('timer')
        if timer:
            try:
                timer.cancel()
            except Exception:
                pass
    with _group_lobby_edit_lock:
        _group_lobby_last_edit.pop(group_id, None)
    return removed_game


def _is_lobby_message_alive(group_id, game):
    """
    بررسی زنده بودن پیام لابی در گروه تلگرام.
    اگر پیام ساخت لابی توسط ادمین یا کاربران در گروه پاک شده باشد، False برمی‌گرداند.
    """
    if not game:
        return False
    msg_id = game.get('msg_id')
    if not msg_id:
        # اگر لابی در همین لحظه در حال ارسال اولیه است، زنده فرض می‌شود
        return (time.time() - float(game.get('created_at', 0))) < 10.0

    with queue_lock:
        current = active_group_games.get(group_id)
        if current is not game:
            return False
        snapshot = dict(current)
        snapshot['players'] = list(current.get('players', ()))
        snapshot['player_names'] = dict(current.get('player_names', {}))

    text, markup = _build_group_lobby_view(group_id, snapshot)
    try:
        bot.edit_message_text(
            text,
            group_id,
            msg_id,
            reply_markup=markup,
            parse_mode='HTML'
        )
        return True
    except Exception as e:
        desc = (getattr(e, 'description', None) or str(e) or '').lower()
        if 'message is not modified' in desc:
            return True
        if _is_lobby_message_deleted_error(e):
            return False
        try:
            bot.edit_message_reply_markup(group_id, msg_id, reply_markup=markup)
            return True
        except Exception as e2:
            if _is_lobby_message_deleted_error(e2):
                return False
            return True


def _is_group_admin_or_initiator(group_id, user_id, initiator_id):
    """بررسی اینکه کاربر سازنده بازی، ادمین ربات یا ادمین گروه است."""
    if user_id == initiator_id or user_id in ADMINS:
        return True
    try:
        member = bot.get_chat_member(group_id, user_id)
        return getattr(member, 'status', None) in ('creator', 'administrator')
    except Exception:
        return False


def _build_group_lobby_view(group_id, game):
    """ساخت متن و دکمه‌های شیشه‌ای لابی بازی گروهی با نمایش لیست بازیکنان."""
    initiator_name = game.get('initiator_name', 'کاربر')
    players = list(game.get('players', ()))
    player_names = game.get('player_names', {})
    count = len(players)

    if players:
        shown_names = [escape_html(player_names.get(uid, 'بازیکن')) for uid in players[:20]]
        roster_text = ' ، '.join(shown_names)
        if count > 20:
            roster_text += f' و {count - 20} نفر دیگر...'
    else:
        roster_text = '<i>هنوز کسی وارد لابی نشده است — اولین نفر باش!</i>'

    ready_badge = (
        '🟢 آماده شروع!' if count >= 4
        else f'🟡 نیازمند حداقل {4 - count} بازیکن دیگر'
    )

    text = (
        f"💘🔥 <b>لابی بلایند دیت گروهی (Blind Ship)</b> 🔥💘\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
        f"👑 <b>شروع‌کننده:</b> {initiator_name}\n"
        f"⏱ <b>مهلت عضوگیری:</b> ۳ دقیقه | <b>زمان دیت:</b> ۱۵ دقیقه\n"
        f"🎯 <b>سیستم مچینگ:</b> هوشمند بر اساس <b>گرایش و پوزیشن</b>\n"
        f"👥 <b>بازیکنان حاضر ({count}/{MAX_GROUP_PLAYERS}):</b> {ready_badge}\n"
        f"└ {roster_text}\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
        f"💡 <i>نکته: پروفایل شما در پی‌وی ربات باید کامل باشد. در پایان دیت ۱۵ دقیقه‌ای، در صورت تایید دوطرفه آیدی‌ها رد و بدل می‌شود!</i>\n"
        f"👇 <b>برای ورود یا خروج از دکمه‌های زیر استفاده کنید:</b>"
    )

    markup = types.InlineKeyboardMarkup(row_width=2)
    markup.add(
        types.InlineKeyboardButton(f"🎮 جوین میشم! ({count} نفر)", callback_data=f"gg_join_{group_id}"),
        types.InlineKeyboardButton("🚪 خروج از لابی", callback_data=f"gg_leave_{group_id}")
    )
    markup.add(
        types.InlineKeyboardButton("🚀 شروع بازی", callback_data=f"gg_start_{group_id}"),
        types.InlineKeyboardButton("❌ لغو بازی", callback_data=f"gg_cancel_{group_id}")
    )
    return text, markup


def _refresh_group_lobby_message(group_id, game, force=False):
    """بروزرسانی ضدفلود پیام لابی در گروه؛ در صورت پاک شدن پیام لابی، بازی را به طور کامل لغو می‌کند."""
    now_ts = time.time()
    with _group_lobby_edit_lock:
        last_ts = _group_lobby_last_edit.get(group_id, 0.0)
        if not force and (now_ts - last_ts) < 1.2:
            return
        _group_lobby_last_edit[group_id] = now_ts

    text, markup = _build_group_lobby_view(group_id, game)
    msg_id = game.get('msg_id')
    if not msg_id:
        return
    try:
        bot.edit_message_text(
            text,
            group_id,
            msg_id,
            reply_markup=markup,
            parse_mode='HTML'
        )
    except Exception as e:
        if _is_lobby_message_deleted_error(e):
            _cancel_group_lobby(group_id, expected_msg_id=msg_id)
            return
        try:
            bot.edit_message_reply_markup(group_id, msg_id, reply_markup=markup)
        except Exception as e2:
            if _is_lobby_message_deleted_error(e2):
                _cancel_group_lobby(group_id, expected_msg_id=msg_id)


def _position_compatibility_score(pos1, pos2):
    """
    امتیازدهی هوشمند پوزیشن در بلایند دیت:
    - تاپ (top) + بات (bottom): مکمل کامل (+55)
    - ورس (vers) با همه پوزیشن‌ها (top, bottom, vers, unknown) سازگار است (+50 تا +52)
    - نامشخص (unknown) با همه سازگاری خنثی/مثبت دارد (+28)
    - تاپ + تاپ یا بات + بات: جریمه سنگین (-45) تا زمانی که گزینه بهتری وجود دارد هرگز با هم نیفتند.
    """
    p1 = (pos1 or 'unknown').lower()
    p2 = (pos2 or 'unknown').lower()

    if (p1 == 'top' and p2 == 'bottom') or (p1 == 'bottom' and p2 == 'top'):
        return 55, 'مکمل پوزیشن (تاپ و بات) 🔥'

    if p1 == 'vers' or p2 == 'vers':
        if p1 == 'vers' and p2 == 'vers':
            return 50, 'پوزیشن ورس دوطرفه ↔️'
        if 'top' in (p1, p2) or 'bottom' in (p1, p2):
            return 52, 'پوزیشن تطبیق‌پذیر (ورس) ⚡️'
        return 42, 'پوزیشن منعطف ✨'

    if p1 == 'unknown' or p2 == 'unknown':
        return 28, 'پوزیشن آزاد 🎭'

    if (p1 == 'top' and p2 == 'top') or (p1 == 'bottom' and p2 == 'bottom'):
        return -45, 'پوزیشن مشابه'

    return 25, 'پوزیشن عمومی'


def _orientation_compatibility_score(g1, g2, pos_score):
    """
    امتیازدهی هوشمند گرایش و جنسیت در بلایند دیت:
    - جفت‌های مکمل مرد/ترنس‌مرد با زن/ترنس‌زن
    - هم‌گرایش‌های مستقیم (گی با گی/بای/پن/فمبوی/مرد، لزبین با لزبین/بای/پن/تامبوی/زن)
    - بایسکشوال و پنسکشوال سازگار با تمام گرایش‌ها
    - جلوگیری از جفت شدن گرایش‌های متضاد (مثلاً گی با لزبین/زن یا لزبین با گی/مرد)
    """
    g1 = (g1 or 'other').lower()
    g2 = (g2 or 'other').lower()
    pair_set = {g1, g2}

    male_spec = {'male', 'transmale'}
    female_spec = {'female', 'transfemale'}
    bi_pan = {'bisexual', 'pansexual'}
    fem_trans = {'femboy', 'shemale'}
    queer_nb = {'nonbinary', 'genderfluid', 'bigender', 'agender', 'other'}

    # ۱. عدم تطابق صریح گرایش‌ها (گی با زن/لزبین/تامبوی یا لزبین با مرد/گی/فمبوی/شیمیل)
    if 'gay' in pair_set and (pair_set & (female_spec | {'lesbian', 'tomboy'})):
        return -75, 'گرایش غیرهمسو'
    if 'lesbian' in pair_set and (pair_set & (male_spec | {'gay'} | fem_trans)):
        return -75, 'گرایش غیرهمسو'

    # ۲. ای‌سکشوال با ای‌سکشوال
    if g1 == 'asexual' and g2 == 'asexual':
        return 60, 'هم‌گرایش (ای‌سکشوال) 🖤'

    # ۳. پنسکشوال و بایسکشوال (سازگاری بالا با همه؛ چون محدودیت‌های gay/lesbian در گام ۱ چک شده‌اند)
    if g1 in bi_pan or g2 in bi_pan:
        if g1 in bi_pan and g2 in bi_pan:
            return 60, 'گرایش منعطف دوطرفه 💖'
        return 56, 'گرایش مکمل و منعطف 💖'

    # ۴. تطابق کلاسیک جنس مخالف (مرد/ترنس‌مرد + زن/ترنس‌زن)
    if (g1 in male_spec and g2 in female_spec) or (g1 in female_spec and g2 in male_spec):
        return 65, 'گرایش و جنسیت مکمل 💑'

    # ۵. تطابق اختصاصی گی
    if 'gay' in pair_set:
        if g1 == 'gay' and g2 == 'gay':
            return 65, 'هم‌گرایش مستقیم (گی) 🏳️‍🌈'
        if pair_set & (male_spec | fem_trans):
            return 58, 'گرایش همسو و مکمل 🏳️‍🌈'
        return 40, 'گرایش سازگار'

    # ۶. تطابق اختصاصی لزبین
    if 'lesbian' in pair_set:
        if g1 == 'lesbian' and g2 == 'lesbian':
            return 65, 'هم‌گرایش مستقیم (لزبین) 🧡'
        if pair_set & (female_spec | {'tomboy'}):
            return 58, 'گرایش همسو و مکمل 🧡'
        return 40, 'گرایش سازگار'

    # ۷. تطابق فمبوی و شیمیل
    if pair_set & fem_trans:
        if pair_set & (male_spec | fem_trans | {'tomboy'}):
            return 56, 'تطابق جذاب گرایش 🎀'
        if pair_set & female_spec:
            return 32, 'تطابق عمومی'
        return 45, 'گرایش سازگار'

    # ۸. تطابق تامبوی
    if 'tomboy' in pair_set:
        if pair_set & (female_spec | male_spec | {'tomboy'}):
            return 54, 'تطابق جذاب گرایش 🧢'
        return 42, 'گرایش سازگار'

    # ۹. دو هم‌جنس (male+male یا female+female) که پوزیشن مکمل دارند (تاپ+بات یا ورس)
    if (g1 in male_spec and g2 in male_spec) or (g1 in female_spec and g2 in female_spec):
        if pos_score >= 45:
            return 44, 'هم‌گرایش با پوزیشن مکمل ⚡️'
        return 15, 'تطابق عمومی'

    # ۱۰. غیرباینری، جندرفلوید، بایجندر و سایر
    if pair_set & queer_nb:
        return 42, 'گرایش آزاد و سازگار ✨'

    return 30, 'تطابق عمومی'


def _calculate_blind_date_compatibility(u1_info, u2_info, had_recent_chat=False):
    """
    محاسبه امتیاز کل سازگاری بین دو بازیکن در یک رده سنی یکسان.
    خروجی: (total_score, badge_text)
    """
    pos_score, pos_label = _position_compatibility_score(u1_info['pos'], u2_info['pos'])
    ori_score, ori_label = _orientation_compatibility_score(u1_info['gender'], u2_info['gender'], pos_score)

    total = pos_score + ori_score

    # امتیاز نزدیکی سنی (در همان رده سنی مجاز)
    age1, age2 = u1_info.get('age'), u2_info.get('age')
    if isinstance(age1, int) and isinstance(age2, int):
        age_diff = abs(age1 - age2)
        if age_diff <= 3:
            total += 10
        elif age_diff <= 7:
            total += 5

    # امتیاز هم‌استان بودن
    prov1, prov2 = u1_info.get('prov'), u2_info.get('prov')
    if prov1 and prov2 and prov1 != 'نامشخص' and prov1 == prov2:
        total += 12

    # جریمه ملایم در صورت چت اخیر، تا در دورهای متوالی افراد جدید به هم بیفتند
    if had_recent_chat:
        total -= 25

    if total >= 95:
        badge = f"🔥 فوق‌العاده ({ori_label} + {pos_label})"
    elif total >= 70:
        badge = f"⚡️ عالی ({ori_label} | {pos_label})"
    elif total >= 40:
        badge = f"✨ خوب ({pos_label})"
    else:
        badge = "🎲 شانسی / آزاد"

    return total, badge


def _match_blind_date_pool(pool, player_info, blocked_pairs, recent_chat_pairs):
    """
    الگوریتم پیشرفته جفت‌سازی بلایند دیت برای یک رده سنی:
    ۱. ساخت گراف سازگاری تمام جفت‌های مجاز (بدون بلاک دوطرفه).
    ۲. انتخاب اولویت‌دار بر اساس کمترین گزینه‌های سازگار (MRV) + بالاترین امتیاز (تا ورس‌ها که با همه سازگارند، جفتِ افراد خاص را نگیرند).
    ۳. بهینه‌سازی محلی 2-Opt Swap برای حذف هرگونه تداخل پوزیشن و بیشینه‌سازی مجموع امتیاز کل جفت‌ها.
    """
    n = len(pool)
    if n < 2:
        return [], list(pool)

    scores = {}
    badges = {}
    compatible_neighbors = defaultdict(set)

    for i in range(n):
        u1 = pool[i]
        for j in range(i + 1, n):
            u2 = pool[j]
            pair_key = (min(u1, u2), max(u1, u2))
            if pair_key in blocked_pairs:
                continue
            sc, bdg = _calculate_blind_date_compatibility(
                player_info[u1],
                player_info[u2],
                had_recent_chat=(pair_key in recent_chat_pairs)
            )
            scores[pair_key] = sc
            badges[pair_key] = bdg
            if sc >= 35:
                compatible_neighbors[u1].add(u2)
                compatible_neighbors[u2].add(u1)

    remaining = set(pool)
    pairs = []

    # فاز ۱: جفت‌سازی هوشمند با اولویت بازیکنان دارای گزینه‌های محدودتر (MRV) و امتیاز بالاتر
    while len(remaining) >= 2:
        best_u1 = None
        best_u2 = None
        best_tuple = None

        shuffled_rem = list(remaining)
        random.shuffle(shuffled_rem)

        for u1 in shuffled_rem:
            deg1 = len(compatible_neighbors[u1] & remaining)
            for u2 in shuffled_rem:
                if u1 >= u2:
                    continue
                pair_key = (u1, u2)
                if pair_key not in scores:
                    continue
                sc = scores[pair_key]
                deg2 = len(compatible_neighbors[u2] & remaining)
                min_deg = min(deg1, deg2)
                # اولویت اول: آیا جفت سازگاری مثبت (sc >= 35) است؟
                # اولویت دوم: بازیکنی که گزینه‌های سازگار کمتری در لابی دارد (که ورس‌ها در آخر با هر کسی که ماند مچ شوند)
                # اولویت سوم: بالاترین امتیاز سازگاری گرایش و پوزیشن
                is_strong = 1 if sc >= 35 else 0
                cand_tuple = (
                    is_strong,
                    -min_deg if is_strong else 0,
                    sc,
                    random.random()
                )
                if best_tuple is None or cand_tuple > best_tuple:
                    best_tuple = cand_tuple
                    best_u1, best_u2 = u1, u2

        if best_u1 is None or best_u2 is None:
            break

        pairs.append((best_u1, best_u2))
        remaining.discard(best_u1)
        remaining.discard(best_u2)

    # فاز ۲: بهینه‌سازی 2-Opt Swap بین جفت‌های تشکیل‌شده برای رسیدن به بالاترین تطابق پوزیشن و گرایش
    improved = True
    iterations = 0
    while improved and iterations < 30 and len(pairs) >= 2:
        improved = False
        iterations += 1
        for i in range(len(pairs)):
            for j in range(i + 1, len(pairs)):
                a, b = pairs[i]
                c, d = pairs[j]
                cur_key1 = (min(a, b), max(a, b))
                cur_key2 = (min(c, d), max(c, d))
                cur_score = scores.get(cur_key1, -999) + scores.get(cur_key2, -999)

                # حالت اول جابجایی: (a, c) و (b, d)
                alt1_k1 = (min(a, c), max(a, c))
                alt1_k2 = (min(b, d), max(b, d))
                alt1_score = (
                    scores[alt1_k1] + scores[alt1_k2]
                    if alt1_k1 in scores and alt1_k2 in scores
                    else -9999
                )

                # حالت دوم جابجایی: (a, d) و (b, c)
                alt2_k1 = (min(a, d), max(a, d))
                alt2_k2 = (min(b, c), max(b, c))
                alt2_score = (
                    scores[alt2_k1] + scores[alt2_k2]
                    if alt2_k1 in scores and alt2_k2 in scores
                    else -9999
                )

                if alt1_score > cur_score + 2 and alt1_score >= alt2_score:
                    pairs[i] = (a, c)
                    pairs[j] = (b, d)
                    improved = True
                    break
                if alt2_score > cur_score + 2:
                    pairs[i] = (a, d)
                    pairs[j] = (b, c)
                    improved = True
                    break
            if improved:
                break

    final_pairs = []
    for u1, u2 in pairs:
        pk = (min(u1, u2), max(u1, u2))
        final_pairs.append((u1, u2, badges.get(pk, '✨ تطابق هوشمند')))

    return final_pairs, list(remaining)


@bot.message_handler(commands=['startgame'])
def start_group_game_cmd(message):
    if message.chat.type == 'private':
        bot.reply_to(message, "❌ این دستور مخصوص استفاده در گروه‌هاست!")
        return

    group_id = message.chat.id
    initiator_id = message.from_user.id
    initiator_name = escape_html(message.from_user.first_name or 'کاربر')
    group_title = escape_html(message.chat.title or 'گروه')

    lobby_lock = _group_lobby_locks[abs(int(group_id)) % len(_group_lobby_locks)]
    with lobby_lock:
        with queue_lock:
            existing_game = active_group_games.get(group_id)

        if existing_game is not None:
            # بررسی اینکه آیا پیام لابی قبلی هنوز در گروه وجود دارد یا پاک شده است
            if _is_lobby_message_alive(group_id, existing_game):
                existing_msg_id = existing_game.get('msg_id')
                warn_text = (
                    "⚠️ <b>این گروه هم‌اکنون یک لابی فعال دارد!</b>\n"
                    "هر گروه فقط می‌تواند یک لابی همزمان داشته باشد. از طریق پیام لابیِ بالا وارد بازی شوید یا آن را لغو کنید."
                )
                try:
                    if existing_msg_id:
                        bot.send_message(
                            group_id,
                            warn_text,
                            reply_to_message_id=existing_msg_id,
                            parse_mode='HTML'
                        )
                    else:
                        bot.reply_to(message, warn_text, parse_mode='HTML')
                    return
                except Exception as reply_exc:
                    if not _is_lobby_message_deleted_error(reply_exc):
                        try:
                            bot.reply_to(message, warn_text, parse_mode='HTML')
                        except Exception:
                            pass
                        return
                    # اگر در همین لحظه پیام لابی پاک شده بود، لابی قبلی را کامل لغو کن و لابی جدید بساز
                    _cancel_group_lobby(group_id, expected_game=existing_game)
            else:
                # پیام ساخت لابی قبلی پاک شده است -> لغو کامل لابی قبلی و ساخت لابی جدید
                _cancel_group_lobby(group_id, expected_game=existing_game)

        with queue_lock:
            if group_id in active_group_games:
                bot.reply_to(message, "⚠️ یک لابی بازی در این گروه در حال عضوگیری است!")
                return

            game_data = {
                'initiator': initiator_id,
                'initiator_name': initiator_name,
                'players': [],
                'player_names': {},
                'msg_id': None,
                'timer': None,
                'title': group_title,
                'created_at': time.time(),
            }
            active_group_games[group_id] = game_data

        text, markup = _build_group_lobby_view(group_id, game_data)
        try:
            msg = bot.send_message(group_id, text, reply_markup=markup, parse_mode='HTML')
        except Exception:
            _cancel_group_lobby(group_id, expected_game=game_data)
            return

        timer = threading.Timer(BLIND_DATE_LOBBY_SECONDS, execute_group_game, args=(group_id, group_title))
        timer.daemon = True

        with queue_lock:
            current_game = active_group_games.get(group_id)
            if current_game is game_data:
                current_game['msg_id'] = msg.message_id
                current_game['timer'] = timer
                timer.start()


@bot.callback_query_handler(func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('gg_'))
def group_game_lobby_callbacks(call):
    parts = (call.data or '').split('_')
    if len(parts) != 3 or parts[1] not in ('join', 'leave', 'start', 'cancel'):
        bot.answer_callback_query(call.id, '🚫 دکمه نامعتبر است.', show_alert=True)
        return

    action = parts[1]
    try:
        group_id = int(parts[2])
    except ValueError:
        bot.answer_callback_query(call.id, '🚫 شناسه گروه نامعتبر است.', show_alert=True)
        return

    user_id = call.from_user.id
    user_first_name = (getattr(call.from_user, 'first_name', None) or 'بازیکن')[:25]

    with queue_lock:
        game = active_group_games.get(group_id)
        if not game:
            bot.answer_callback_query(call.id, '❌ این لابی منقضی شده یا بازی قبلاً شروع شده است.', show_alert=True)
            return
        if call.message and game.get('msg_id') and call.message.message_id != game.get('msg_id'):
            bot.answer_callback_query(call.id, '❌ این پیام مربوط به لابی قبلی است و منقضی شده است.', show_alert=True)
            return
        initiator = game['initiator']

    # ۱. لغو بازی توسط سازنده یا ادمین گروه/ربات
    if action == 'cancel':
        if not _is_group_admin_or_initiator(group_id, user_id, initiator):
            bot.answer_callback_query(call.id, '⛔ فقط سازنده بازی یا ادمین‌های گروه می‌توانند بازی را لغو کنند.', show_alert=True)
            return

        game = _cancel_group_lobby(group_id, expected_game=game)
        if not game:
            bot.answer_callback_query(call.id, '❌ بازی قبلاً بسته شده است.', show_alert=True)
            return

        try:
            bot.edit_message_text(
                f"❌ <b>لابی بازی بلایند دیت توسط {escape_html(user_first_name)} لغو شد.</b>",
                group_id,
                game['msg_id'],
                parse_mode='HTML',
                reply_markup=types.InlineKeyboardMarkup()
            )
        except Exception:
            pass
        bot.answer_callback_query(call.id, '✅ لابی بازی لغو شد.')
        return

    # ۲. شروع بازی توسط سازنده یا ادمین گروه/ربات
    if action == 'start':
        if not _is_group_admin_or_initiator(group_id, user_id, initiator):
            bot.answer_callback_query(call.id, '⛔ فقط سازنده بازی یا ادمین گروه می‌تواند بازی را شروع کند.', show_alert=True)
            return

        with queue_lock:
            game = active_group_games.get(group_id)
            if not game:
                bot.answer_callback_query(call.id, '❌ بازی منقضی شده است.', show_alert=True)
                return
            if len(game['players']) < 4:
                bot.answer_callback_query(
                    call.id,
                    f'⚠️ برای شروع بازی حداقل ۴ بازیکن لازم است (فعلاً {len(game["players"])} نفر عضو شده‌اند).',
                    show_alert=True
                )
                return
            title = game['title']
            if game.get('timer'):
                try:
                    game['timer'].cancel()
                except Exception:
                    pass

        bot.answer_callback_query(call.id, '🚀 بلایند دیت شروع شد!')
        execute_group_game(group_id, title)
        return

    # ۳. خروج از لابی
    if action == 'leave':
        with queue_lock:
            game = active_group_games.get(group_id)
            if not game:
                bot.answer_callback_query(call.id, '❌ بازی منقضی شده است.', show_alert=True)
                return
            if user_id not in game['players']:
                bot.answer_callback_query(call.id, 'ℹ️ شما در لابی این بازی حضور ندارید.', show_alert=True)
                return
            game['players'].remove(user_id)
            game['player_names'].pop(user_id, None)
            snapshot = dict(game)
            snapshot['players'] = list(game['players'])
            snapshot['player_names'] = dict(game['player_names'])

        _refresh_group_lobby_message(group_id, snapshot, force=True)
        bot.answer_callback_query(call.id, '🚪 شما از لابی بازی خارج شدید.')
        return

    # ۴. ورود به لابی (اعتبارسنجی کامل بیرون از queue_lock)
    user_row = get_user(user_id)
    if not user_row or not is_profile_complete(user_id):
        bot.answer_callback_query(
            call.id,
            '⚠️ ابتدا وارد پی‌وی ربات شوید، /start را بزنید و پروفایل خود (سن، جنسیت، پوزیشن و شهر) را تکمیل کنید!',
            show_alert=True
        )
        return

    if is_blocked(user_id):
        bot.answer_callback_query(call.id, '🚫 حساب شما در ربات مسدود است.', show_alert=True)
        return

    if is_user_in_chat(user_id):
        bot.answer_callback_query(call.id, '⚠️ شما هم‌اکنون در یک چت فعال هستید! ابتدا چت قبلی را ببندید.', show_alert=True)
        return

    if not check_channels_membership(user_id):
        bot.answer_callback_query(call.id, '❌ ابتدا در کانال‌های اجباری ربات عضو شوید.', show_alert=True)
        return

    display_name = (user_row[4] or user_first_name or 'بازیکن').strip()[:25]

    with queue_lock:
        game = active_group_games.get(group_id)
        if not game:
            bot.answer_callback_query(call.id, '❌ بازی منقضی شده است.', show_alert=True)
            return
        if user_id in game['players']:
            bot.answer_callback_query(call.id, '✅ شما از قبل در لابی حضور دارید!')
            return
        if len(game['players']) >= MAX_GROUP_PLAYERS:
            bot.answer_callback_query(call.id, '⚠️ ظرفیت این دور از بازی تکمیل شده است.', show_alert=True)
            return

        game['players'].append(user_id)
        game['player_names'][user_id] = display_name
        count = len(game['players'])
        snapshot = dict(game)
        snapshot['players'] = list(game['players'])
        snapshot['player_names'] = dict(game['player_names'])

    _refresh_group_lobby_message(group_id, snapshot, force=(count <= 6 or count % 2 == 0))
    bot.answer_callback_query(call.id, '💘 شما با موفقیت وارد لابی بلایند دیت شدید!')


def _send_game_one_minute_warning(u1, u2, chat_id):
    """ارسال هشدار ۱ دقیقه پایانی در بلایند دیت در صورتی که چت هنوز برقرار باشد."""
    with queue_lock:
        p1 = active_pairs.get(u1)
        p2 = active_pairs.get(u2)
        if not (
            p1 and p2
            and p1[0] == u2 and p2[0] == u1
            and p1[1] == chat_id and p2[1] == chat_id
            and chat_id in game_sessions
        ):
            return

    warn_text = (
        "⏳ <b>۱ دقیقه تا پایان زمان بلایند دیت باقی مانده است!</b>\n"
        "💡 <i>بعد از اتمام زمان، از هر دوی شما پرسیده می‌شود که آیا مایل به تبادل آیدی تلگرام هستید یا خیر.</i>"
    )
    for uid in (u1, u2):
        try:
            bot.send_message(uid, warn_text, parse_mode='HTML')
        except Exception:
            pass


def execute_group_game(group_id, group_title):
    """بستن لابی، اجرای الگوریتم هوشمند تطابق گرایش/پوزیشن با جداسازی رده سنی و شروع دیت‌ها."""
    with queue_lock:
        game = active_group_games.pop(group_id, None)

    if not game:
        return

    lobby_timer = game.get('timer')
    if lobby_timer:
        try:
            lobby_timer.cancel()
        except Exception:
            pass
    with _group_lobby_edit_lock:
        _group_lobby_last_edit.pop(group_id, None)

    msg_id = game.get('msg_id')
    if not msg_id:
        return

    # بررسی اولیه: اگر پیام ساخت لابی در گروه پاک شده باشد، کل بازی باید لغو شود و هیچ دیتی شروع نشود
    try:
        bot.edit_message_reply_markup(
            group_id,
            msg_id,
            reply_markup=types.InlineKeyboardMarkup()
        )
    except Exception as e:
        if _is_lobby_message_deleted_error(e):
            logger.info('Group lobby message was deleted in group=%s; cancelling blind date completely.', group_id)
            return

    def close_lobby(text):
        try:
            bot.edit_message_text(
                text,
                group_id,
                msg_id,
                parse_mode='HTML',
                reply_markup=types.InlineKeyboardMarkup()
            )
        except Exception:
            pass

    raw_players = list(game.get('players', ()))
    valid_players = []
    player_info = {}

    for uid in raw_players:
        try:
            if is_blocked(uid) or is_user_in_chat(uid):
                continue
            if not is_profile_complete(uid):
                continue
            row = get_user(uid)
            if not row:
                continue
            try:
                age_val = int(row[6])
            except (TypeError, ValueError):
                age_val = None
            loc_str = row[8] or 'نامشخص'
            prov_str = loc_str.split(' - ')[0] if ' - ' in loc_str else loc_str
            gender_key = (row[9] or 'other').lower()
            pos_key = (get_user_position(uid) or 'unknown').lower()

            player_info[uid] = {
                'age': age_val,
                'gender': gender_key,
                'pos': pos_key,
                'prov': prov_str,
                'name': row[4] or 'بازیکن',
            }
            valid_players.append(uid)
        except Exception:
            logger.warning('Group game validation failed for user=%s', uid, exc_info=True)

    if len(valid_players) < 4:
        close_lobby(
            f"❌ <b>بلایند دیت گروه «{escape_html(group_title)}» لغو شد!</b>\n"
            f"👥 حداقل ۴ بازیکن واجد شرایط لازم بود (افراد معتبر حاضر: {len(valid_players)} نفر).\n"
            "برای شروع مجدد دستور /startgame را ارسال کنید."
        )
        return

    # استخراج بلاک‌های دوطرفه و آخرین چت‌های اخیر بین بازیکنان حاضر در یک کوئری سریع
    blocked_pairs = set()
    recent_chat_pairs = set()
    try:
        placeholders = ','.join('?' for _ in valid_players)
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute(
                f'''SELECT blocker_id, blocked_id FROM user_blocks
                    WHERE blocker_id IN ({placeholders}) AND blocked_id IN ({placeholders})''',
                tuple(valid_players) + tuple(valid_players)
            )
            for b1, b2 in cur.fetchall():
                blocked_pairs.add((min(int(b1), int(b2)), max(int(b1), int(b2))))

            cur.execute(
                f'''SELECT user1_id, user2_id FROM chats
                    WHERE user1_id IN ({placeholders}) AND user2_id IN ({placeholders})
                    ORDER BY chat_id DESC LIMIT 60''',
                tuple(valid_players) + tuple(valid_players)
            )
            for c1, c2 in cur.fetchall():
                recent_chat_pairs.add((min(int(c1), int(c2)), max(int(c1), int(c2))))
    except Exception:
        logger.warning('Could not prefetch block/chat history for group game', exc_info=True)

    # تفکیک سخت‌گیرانه رده‌های سنی (بزرگسال +۱۸ و زیر ۱۸ سال هرگز با هم جفت نمی‌شوند)
    adults = []
    minors = []
    unclassified = []
    for uid in valid_players:
        age = player_info[uid].get('age')
        if age is None:
            unclassified.append(uid)
        elif age >= 18:
            adults.append(uid)
        else:
            minors.append(uid)

    matched_pairs = []
    unmatched = []

    adult_pairs, adult_rem = _match_blind_date_pool(adults, player_info, blocked_pairs, recent_chat_pairs)
    minor_pairs, minor_rem = _match_blind_date_pool(minors, player_info, blocked_pairs, recent_chat_pairs)

    matched_pairs.extend(adult_pairs)
    matched_pairs.extend(minor_pairs)
    unmatched.extend(adult_rem)
    unmatched.extend(minor_rem)
    unmatched.extend(unclassified)

    if not matched_pairs:
        close_lobby(
            f"❌ <b>بازی گروه «{escape_html(group_title)}» اجرا نشد!</b>\n"
            "با توجه به محدودیت‌های رده سنی یا بلاک‌ها، جفت مجازی تشکیل نشد."
        )
        for uid in valid_players:
            try:
                bot.send_message(
                    uid,
                    '⚠️ در این دور از بلایند دیت جفت مناسبی برای شما پیدا نشد. لطفاً در دور بعد دوباره شرکت کنید.'
                )
            except Exception:
                pass
        return

    # شروع جلسات بلایند دیت و ارسال کارت معرفی + سوال یخ‌شکن
    successful_pairs_count = 0
    for u1, u2, compat_badge in matched_pairs:
        # اگر کاربر قبلاً در صف جستجوی عادی بوده، رزرو سکه/سهمیه او را آزاد کن تا نسوزد
        release_search_reservation(u1)
        release_search_reservation(u2)

        try:
            paired = pair_users(u1, u2, via_link=False, is_game=True)
        except Exception:
            paired = False
            logger.error('Group game pairing failed: %s/%s', u1, u2, exc_info=True)

        if not paired:
            unmatched.extend([u1, u2])
            for uid in (u1, u2):
                try:
                    bot.send_message(uid, '⚠️ اتصال بلایند دیت این دور برقرار نشد (طرف مقابل در دسترس نبود).')
                except Exception:
                    pass
            continue

        successful_pairs_count += 1
        with queue_lock:
            pair = active_pairs.get(u1)
            if not pair or pair[0] != u2:
                continue
            chat_id = pair[1]
            game_sessions.add(chat_id)
            game_chat_groups[chat_id] = group_id

        icebreaker_q = random.choice(ICEBREAKER_NORMAL)
        intro_card = (
            f"💘 <b>کارت بلایند دیت شما | گروه «{escape_html(group_title)}»</b>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"🎯 <b>تطابق هوشمند:</b> <code>{escape_html(compat_badge)}</code>\n"
            f"⏱ <b>زمان گفتگو:</b> <code>۱۵ دقیقه</code>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"🧊 <b>سوال یخ‌شکن برای شروع صحبت:</b>\n"
            f"🗣 <i>«{escape_html(icebreaker_q)}»</i>\n\n"
            f"💡 <i>هر دو نفر به این سوال جواب بدید یا سر صحبت رو باز کنید!</i>"
        )
        for uid in (u1, u2):
            try:
                bot.send_message(uid, intro_card, parse_mode='HTML')
            except Exception:
                pass

        warn_timer = threading.Timer(
            BLIND_DATE_WARNING_SECONDS,
            _send_game_one_minute_warning,
            args=(u1, u2, chat_id)
        )
        warn_timer.daemon = True
        warn_timer.start()

        session_timer = threading.Timer(
            BLIND_DATE_CHAT_SECONDS,
            end_game_session,
            args=(u1, u2, chat_id, group_title)
        )
        session_timer.daemon = True
        session_timer.start()

    paired_players_count = successful_pairs_count * 2
    close_lobby(
        f"💘🚀 <b>بلایند دیت گروه «{escape_html(group_title)}» آغاز شد!</b>\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
        f"✅ <b>تعداد افراد مچ‌شده:</b> <code>{paired_players_count} نفر ({successful_pairs_count} جفت)</code>\n"
        f"⚠️ <b>تعداد افراد بدون جفت در این دور:</b> <code>{len(unmatched)} نفر</code>\n"
        f"⏱ <b>مدت زمان هر دیت:</b> ۱۵ دقیقه\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
        f"📩 <i>بازیکنان جفت‌شده همین الان به پی‌وی ربات بروند!</i>"
    )

    for uid in unmatched:
        try:
            bot.send_message(
                uid,
                '⚠️ در این دور از بلایند دیت جفت هم‌رده و متناسبی برای شما باقی نماند (به دلیل فرد بودن تعداد رده سنی یا محدودیت تطابق).\n'
                'در دور بعدی حتماً دوباره شرکت کنید! 🌹'
            )
        except Exception:
            pass


# ==========================================
# 🎮 سیستم بازی گروهی: پایان چت و افشای هویت دوطرفه
# ==========================================
game_chat_groups = {}


def end_game_session(u1, u2, chat_id, group_title):
    """پایان چت ۱۵ دقیقه‌ای بلایند دیت و باز کردن پنجره ۹۰ ثانیه‌ای رضایت دوطرفه."""
    with queue_lock:
        p1 = active_pairs.get(u1)
        p2 = active_pairs.get(u2)
        if not (
            p1 and p2
            and p1[0] == u2 and p2[0] == u1
            and p1[1] == chat_id and p2[1] == chat_id
        ):
            return

        if chat_id in game_reveal_choices:
            return
        group_id = game_chat_groups.get(chat_id)
        game_reveal_choices[chat_id] = {
            'timestamp': time.time(),
            'phase': 'closing',
            'group_id': group_id,
        }

    terminated = terminate_chat_session(chat_id, u1, u2, send_feedback=False)
    if not terminated:
        with queue_lock:
            game_reveal_choices.pop(chat_id, None)
            game_chat_groups.pop(chat_id, None)
        return

    with queue_lock:
        state = game_reveal_choices.get(chat_id)
        if not state or state.get('phase') != 'closing':
            return
        group_id = state.get('group_id')
        state.clear()
        state.update({
            'timestamp': time.time(),
            'phase': 'pending',
            'group_id': group_id,
        })

    def _build_reveal_markup(partner_uid):
        m = types.InlineKeyboardMarkup(row_width=1)
        m.add(
            types.InlineKeyboardButton('😍 بله، آیدیم رو بهش بده! (مچ شدن)', callback_data=f'reveal_yes_{chat_id}_{partner_uid}'),
            types.InlineKeyboardButton('🤝 افزودن به دوستان ناشناس (بدون لو رفتن آیدی)', callback_data=f'friend_postchat_{chat_id}_{partner_uid}'),
            types.InlineKeyboardButton('🥷 نه، ناشناس بمونم', callback_data=f'reveal_no_{chat_id}_{partner_uid}')
        )
        return m

    text = (
        f"⏱💘 <b>زمان بلایند دیت شما در گروه «{escape_html(group_title)}» به پایان رسید!</b>\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
        f"❓ <b>آیا از هم‌بازی خود خوشتان آمد؟</b>\n"
        f"• اگر <b>هر دو نفر</b> دکمه «بله، آیدیم رو بهش بده 😍» را بزنید، آیدی تلگرام شما برای یکدیگر ارسال می‌شود!\n"
        f"• همچنین می‌توانید بدون لو رفتن آیدی تلگرام، ایشان را به <b>«دوستان ناشناس 🤝»</b> داخل ربات اضافه کنید.\n\n"
        f"⏳ <i>برای تصمیم‌گیری ۹۰ ثانیه فرصت دارید؛ در غیر این صورت هویت هر دو نفر کاملاً مخفی می‌ماند.</i>"
    )

    for uid, partner_uid in ((u1, u2), (u2, u1)):
        try:
            bot.send_message(uid, text, reply_markup=_build_reveal_markup(partner_uid), parse_mode='HTML')
            show_main_menu(uid, uid in ADMINS)
        except Exception:
            logger.warning('Could not send game reveal prompt to user=%s', uid, exc_info=True)

    reveal_timer = threading.Timer(
        BLIND_DATE_REVEAL_SECONDS,
        expire_game_reveal_choices,
        args=(chat_id, u1, u2)
    )
    reveal_timer.daemon = True
    reveal_timer.start()


def expire_game_reveal_choices(chat_id, u1, u2):
    """بستن پنجره ۹۰ ثانیه‌ای در صورت عدم تایید دوطرفه."""
    with queue_lock:
        state = game_reveal_choices.get(chat_id)
        if not state or state.get('phase') != 'pending':
            return False
        game_reveal_choices.pop(chat_id, None)
        game_chat_groups.pop(chat_id, None)

    for uid in (u1, u2):
        try:
            bot.send_message(
                uid,
                '⏱ مهلت ۹۰ ثانیه‌ای بلایند دیت تمام شد. از آنجا که تایید دوطرفه ثبت نشد، '
                'آیدی‌ها مبادله نشدند و هویت هر دو نفر ناشناس باقی ماند 🥷'
            )
        except Exception:
            pass
    return True


@bot.callback_query_handler(func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('reveal_'))
def handle_game_reveal_choice(call):
    """مدیریت تصمیم نهایی افشای آیدی در پایان بلایند دیت با بررسی امنیتی کامل."""
    parts = (call.data or '').split('_')
    if len(parts) != 4 or parts[0] != 'reveal':
        bot.answer_callback_query(call.id, '🚫 دکمه نامعتبر است.', show_alert=True)
        return

    choice = parts[1]
    if choice not in ('yes', 'no'):
        bot.answer_callback_query(call.id, '🚫 انتخاب نامعتبر است.', show_alert=True)
        return

    try:
        chat_id = int(parts[2])
        partner_id = int(parts[3])
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '🚫 دکمه منقضی است.', show_alert=True)
        return

    user_id = call.from_user.id
    if user_id == partner_id:
        bot.answer_callback_query(call.id, '🚫 دکمه نامعتبر است.', show_alert=True)
        return

    try:
        with get_db_connection() as conn:
            row = conn.execute(
                '''SELECT c.user1_id, c.user2_id, c.end_time,
                          u1.age, u2.age
                   FROM chats AS c
                   LEFT JOIN users AS u1 ON u1.user_id = c.user1_id
                   LEFT JOIN users AS u2 ON u2.user_id = c.user2_id
                   WHERE c.chat_id = ?''',
                (chat_id,)
            ).fetchone()
    except Exception:
        logger.error('Could not validate game reveal request', exc_info=True)
        bot.answer_callback_query(call.id, '❌ خطا در بررسی بازی.', show_alert=True)
        return

    if (
        not row
        or not row[2]
        or {int(row[0]), int(row[1])} != {user_id, partner_id}
    ):
        bot.answer_callback_query(
            call.id,
            '🚫 این درخواست معتبر یا متعلق به چت شما نیست.',
            show_alert=True
        )
        return

    try:
        age_by_user = {
            int(row[0]): int(row[3]),
            int(row[1]): int(row[4]),
        }
        both_adult_profiles = (
            age_by_user[user_id] >= 18
            and age_by_user[partner_id] >= 18
        )
    except (TypeError, ValueError, KeyError):
        both_adult_profiles = False

    if not both_adult_profiles:
        with queue_lock:
            game_reveal_choices.pop(chat_id, None)
            game_chat_groups.pop(chat_id, None)

        try:
            bot.delete_message(call.message.chat.id, call.message.message_id)
        except Exception:
            pass

        notice = (
            '🔒 برای حفظ حریم خصوصی رده سنی زیر ۱۸ سال، تبادل آیدی مستقیم تلگرام غیرفعال است.\n'
            'می‌توانید از سیستم «دوستان ناشناس 🤝» داخل ربات استفاده کنید.'
        )
        try:
            bot.answer_callback_query(
                call.id,
                '🔒 تبادل آیدی مستقیم برای این چت فعال نیست.',
                show_alert=True
            )
        except Exception:
            pass
        for uid in (user_id, partner_id):
            try:
                bot.send_message(uid, notice)
            except Exception:
                pass
        return

    outcome = None
    group_id = None
    with queue_lock:
        state = game_reveal_choices.get(chat_id)
        if not state or state.get('phase') != 'pending':
            outcome = 'expired'
        else:
            group_id = state.get('group_id')
            try:
                created_at = float(state.get('timestamp', 0))
            except (TypeError, ValueError):
                created_at = 0

            if not created_at or time.time() - created_at >= BLIND_DATE_REVEAL_SECONDS:
                game_reveal_choices.pop(chat_id, None)
                game_chat_groups.pop(chat_id, None)
                outcome = 'expired'
            elif choice == 'no':
                game_reveal_choices.pop(chat_id, None)
                game_chat_groups.pop(chat_id, None)
                outcome = 'no'
            else:
                state[user_id] = 'yes'
                if state.get(partner_id) == 'yes':
                    game_reveal_choices.pop(chat_id, None)
                    game_chat_groups.pop(chat_id, None)
                    outcome = 'both_yes'
                else:
                    outcome = 'waiting'

    try:
        bot.delete_message(call.message.chat.id, call.message.message_id)
    except Exception:
        pass

    if outcome == 'expired':
        bot.answer_callback_query(
            call.id,
            '⏱ مهلت پاسخ تمام شده؛ آیدی‌ها مبادله نشدند.',
            show_alert=True
        )
        return

    if outcome == 'no':
        bot.answer_callback_query(call.id, '🥷 هویت هر دو نفر مخفی می‌ماند.')
        for uid, text in (
            (user_id, '🥷 درخواست تبادل آیدی رد شد؛ هویت شما مخفی ماند.'),
            (partner_id, '🥷 طرف مقابل تمایلی به تبادل آیدی نداشت؛ هویت هر دو نفر مخفی ماند.')
        ):
            try:
                bot.send_message(uid, text)
            except Exception:
                pass
        return

    if outcome == 'waiting':
        bot.answer_callback_query(
            call.id,
            '⏳ نظر مثبت شما ثبت شد! منتظر پاسخ مخاطب بمانید...'
        )
        try:
            bot.send_message(
                user_id,
                '💘 <b>چراغ سبز شما ثبت شد!</b>\nمنتظریم ببینیم هم‌بازی شما هم گزینه «بله» را می‌زند یا نه... ⏳',
                parse_mode='HTML'
            )
        except Exception:
            pass
        return

    bot.answer_callback_query(
        call.id,
        '💘 تبریک! هر دو نفر موافقت کردید؛ آیدی‌ها ارسال می‌شوند.'
    )
    a = get_user(user_id)
    b = get_user(partner_id)
    if not a or not b:
        for uid in (user_id, partner_id):
            try:
                bot.send_message(
                    uid,
                    '❌ اطلاعات یکی از کاربران پیدا نشد؛ آیدی‌ها ارسال نشدند.'
                )
            except Exception:
                pass
        return

    a_name = escape_html(a[4] or 'کاربر')
    b_name = escape_html(b[4] or 'کاربر')
    a_username = escape_html(a[1]) if a[1] else None
    b_username = escape_html(b[1]) if b[1] else None
    a_link = (
        f'@{a_username}'
        if a_username else f'<a href="tg://user?id={user_id}">{a_name}</a>'
    )
    b_link = (
        f'@{b_username}'
        if b_username else f'<a href="tg://user?id={partner_id}">{b_name}</a>'
    )

    for uid, partner_uid, link in (
        (user_id, partner_id, b_link),
        (partner_id, user_id, a_link)
    ):
        match_markup = types.InlineKeyboardMarkup()
        if not are_users_friends(uid, partner_uid):
            match_markup.add(
                types.InlineKeyboardButton(
                    '🤝 افزودن به لیست دوستان داخل ربات',
                    callback_data=f'friend_postchat_{chat_id}_{partner_uid}'
                )
            )
        try:
            bot.send_message(
                uid,
                f"🎉💘 <b>مچ شدید! (It's a Match!)</b>\n"
                f"━━━━━━━━━━━━━━━━━━━\n"
                f"هر دوی شما در بلایند دیت به یکدیگر چراغ سبز نشان دادید 😍\n\n"
                f"🆔 <b>آیدی هم‌بازی شما:</b> {link}",
                reply_markup=match_markup,
                parse_mode='HTML'
            )
        except Exception:
            logger.warning('Could not send revealed ID to user=%s', uid, exc_info=True)

    # اعلان هیجان‌انگیز اما کاملاً ناشناس در گروه
    if group_id:
        try:
            bot.send_message(
                group_id,
                "💘🎉 <b>یک جفت در بلایند دیت این گروه با هم مچ شدند (It's a Match!) و تصمیم گرفتند آشنایی را ادامه دهند!</b> 🔥",
                parse_mode='HTML'
            )
        except Exception:
            pass

# ==========================================
# 🌟 سیستم جستجوی آیدی و تعاملات پیشرفته
# ==========================================
@bot.message_handler(func=lambda message: message.text and message.text.startswith('/user_'))
def handle_profile_search(message):
    user_id = message.from_user.id
    update_last_seen(user_id)
    
    # استخراج آیدی از کاماند
    try:
        prof_id = message.text.split('_')[1]
    except:
        return
        
    target_row = get_user_by_profile_id(prof_id)
    
    if not target_row:
        bot.send_message(user_id, "❌ کاربری با این آیدی یافت نشد!")
        return
        
    target_id = target_row[0]
    
    if target_id == user_id:
        bot.send_message(user_id, "⚠️ این آیدی پروفایل خودتونه!")
        return
        
    if are_users_blocked_mutually(user_id, target_id):
        bot.send_message(user_id, "❌ ارتباط شما با این کاربر مسدود شده است.")
        return

    # 🌟 استخراج تمام اطلاعات کاربر هدف (دقیقاً مشابه پروفایل اصلی)
    display_name = escape_html(target_row[4] or "ناشناس")
    age = escape_html(str(target_row[6]) if target_row[6] else "نامشخص")
    edu = escape_html(target_row[7] or "نامشخص")
    loc = escape_html(target_row[8] or "نامشخص")
    gender_key = target_row[9]
    gender_fa = GENDER_MAP.get(gender_key, (gender_key, 'O'))[0] if gender_key else "نامشخص"
    karma = target_row[11] if target_row[11] is not None else 0  
    photo_id = target_row[13]
    bio = escape_html(target_row[14] or "بیوگرافی ثبت نشده است.")
    
    pos_key = get_user_position(target_id)
    pos_fa = POS_MAP.get(pos_key, "نامشخص")
    
    # 🌟 دریافت آمار لایک و دیس‌لایک
    likes, dislikes = get_user_vote_stats(target_id)
    satisfaction_text = get_satisfaction_display(likes, dislikes)
    is_friend = are_users_friends(user_id, target_id)
    friend_badge = "\n🤝 <b>وضعیت دوستی:</b> در لیست دوستان شما ✅" if is_friend else ""
    
    text = (
        f"🔍 <b>پروفایل یافت شد:</b>\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
        f"👤 <b>نام:</b> {display_name}\n"
        f"🔹 <b>سن:</b> {age} سال  |  <b>جنسیت:</b> {gender_fa}\n"
        f"🔹 <b>پوزیشن:</b> {pos_fa}  |  <b>تحصیلات:</b> {edu}\n"
        f"📍 <b>موقعیت:</b> {loc}\n"
        f"✨ <b>کاریزما:</b> {karma}  |  📊 <b>رضایت:</b> {satisfaction_text}"
        f"{friend_badge}\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
        f"❞ <b>درباره من و توضیحات:</b>\n"
        f"{bio}\n\n"
        f"👇 برای ارتباط با این کاربر از گزینه‌های زیر استفاده کنید:"
    )

    markup = types.InlineKeyboardMarkup(row_width=2)
    
    # 🌟 دکمه‌های لایک و دیس‌لایک در ردیف اول قرار گرفتند
    markup.add(
        types.InlineKeyboardButton(f"👍 لایک ({likes})", callback_data=f"vote_like_{target_id}"),
        types.InlineKeyboardButton(f"👎 دیس‌لایک ({dislikes})", callback_data=f"vote_dislike_{target_id}")
    )
    if is_friend:
        markup.add(
            types.InlineKeyboardButton("🤝 پنل دوستی (چت و پیام رایگان)", callback_data=f"friend_view_{target_id}_0")
        )
    
    # سایر دکمه‌های ارتباطی
    markup.add(
        types.InlineKeyboardButton("خرید سکه برای کاربر 🎁", callback_data=f"gift_coins_{target_id}")
    )
    # 🌟 آپدیت: تغییر هزینه‌ها به ۵ سکه
    markup.add(
        types.InlineKeyboardButton("پیام دایرکت (۵ سکه) ✉️", callback_data=f"directmsg_{target_id}"),
        types.InlineKeyboardButton("درخواست چت (۵ سکه) 💬", callback_data=f"chatreq_{target_id}")
    )
    markup.add(
        types.InlineKeyboardButton("بلاک کردن کاربر 🔒", callback_data=f"block_user_{target_id}"),
        types.InlineKeyboardButton("گزارش کاربر 🚫", callback_data=f"report_user_{target_id}")
    )

    # سیستم هوشمند ارسال عکس (اختصاصی یا دیفالت)
    _send_profile_card(user_id, text, markup, photo_id=photo_id)

# ----------------- هندلرهای دکمه‌های پروفایل -----------------

@bot.callback_query_handler(func=lambda call: call.data.startswith('directmsg_'))
def process_direct_message_start(call):
    user_id = call.from_user.id
    try:
        target_id = int(call.data.split('_')[1])
    except (IndexError, ValueError):
        bot.answer_callback_query(call.id)
        return

    if target_id == user_id:
        bot.answer_callback_query(call.id, "❌ نمی‌توانید به خودتان پیام بدهید.", show_alert=True)
        return

    if not get_user(target_id) or is_blocked(target_id) or are_users_blocked_mutually(user_id, target_id):
        bot.answer_callback_query(call.id, "❌ امکان ارسال پیام به این کاربر وجود ندارد.", show_alert=True)
        return

    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT COALESCE(coins, 0) FROM users WHERE user_id = ?', (user_id,))
        row = cur.fetchone()
        if not row:
            bot.answer_callback_query(call.id)
            return
        coins = row[0]
        if coins < 5:
            bot.answer_callback_query(call.id, "❌ برای ارسال دایرکت به ۵ سکه نیاز دارید! موجودی شما کافی نیست.", show_alert=True)
            return

    bot.answer_callback_query(call.id)
    msg = bot.send_message(
        user_id,
        "✉️ <b>ارسال پیام دایرکت</b>\n\nلطفاً پیام خود را بنویسید (متن، عکس یا ویدیو).\n"
        "⚠️ <i>به محض ارسال پیام، ۵ سکه از شما کسر خواهد شد.</i>",
        parse_mode='HTML'
    )
    bot.register_next_step_handler(msg, process_send_direct_message, target_id)

DIRECT_CAPTION_TYPES = ('photo', 'video', 'document', 'audio', 'voice', 'animation')

def process_send_direct_message(message, target_id):
    user_id = message.from_user.id
    if message.content_type == 'text' and message.text in ['/cancel', 'بازگشت', '/start']:
        bot.send_message(user_id, "❌ ارسال دایرکت لغو شد.")
        return

    # اعتبارسنجی دوباره (بین کلیک دکمه و ارسال پیام ممکن است وضعیت عوض شده باشد)
    if not get_user(target_id) or is_blocked(target_id) or are_users_blocked_mutually(user_id, target_id):
        bot.send_message(user_id, "❌ امکان ارسال پیام به این کاربر وجود ندارد.")
        return

    # فیلتر لینک (قبل از کسر سکه)
    raw_text = message.text if message.content_type == 'text' else (message.caption or '')
    entities = (message.entities if message.content_type == 'text' else message.caption_entities) or []
    has_link_entity = any(e.type in ('url', 'text_link') for e in entities)
    if BLOCK_LINKS_IN_DIRECT and (contains_link(raw_text) or has_link_entity):
        bot.send_message(user_id, "❌ <b>اخطار امنیتی:</b> ارسال لینک در دایرکت مجاز نیست. سکه‌ای از شما کسر نشد.", parse_mode='HTML')
        return

    # 🌟 کسر اتمیک ۵ سکه
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('UPDATE users SET coins = COALESCE(coins, 0) - 5 WHERE user_id = ? AND COALESCE(coins, 0) >= 5', (user_id,))
        if cur.rowcount == 0:
            bot.send_message(user_id, "❌ موجودی سکه شما برای این عملیات کافی نیست! (احتمالاً همزمان در جای دیگری خرج شده است)")
            return
        conn.commit()

    my_prof_id = get_or_create_profile_id(user_id)
    header = (f"💌 <b>شما یک پیام دایرکت جدید دارید!</b>\n"
              f"از طرف: /user_{my_prof_id}\n\n"
              f"👇 <b>پیام:</b>\n")

    try:
        if message.content_type == 'text':
            # اول برش روی متن خام، بعد escape (محدودیت ۴۰۹۶ تلگرام روی متن نهایی است)
            bot.send_message(target_id, header + escape_html(raw_text[:3500]), parse_mode='HTML')
        elif message.content_type in DIRECT_CAPTION_TYPES:
            caption = header + escape_html(raw_text[:800])   # محدودیت کپشن ۱۰۲۴
            bot.copy_message(chat_id=target_id, from_chat_id=user_id, message_id=message.message_id,
                             caption=caption, parse_mode='HTML')
        else:
            # استیکر، ویدیو‌نوت، لوکیشن، مخاطب و ... کپشن ندارند؛ هدر جدا فرستاده می‌شود
            bot.send_message(target_id, header.rstrip() + " 👇", parse_mode='HTML')
            bot.copy_message(chat_id=target_id, from_chat_id=user_id, message_id=message.message_id)

        bot.send_message(user_id, "✅ پیام شما با موفقیت به دایرکت کاربر ارسال شد و ۵ سکه کسر گردید.")
    except Exception:
        logger.error(f"direct message failed {user_id} -> {target_id}", exc_info=True)
        bot.send_message(user_id, "❌ ارسال پیام ممکن نشد (احتمالاً کاربر ربات را مسدود کرده است). سکه‌های شما بازگردانده شد.")
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('UPDATE users SET coins = COALESCE(coins, 0) + 5 WHERE user_id = ?', (user_id,))
            conn.commit()

# ============================================================================
# سیستم درخواست چت اختصاصی (Chat Request) با کسر اتمیک سکه و گارد سن/کول‌داون
# ============================================================================
@bot.callback_query_handler(func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('chatreq_'))
def process_chat_request_start(call):
    sender_id = call.from_user.id
    try:
        receiver_id = int(call.data.split('_', 1)[1])
    except (IndexError, TypeError, ValueError):
        bot.answer_callback_query(call.id, '❌ درخواست نامعتبر است.', show_alert=True)
        return

    if sender_id == receiver_id:
        bot.answer_callback_query(call.id, '❌ نمی‌توانید به خودتان درخواست چت بدهید.', show_alert=True)
        return

    sender_user = get_user(sender_id)
    receiver_user = get_user(receiver_id)
    if (not sender_user or not receiver_user or is_blocked(sender_id) or is_blocked(receiver_id)
            or are_users_blocked_mutually(sender_id, receiver_id)):
        bot.answer_callback_query(call.id, '❌ ارتباط با این کاربر ممکن نیست.', show_alert=True)
        return

    if not is_profile_complete(sender_id):
        bot.answer_callback_query(call.id, '⚠️ ابتدا پروفایل خود را کامل کنید.', show_alert=True)
        return
    if not is_profile_complete(receiver_id):
        bot.answer_callback_query(call.id, '⚠️ پروفایل مخاطب کامل نیست.', show_alert=True)
        return

    try:
        sender_age = int(sender_user[6])
        receiver_age = int(receiver_user[6])
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '❌ سن پروفایل یکی از کاربران معتبر نیست.', show_alert=True)
        return

    # این جداسازی فقط بر اساس سن ثبت‌شده در پروفایل است، نه احراز سن واقعی.
    if ((sender_age >= 18) != (receiver_age >= 18)):
        bot.answer_callback_query(call.id, '🔒 امکان درخواست چت بین این دو رده سنی وجود ندارد.', show_alert=True)
        return

    with queue_lock:
        if sender_id in active_pairs or receiver_id in active_pairs:
            bot.answer_callback_query(call.id, '⚠️ یکی از دو نفر در حال حاضر در چت است.', show_alert=True)
            return

    now = datetime.now()
    now_str = now.strftime('%Y-%m-%d %H:%M:%S')
    fee_paid = 0

    try:
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('BEGIN IMMEDIATE')
            cur.execute(
                '''SELECT status, last_action_time, COALESCE(fee_paid, 0)
                   FROM chat_requests WHERE sender_id = ? AND receiver_id = ?''',
                (sender_id, receiver_id)
            )
            existing = cur.fetchone()

            if existing and existing[0] == 'accepting':
                conn.rollback()
                bot.answer_callback_query(call.id, '⏳ درخواست شما در حال پردازش است.', show_alert=True)
                return

            if existing and existing[0] == 'pending':
                try:
                    age_seconds = (now - datetime.strptime(existing[1], '%Y-%m-%d %H:%M:%S')).total_seconds()
                except (TypeError, ValueError):
                    age_seconds = CHAT_REQUEST_TTL_SECONDS + 1
                if age_seconds < CHAT_REQUEST_TTL_SECONDS:
                    conn.rollback()
                    bot.answer_callback_query(call.id, '⚠️ یک درخواست فعال برای این کاربر دارید.', show_alert=True)
                    return

            if existing and existing[0] == 'rejected':
                try:
                    rejected_age = (now - datetime.strptime(existing[1], '%Y-%m-%d %H:%M:%S')).total_seconds()
                except (TypeError, ValueError):
                    rejected_age = 0
                if rejected_age < 93600:
                    conn.rollback()
                    bot.answer_callback_query(call.id, '🚫 درخواست قبلی رد شده؛ تا ۲۶ ساعت امکان ارسال مجدد ندارید.', show_alert=True)
                    return

            # اگر درخواست قبلی pending و منقضی بود، هزینه قبلی را پیش از درخواست تازه برگردان.
            if existing and existing[0] == 'pending' and existing[2]:
                cur.execute(
                    'UPDATE users SET coins = COALESCE(coins, 0) + ? WHERE user_id = ?',
                    (CHAT_REQUEST_COST, sender_id)
                )
                if cur.rowcount != 1:
                    conn.rollback()
                    bot.answer_callback_query(call.id, '❌ بازپرداخت درخواست قبلی انجام نشد؛ دوباره تلاش کنید.', show_alert=True)
                    return

            cur.execute('SELECT 1 FROM users WHERE user_id = ?', (sender_id,))
            if not cur.fetchone():
                conn.rollback()
                bot.answer_callback_query(call.id, '❌ حساب کاربری پیدا نشد.', show_alert=True)
                return

            cur.execute(
                '''UPDATE users SET coins = COALESCE(coins, 0) - ?
                   WHERE user_id = ? AND COALESCE(coins, 0) >= ?''',
                (CHAT_REQUEST_COST, sender_id, CHAT_REQUEST_COST)
            )
            if cur.rowcount != 1:
                conn.rollback()
                bot.answer_callback_query(call.id, f'❌ برای درخواست چت {CHAT_REQUEST_COST} سکه نیاز دارید.', show_alert=True)
                return
            fee_paid = 1

            cur.execute(
                '''INSERT INTO chat_requests(sender_id, receiver_id, last_action_time, status, fee_paid)
                   VALUES (?, ?, ?, 'pending', ?)
                   ON CONFLICT(sender_id, receiver_id)
                   DO UPDATE SET last_action_time=excluded.last_action_time,
                                 status='pending', fee_paid=excluded.fee_paid''',
                (sender_id, receiver_id, now_str, fee_paid)
            )
            conn.commit()

    except Exception:
        logger.error('Could not create chat request', exc_info=True)
        bot.answer_callback_query(call.id, '❌ خطا در ثبت درخواست؛ هزینه‌ای نباید کسر شده باشد.', show_alert=True)
        return

    # پیام را جدا از answer_callback_query می‌فرستیم؛ خطای پاسخ Callback نباید ریفاند ایجاد کند.
    try:
        sender_prof_id = get_or_create_profile_id(sender_id)
        if not sender_prof_id:
            raise RuntimeError('sender profile id unavailable')
        markup = types.InlineKeyboardMarkup(row_width=2)
        markup.add(
            types.InlineKeyboardButton('✅ قبول', callback_data=f'req_accept_{sender_id}'),
            types.InlineKeyboardButton('❌ رد', callback_data=f'req_reject_{sender_id}')
        )
        bot.send_message(
            receiver_id,
            f'💬 <b>درخواست چت جدید</b>\n\nکاربر /user_{sender_prof_id} می‌خواهد با شما وارد چت ناشناس شود.',
            reply_markup=markup,
            parse_mode='HTML'
        )
    except Exception:
        logger.error('chat request delivery failed', exc_info=True)
        try:
            changed, refunded = _finish_chat_request(
                sender_id, receiver_id, 'delivery_failed', refund_if_paid=True
            )
        except Exception:
            logger.error('chat request refund/finalization failed', exc_info=True)
            changed, refunded = False, False

        if changed:
            text = '❌ ارسال درخواست ناموفق بود.' + (' هزینه به حساب شما برگشت.' if refunded else ' هزینه‌ای کسر نشده بود.')
        else:
            text = '⚠️ درخواست در مسیر دیگری تغییر وضعیت داده است؛ از پرداخت تکراری جلوگیری شد.'
        bot.answer_callback_query(call.id, text, show_alert=True)
        return

    bot.answer_callback_query(call.id, '✅ درخواست چت ارسال شد.')

# ==========================================
# 🎁 سیستم پیشرفته هدیه و انتقال سکه بین کاربران
# ==========================================
@bot.callback_query_handler(func=lambda call: call.data.startswith('gift_coins_'))
def process_gift_coins_start(call):
    user_id = call.from_user.id
    target_id = int(call.data.split('_')[2]) # استخراج آیدی هدف از gift_coins_12345
    update_last_seen(user_id)
    
    # امنیت: جلوگیری از ارسال هدیه به خود
    if user_id == target_id:
        bot.answer_callback_query(call.id, "❌ شما نمی‌توانید به خودتان سکه هدیه دهید!", show_alert=True)
        return

    # بررسی موجودی فرستنده
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('SELECT coins FROM users WHERE user_id = ?', (user_id,))
        row = cur.fetchone()
        if not row: return
        sender_coins = row[0]

    # اگر کاربر کمتر از ۵ سکه داشته باشد، اجازه ورود به منو را نمی‌دهیم
    if sender_coins < 5:
        bot.answer_callback_query(call.id, "❌ موجودی سکه شما کافی نیست! (حداقل ۵ سکه نیاز است)", show_alert=True)
        return

    bot.answer_callback_query(call.id)
    
    # 🌟 رابط کاربری: کیبورد شیشه‌ای زیبا و سریع برای انتخاب مقدار سکه
    markup = types.InlineKeyboardMarkup(row_width=3)
    markup.add(
        types.InlineKeyboardButton("🎁 5 سکه", callback_data=f"sendgift_5_{target_id}"),
        types.InlineKeyboardButton("🎁 10 سکه", callback_data=f"sendgift_10_{target_id}"),
        types.InlineKeyboardButton("🎁 50 سکه", callback_data=f"sendgift_50_{target_id}")
    )
    markup.add(
        types.InlineKeyboardButton("💎 100 سکه", callback_data=f"sendgift_100_{target_id}"),
        types.InlineKeyboardButton("👑 تمام موجودی", callback_data=f"sendgift_all_{target_id}")
    )
    markup.add(types.InlineKeyboardButton("❌ انصراف", callback_data="cancel_step"))
    
    target_row = get_user(target_id)
    target_name = escape_html(target_row[4] or "کاربر")
    
    text = (
        f"🎁 <b>ارسال هدیه برای {target_name}</b>\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
        f"💰 <b>موجودی فعلی شما:</b> <code>{sender_coins} سکه</code>\n\n"
        f"لطفاً مشخص کنید چه تعداد سکه می‌خواهید از حساب شما کسر و به ایشان هدیه داده شود؟ 👇"
    )
    bot.send_message(user_id, text, reply_markup=markup, parse_mode='HTML')

# ============================================================================
# 16) Gift transfer (انتقال امن و ثبت در دفتر کل)
# ============================================================================
@bot.callback_query_handler(func=lambda call: call.data.startswith('sendgift_'))
def process_send_gift(call):
    user_id = call.from_user.id
    parts = call.data.split('_')
    if len(parts) != 3:
        bot.answer_callback_query(call.id, '❌ درخواست نامعتبر است.', show_alert=True)
        return
    amount_str, target_str = parts[1], parts[2]
    try:
        target_id = int(target_str)
    except ValueError:
        bot.answer_callback_query(call.id, '❌ کاربر نامعتبر است.', show_alert=True)
        return
    if target_id == user_id:
        bot.answer_callback_query(call.id, '❌ نمی‌توانید به خودتان هدیه بدهید.', show_alert=True)
        return

    try:
        amount_requested = int(amount_str) if amount_str != 'all' else None
    except ValueError:
        bot.answer_callback_query(call.id, '❌ مقدار نامعتبر است.', show_alert=True)
        return

    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('BEGIN IMMEDIATE') # 🌟 قفل اتمیک
        cur.execute('SELECT COALESCE(coins, 0) FROM users WHERE user_id = ?', (user_id,))
        sender_row = cur.fetchone()
        cur.execute('SELECT 1 FROM users WHERE user_id = ?', (target_id,))
        target_exists = cur.fetchone() is not None
        if not sender_row or not target_exists:
            conn.rollback()
            bot.answer_callback_query(call.id, '❌ یکی از حساب‌ها یافت نشد.', show_alert=True)
            return

        sender_coins = sender_row[0]
        amount = sender_coins if amount_requested is None else amount_requested
        if amount <= 0:
            conn.rollback()
            bot.answer_callback_query(call.id, '❌ مقدار هدیه نامعتبر است.', show_alert=True)
            return

        cur.execute('''UPDATE users SET coins = coins - ?
                       WHERE user_id = ? AND COALESCE(coins, 0) >= ?''',
                    (amount, user_id, amount))
        if cur.rowcount != 1:
            conn.rollback()
            bot.answer_callback_query(call.id, '❌ موجودی شما کافی نیست.', show_alert=True)
            return

        cur.execute('UPDATE users SET coins = COALESCE(coins, 0) + ? WHERE user_id = ?', (amount, target_id))
        if cur.rowcount != 1:
            conn.rollback()
            bot.answer_callback_query(call.id, '❌ انتقال انجام نشد و مبلغ شما حفظ شد.', show_alert=True)
            return

        # 🌟 ثبت در دفتر کل (Ledger)
        cur.execute('''INSERT INTO coin_transfers(sender_id, receiver_id, amount, created_at)
                       VALUES (?, ?, ?, ?)''',
                    (user_id, target_id, amount, datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
        conn.commit()

    bot.answer_callback_query(call.id, f'✅ {amount} سکه منتقل شد.', show_alert=True)
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
    
    try:
        prof_id = get_or_create_profile_id(user_id)
        bot.send_message(target_id,
                         f'🎉 یک هدیه <b>{amount} سکه‌ای</b> دریافت کردید!\nفرستنده: /user_{prof_id}',
                         parse_mode='HTML')
    except Exception: pass

# ==========================================
# 🎲 سیستم یخ‌شکن (Ice-Breaker) و سوالات تصادفی
# ==========================================
@bot.callback_query_handler(func=lambda call: call.data == 'cancel_icebreaker')
def cancel_icebreaker_callback(call):
    try: bot.delete_message(call.message.chat.id, call.message.message_id)
    except: pass
    bot.answer_callback_query(call.id, "❌ لغو شد.")

@bot.message_handler(func=lambda message: message.text == '🎲 سوال تصادفی')
def trigger_icebreaker(message):
    user_id = message.from_user.id
    update_last_seen(user_id)
    
    if user_id not in active_pairs:
        bot.send_message(user_id, "⚠️ شما در حال حاضر در چتی نیستید.")
        return
        
    text = (
        "🧊 <b>سیستم یخ‌شکن (Ice-Breaker)</b>\n\n"
        "نمیدونی چطوری سر صحبت رو باز کنی؟ یه سوال تصادفی انتخاب کن تا ربات اون رو برای هر دوتاتون بفرسته!\n\n"
        "👇 <b>نوع سوال رو انتخاب کن:</b>"
    )
    
    markup = types.InlineKeyboardMarkup(row_width=1)
    markup.add(
        types.InlineKeyboardButton("💬 سوال معمولی (کسر ۱ سکه)", callback_data="icebreaker_normal"),
        types.InlineKeyboardButton("🔥 سوال +18 (کسر ۲ سکه)", callback_data="icebreaker_18plus"),
        types.InlineKeyboardButton("لغو ❌", callback_data="cancel_icebreaker") 
    )
    
    bot.send_message(user_id, text, reply_markup=markup, parse_mode='HTML')

@bot.callback_query_handler(func=lambda call: call.data.startswith('icebreaker_'))
def process_icebreaker_choice(call):
    user_id = call.from_user.id
    q_type = call.data.split('_')[1]
    
    if user_id not in active_pairs:
        bot.answer_callback_query(call.id, "❌ چت شما پایان یافته است.", show_alert=True)
        try: bot.delete_message(call.message.chat.id, call.message.message_id)
        except Exception: pass
        return
        
    partner_id, chat_id, via_link = active_pairs[user_id]
    
    cost = 1 if q_type == 'normal' else 2
    
    # 🌟 کسر اتمیک
    with get_db_connection() as conn:
        cur = conn.cursor()
        cur.execute('UPDATE users SET coins = COALESCE(coins, 0) - ? WHERE user_id = ? AND COALESCE(coins, 0) >= ?', (cost, user_id, cost))
        if cur.rowcount == 0:
            bot.answer_callback_query(call.id, f"❌ موجودی سکه شما کافی نیست!\nبرای این سوال به {cost} سکه نیاز دارید.", show_alert=True)
            return
        conn.commit()
            
    question_pool = ICEBREAKER_NORMAL if q_type == 'normal' else ICEBREAKER_18PLUS
    selected_question = random.choice(question_pool)
    
    user_row = get_user(user_id)
    display_name = escape_html(user_row[4] or "مخاطب")
    
    q_text = (
        f"🎲 <b>سوال تصادفی از طرف ({display_name}):</b>\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
        f"🗣 <i>{selected_question}</i>\n"
        f"━━━━━━━━━━━━━━━━━━━\n"
        f"💡 حالا هر دو نفر جواب بدید تا بحث شکل بگیره!"
    )
    
    is_secure = chat_id in secure_chats
    
    try:
        msg1 = bot.send_message(user_id, q_text, parse_mode='HTML', protect_content=is_secure)
        msg2 = bot.send_message(partner_id, q_text, parse_mode='HTML', protect_content=is_secure)
        
        if is_secure:
            chat_msg_tracker[chat_id][user_id].append(msg1.message_id)
            chat_msg_tracker[chat_id][partner_id].append(msg2.message_id)
            
        bot.answer_callback_query(call.id, f"✅ سوال ارسال شد! ({cost} سکه کسر گردید)")
        bot.delete_message(call.message.chat.id, call.message.message_id)
    except Exception as e:
        logger.error("Error sending icebreaker", exc_info=True)
        bot.answer_callback_query(call.id, "❌ خطایی رخ داد! احتمالاً کاربر مقابل از ربات خارج شده است.", show_alert=True)
        
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('UPDATE users SET coins = COALESCE(coins, 0) + ? WHERE user_id = ?', (cost, user_id))
            conn.commit()
                
        terminate_chat_session(chat_id, user_id, partner_id, send_feedback=False)
        bot.send_message(user_id, "⚠️ ارتباط با کاربر قطع شد. (سکه‌ی شما بازگشت داده شد)", reply_markup=types.ReplyKeyboardRemove())
        show_main_menu(user_id, user_id in ADMINS)

# ==========================================
# 🛒 سیستم فروشگاه (کارت به کارت + Telegram Stars)
# ==========================================

# --- بخش درگاه Telegram Stars ---
def _parse_stars_payload(payload):
    """payload باید دقیقاً به شکل xtr_<سکه>_<آیدی‌کاربر> باشد. خروجی: (coins, user_id) یا None"""
    try:
        prefix, coins, uid = payload.split('_')
        if prefix != 'xtr':
            return None
        return int(coins), int(uid)
    except (ValueError, AttributeError):
        return None

@bot.callback_query_handler(func=lambda call: bool(getattr(call, 'data', None)) and call.data.startswith('buy_stars_'))
def process_buy_stars(call):
    """ایجاد فاکتور Stars فقط در گفت‌وگوی خصوصی صاحب دکمه و برای بسته معتبر."""
    user = getattr(call, 'from_user', None)
    user_id = getattr(user, 'id', None)
    message = getattr(call, 'message', None)

    def reject(text):
        try:
            bot.answer_callback_query(call.id, text, show_alert=True)
        except Exception:
            logger.warning("Could not answer buy_stars callback for user=%s", user_id, exc_info=True)

    # callback باید از پیام خصوصی همان کاربر آمده باشد؛ فاکتور Stars در گروه ساخته نشود.
    chat = getattr(message, 'chat', None) if message is not None else None
    if (not isinstance(user_id, int) or user_id <= 0 or chat is None
            or getattr(chat, 'type', None) != 'private'
            or getattr(chat, 'id', None) != user_id):
        reject("این خرید فقط از فروشگاه داخل گفت‌وگوی خصوصی ربات امکان‌پذیر است.")
        return

    # ساختار callback دقیقاً buy_stars_<coins> باشد.
    parts = (getattr(call, 'data', None) or '').split('_')
    if (len(parts) != 3 or parts[0] != 'buy' or parts[1] != 'stars'
            or not parts[2].isdigit()):
        reject("❌ اطلاعات بسته نامعتبر است. دوباره از فروشگاه انتخاب کنید.")
        return

    try:
        amount = int(parts[2])
    except (TypeError, ValueError):
        reject("❌ بسته نامعتبر است.")
        return

    price_in_stars = PACKAGES_STARS.get(amount)
    if not isinstance(price_in_stars, int) or isinstance(price_in_stars, bool) or price_in_stars <= 0:
        reject("❌ بسته انتخاب‌شده معتبر نیست.")
        return

    try:
        if is_blocked(user_id) or get_user(user_id) is None:
            reject("امکان خرید برای این حساب وجود ندارد.")
            return
    except Exception:
        logger.error("Could not validate Stars buyer user=%s", user_id, exc_info=True)
        reject("در بررسی حساب شما خطایی رخ داد. لطفاً کمی بعد دوباره تلاش کنید.")
        return

    prices = [types.LabeledPrice(label=f"بسته {amount} سکه", amount=price_in_stars)]

    try:
        bot.answer_callback_query(call.id)
    except Exception:
        # شکست پاسخ callback نباید به‌تنهایی مانع بررسی ارسال فاکتور شود.
        logger.warning("Could not acknowledge buy_stars callback user=%s", user_id, exc_info=True)

    try:
        bot.send_invoice(
            user_id,
            title="خرید سکه (فروشگاه)",
            description=f"شارژ خودکار {amount} سکه برای استفاده در امکانات چت ناشناس",
            invoice_payload=f"xtr_{amount}_{user_id}",
            provider_token="",
            currency="XTR",
            prices=prices
        )
    except Exception:
        logger.error("Could not create Stars invoice user=%s amount=%s", user_id, amount, exc_info=True)
        try:
            bot.send_message(user_id, "❌ ساخت فاکتور پرداخت انجام نشد. مبلغی از حساب شما کسر نشده است؛ لطفاً دوباره از فروشگاه تلاش کنید.")
        except Exception:
            logger.error("Could not notify Stars invoice failure user=%s", user_id, exc_info=True)
        return

    # فقط پس از ایجاد موفق فاکتور، پیام قبلی فروشگاه پاک می‌شود.
    try:
        bot.delete_message(user_id, message.message_id)
    except Exception:
        logger.debug("Could not delete previous Stars store message user=%s", user_id, exc_info=True)

@bot.pre_checkout_query_handler(func=lambda query: True)
def checkout_handler(pre_checkout_query):
    """اعتبارسنجی نهایی درخواست پیش‌پرداخت Telegram Stars قبل از تأیید."""
    q = pre_checkout_query
    answered = False

    def answer_once(ok, error_message=None):
        nonlocal answered
        if answered:
            return

        query_id = getattr(q, 'id', None)
        if not query_id:
            logger.error("[STARS] pre_checkout query has no id; cannot answer Telegram")
            answered = True
            return

        # پیش از درخواست شبکه علامت می‌زنیم تا در صورت timeout پاسخ تکراری ارسال نشود.
        answered = True
        try:
            if ok:
                bot.answer_pre_checkout_query(query_id, ok=True)
            else:
                bot.answer_pre_checkout_query(
                    query_id,
                    ok=False,
                    error_message=(error_message or "فاکتور نامعتبر است؛ لطفاً دوباره خرید کنید.")[:200]
                )
        except Exception:
            logger.error(
                "[STARS] answer_pre_checkout_query failed (ok=%s)",
                ok,
                exc_info=True
            )

    def reject(message):
        answer_once(False, message)

    try:
        payer = getattr(q, 'from_user', None)
        payer_id = getattr(payer, 'id', None)
        payload = getattr(q, 'invoice_payload', None)

        if not isinstance(payer_id, int) or isinstance(payer_id, bool) or payer_id <= 0:
            logger.warning("[STARS] pre_checkout rejected: invalid payer id")
            return reject("حساب خریدار معتبر نیست. لطفاً دوباره تلاش کنید.")

        if getattr(payer, 'is_bot', False):
            return reject("پرداخت فقط از حساب کاربری عادی امکان‌پذیر است.")

        # payload باید دقیقاً قالب تولیدشده توسط خود ربات را داشته باشد.
        if not isinstance(payload, str) or not payload or len(payload) > 128 or not payload.isascii():
            return reject("فاکتور نامعتبر است. لطفاً دوباره از فروشگاه خرید کنید.")

        parsed = _parse_stars_payload(payload)
        if not parsed:
            return reject("فاکتور نامعتبر است. لطفاً دوباره از فروشگاه خرید کنید.")

        coins, payload_uid = parsed
        if not isinstance(coins, int) or isinstance(coins, bool) or coins <= 0:
            return reject("بستهٔ انتخابی معتبر نیست.")
        if not isinstance(payload_uid, int) or isinstance(payload_uid, bool) or payload_uid <= 0:
            return reject("شناسهٔ خریدار در فاکتور معتبر نیست.")
        if payload != f"xtr_{coins}_{payload_uid}":
            return reject("ساختار فاکتور معتبر نیست. لطفاً خرید را از فروشگاه شروع کنید.")

        expected_stars = PACKAGES_STARS.get(coins)
        currency = getattr(q, 'currency', None)
        total_amount = getattr(q, 'total_amount', None)
        if (
            expected_stars is None
            or not isinstance(expected_stars, int)
            or expected_stars <= 0
            or currency != 'XTR'
            or not isinstance(total_amount, int)
            or isinstance(total_amount, bool)
            or total_amount != expected_stars
        ):
            logger.warning(
                "[STARS] pre_checkout amount mismatch: payer=%s currency=%r total=%r coins=%r",
                payer_id, currency, total_amount, coins
            )
            return reject("مبلغ فاکتور با بستهٔ انتخابی مطابقت ندارد.")

        if payload_uid != payer_id:
            logger.warning(
                "[STARS] pre_checkout owner mismatch: payer=%s payload_uid=%s",
                payer_id, payload_uid
            )
            return reject("این فاکتور برای حساب دیگری صادر شده است.")

        # وضعیت مسدودی و وجود حساب از دیتابیس خوانده می‌شود، نه فقط از کش حافظه.
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('SELECT 1 FROM users WHERE user_id = ? LIMIT 1', (payer_id,))
            user_exists = cur.fetchone() is not None
            if not user_exists:
                return reject("ابتدا پروفایل خود را در ربات بسازید.")

            cur.execute('SELECT 1 FROM blocked_users WHERE user_id = ? LIMIT 1', (payer_id,))
            user_blocked = cur.fetchone() is not None
            if user_blocked:
                return reject("امکان خرید برای این حساب وجود ندارد.")

        answer_once(True)

    except Exception:
        logger.error("checkout_handler crashed", exc_info=True)
        reject("در بررسی فاکتور مشکلی رخ داد؛ لطفاً دوباره تلاش کنید.")

@bot.message_handler(content_types=['successful_payment'])
def got_stars_payment(message):
    """ثبت idempotent پرداخت Stars و شارژ سکه در همان تراکنش SQLite."""
    sender = getattr(message, 'from_user', None)
    if sender is None or not getattr(sender, 'id', None):
        logger.critical('[STARS] successful_payment arrived without a valid sender')
        try:
            notify_admins('⚠️ <b>پرداخت Stars بدون شناسه کاربر دریافت شد.</b> بررسی دستی لازم است؛ اطلاعات کافی برای Refund خودکار موجود نیست.')
        except Exception:
            logger.error('[STARS] could not notify admins about sender-less payment', exc_info=True)
        return

    try:
        paid_by = int(sender.id)
        if paid_by <= 0:
            raise ValueError('non-positive Telegram user id')
    except (TypeError, ValueError):
        logger.error('[STARS] successful_payment has invalid sender id: %r', getattr(sender, 'id', None))
        try:
            notify_admins('⚠️ <b>پرداخت Stars با شناسه کاربر نامعتبر دریافت شد.</b> بررسی دستی لازم است.')
        except Exception:
            logger.error('[STARS] admin notification failed for invalid sender id', exc_info=True)
        return

    sp = getattr(message, 'successful_payment', None)
    if sp is None:
        logger.error('[STARS] successful_payment handler called without payment payload user=%s', paid_by)
        return

    chat = getattr(message, 'chat', None)
    is_private_chat = (
        chat is not None
        and getattr(chat, 'type', None) == 'private'
        and getattr(chat, 'id', None) == paid_by
    )

    raw_charge_id = getattr(sp, 'telegram_payment_charge_id', None)
    charge_id = str(raw_charge_id).strip() if raw_charge_id is not None else ''
    if not charge_id:
        charge_id = None

    invoice_payload = getattr(sp, 'invoice_payload', None)
    currency = getattr(sp, 'currency', None)
    raw_total = getattr(sp, 'total_amount', None)
    try:
        stars_paid = int(raw_total)
        if stars_paid <= 0:
            raise ValueError('non-positive Stars amount')
    except (TypeError, ValueError, OverflowError):
        stars_paid = None

    parsed = _parse_stars_payload(invoice_payload)
    coins = parsed[0] if parsed else None
    payload_user_id = parsed[1] if parsed else None
    expected_stars = PACKAGES_STARS.get(coins) if coins is not None else None

    reasons = []
    if not is_private_chat:
        reasons.append('پرداخت خارج از گفت‌وگوی خصوصی کاربر دریافت شده است.')
    if not charge_id:
        reasons.append('شناسه پرداخت تلگرام (charge_id) وجود ندارد؛ جلوگیری از ثبت تکراری ممکن نیست.')
    if parsed is None or expected_stars is None or coins is None or coins <= 0 or payload_user_id is None or payload_user_id <= 0:
        reasons.append('payload فاکتور با بسته‌های تعریف‌شده مطابقت ندارد.')
    if currency != 'XTR':
        reasons.append('واحد پول پرداخت XTR نیست.')
    if expected_stars is None or stars_paid != expected_stars:
        reasons.append('تعداد Stars پرداخت‌شده با قیمت بسته مطابقت ندارد.')
    if payload_user_id != paid_by:
        reasons.append('شناسه صاحب فاکتور با پرداخت‌کننده مطابقت ندارد.')

    now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    tx_id = None
    duplicate_kind = None
    duplicate_tx_id = None
    final_status = None
    user_exists = False

    try:
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('BEGIN IMMEDIATE')

            # charge_id یکتا است؛ پیش از درج، رویداد تکراری یا متناقض را تشخیص بده.
            existing = None
            if charge_id:
                existing = cur.execute(
                    '''SELECT id, user_id, amount, method, status, stars_paid
                       FROM transactions WHERE charge_id = ? LIMIT 1''',
                    (charge_id,)
                ).fetchone()

            if existing:
                (duplicate_tx_id, existing_user_id, existing_amount,
                 existing_method, existing_status, existing_stars) = existing
                same_payment = (
                    existing_method == 'stars'
                    and int(existing_user_id or 0) == paid_by
                    and (None if existing_stars is None else int(existing_stars)) == stars_paid
                )
                if same_payment and existing_status == 'paid' and not reasons and int(existing_amount or 0) == coins:
                    duplicate_kind = 'already_paid'
                elif same_payment and existing_status == 'invalid' and reasons:
                    duplicate_kind = 'already_invalid'
                else:
                    duplicate_kind = 'charge_id_conflict'
                conn.rollback()
            else:
                user_row = cur.execute(
                    'SELECT 1 FROM users WHERE user_id = ? LIMIT 1',
                    (paid_by,)
                ).fetchone()
                user_exists = user_row is not None
                if not user_exists:
                    reasons.append('حساب کاربر در جدول users پیدا نشد.')

                final_status = 'paid' if not reasons else 'invalid'
                transaction_coins = coins if final_status == 'paid' else 0

                cur.execute(
                    '''INSERT INTO transactions
                       (user_id, amount, method, status, created_at, charge_id, stars_paid)
                       VALUES (?, ?, 'stars', ?, ?, ?, ?)''',
                    (paid_by, transaction_coins, final_status, now_str, charge_id, stars_paid)
                )
                tx_id = cur.lastrowid

                if final_status == 'paid':
                    cur.execute(
                        'UPDATE users SET coins = COALESCE(coins, 0) + ? WHERE user_id = ?',
                        (coins, paid_by)
                    )
                    if cur.rowcount != 1:
                        # پرداخت ثبت می‌شود اما بدون وجود حساب، اعتبار به کسی داده نمی‌شود.
                        cur.execute(
                            "UPDATE transactions SET status = 'invalid', amount = 0 WHERE id = ? AND status = 'paid'",
                            (tx_id,)
                        )
                        final_status = 'invalid'
                        reasons.append('شارژ کیف پول در دیتابیس انجام نشد؛ نیازمند بررسی یا Refund است.')

                conn.commit()

    except sqlite3.IntegrityError:
        # برخورد هم‌زمان با درج charge_id؛ دوباره اعتبار صادر نکن.
        logger.warning('[STARS] unique charge_id collision user=%s charge_id=%s', paid_by, charge_id, exc_info=True)
        try:
            with get_db_connection() as conn:
                row = conn.execute(
                    '''SELECT id, user_id, amount, method, status, stars_paid
                       FROM transactions WHERE charge_id = ? LIMIT 1''',
                    (charge_id,)
                ).fetchone() if charge_id else None
            if row:
                duplicate_tx_id = row[0]
                duplicate_kind = 'charge_id_conflict'
            else:
                duplicate_kind = 'database_error'
        except Exception:
            logger.error('[STARS] could not inspect duplicate charge_id', exc_info=True)
            duplicate_kind = 'database_error'
    except Exception:
        logger.critical('[STARS] could not persist successful payment user=%s charge_id=%s', paid_by, charge_id, exc_info=True)
        try:
            notify_admins(
                '🚨 <b>پرداخت Stars دریافت شد اما ثبت آن در دیتابیس ناموفق بود.</b>\n'
                f'کاربر: <code>{paid_by}</code>\n'
                f'charge_id: <code>{escape_html(charge_id or "ندارد")}</code>\n'
                'لطفاً قبل از هر اقدام، تراکنش را در پنل تلگرام/دیتابیس بررسی کنید.'
            )
        except Exception:
            logger.error('[STARS] could not notify admins after database failure', exc_info=True)
        try:
            bot.send_message(
                paid_by,
                '⚠️ پرداخت شما به تلگرام رسیده است، اما ثبت آن در ربات با خطا مواجه شد. لطفاً دوباره پرداخت نکنید و برای بررسی با پشتیبانی تماس بگیرید.'
            )
        except Exception:
            logger.error('[STARS] could not notify user after database failure user=%s', paid_by, exc_info=True)
        return

    if duplicate_kind == 'already_paid':
        logger.info('[STARS] duplicate successful_payment ignored; already credited tx=%s user=%s', duplicate_tx_id, paid_by)
        try:
            bot.send_message(
                paid_by,
                f'✅ پرداخت شما قبلاً ثبت و {coins} سکه به کیف پولتان اضافه شده است. اعتبار دوباره‌ای صادر نشد.'
            )
        except Exception:
            logger.error('[STARS] duplicate-payment confirmation failed user=%s', paid_by, exc_info=True)
        return

    if duplicate_kind == 'already_invalid':
        logger.warning('[STARS] duplicate invalid payment update ignored tx=%s user=%s', duplicate_tx_id, paid_by)
        try:
            bot.send_message(
                paid_by,
                '⚠️ این پرداخت قبلاً برای بررسی مدیریت ثبت شده است. لطفاً رسید یا پرداخت دیگری ارسال نکنید.'
            )
        except Exception:
            logger.error('[STARS] invalid duplicate notice failed user=%s', paid_by, exc_info=True)
        return

    if duplicate_kind in ('charge_id_conflict', 'database_error'):
        logger.critical('[STARS] conflicting or unresolved duplicate charge user=%s tx=%s charge_id=%s', paid_by, duplicate_tx_id, charge_id)
        try:
            notify_admins(
                '🚨 <b>تعارض در شناسه پرداخت Stars</b>\n'
                f'کاربر: <code>{paid_by}</code>\n'
                f'charge_id: <code>{escape_html(charge_id or "ندارد")}</code>\n'
                f'تراکنش موجود: <code>{duplicate_tx_id or "نامشخص"}</code>\n'
                'برای جلوگیری از Refund اشتباه، دکمهٔ بازپرداخت خودکار ارائه نشده است؛ بررسی دستی لازم است.'
            )
        except Exception:
            logger.error('[STARS] conflict notification failed', exc_info=True)
        try:
            bot.send_message(paid_by, '⚠️ پرداخت شما با یک شناسهٔ تراکنش ثبت‌شده تداخل دارد. برای جلوگیری از شارژ یا بازپرداخت اشتباه، موضوع به مدیریت ارجاع شد.')
        except Exception:
            logger.error('[STARS] conflict user notification failed user=%s', paid_by, exc_info=True)
        return

    if final_status == 'paid':
        try:
            bot.send_message(
                paid_by,
                f'🎉 <b>پرداخت شما موفقیت‌آمیز بود!</b>\n\nمقدار <b>{coins} سکه</b> به کیف پول شما اضافه شد. ✨',
                parse_mode='HTML'
            )
        except Exception:
            logger.error('[STARS] payment committed but user notification failed user=%s tx=%s', paid_by, tx_id, exc_info=True)
        return

    # پرداخت نامعتبر، charge_id و شناسه کاربر را نگه می‌دارد تا در صورت امکان Refund شود.
    logger.error(
        '[STARS] payment stored as invalid tx=%s user=%s charge_id=%s reasons=%s',
        tx_id, paid_by, charge_id, '; '.join(reasons)
    )
    markup = None
    if tx_id and charge_id:
        markup = types.InlineKeyboardMarkup()
        markup.add(types.InlineKeyboardButton(
            f'↩️ ریفاند این پرداخت (#{tx_id})',
            callback_data=f'sadm_ask_{tx_id}'
        ))
    try:
        notify_admins(
            '⚠️ <b>پرداخت Stars نیازمند بررسی</b>\n'
            f'علت: {escape_html("؛ ".join(reasons) or "وضعیت نامعتبر") }\n'
            f'کاربر: <code>{paid_by}</code>\n'
            f'مبلغ: {escape_html(stars_paid if stars_paid is not None else raw_total)} {escape_html(currency or "نامشخص")}\n'
            f'charge_id: <code>{escape_html(charge_id or "ندارد")}</code>\n'
            'سکه به کیف پول اضافه نشده است؛ در صورت وجود charge_id از دکمهٔ Refund استفاده کنید.',
            markup
        )
    except Exception:
        logger.error('[STARS] could not notify admins about invalid payment tx=%s', tx_id, exc_info=True)

    try:
        bot.send_message(
            paid_by,
            '⚠️ پرداخت شما دریافت شد، اما اطلاعات فاکتور نیازمند بررسی مدیریت است. لطفاً دوباره پرداخت نکنید؛ نتیجه از طرف پشتیبانی اعلام می‌شود.'
        )
    except Exception:
        logger.error('[STARS] invalid-payment user notification failed user=%s tx=%s', paid_by, tx_id, exc_info=True)

STARS_STATUS_LABELS = {
    'paid': '✅ موفق',
    'invalid': '⚠️ نامعتبر',
    'refunding': '⏳ در حال ریفاند',
    'refunded': '↩️ ریفاند شد',
}

def _refund_stars(user_id, charge_id):
    """ریفاند Stars؛ اگر نسخه‌ی کتابخانه متد refund_star_payment را نداشت مستقیم با API صدا می‌زند."""
    if hasattr(bot, 'refund_star_payment'):
        return bot.refund_star_payment(user_id, charge_id)
    return telebot.apihelper._make_request(
        TOKEN, 'refundStarPayment', method='post',
        params={'user_id': user_id, 'telegram_payment_charge_id': charge_id}
    )

def apply_stars_refund_recovery_migration():
    """Add durable retry metadata for interrupted Stars refunds."""
    with get_db_connection() as conn:
        cur = conn.cursor()
        _add_column_if_missing(
            cur, 'transactions', 'refund_attempts', 'INTEGER NOT NULL DEFAULT 0'
        )
        _add_column_if_missing(
            cur, 'transactions', 'refund_retry_at', 'REAL NOT NULL DEFAULT 0'
        )
        _add_column_if_missing(
            cur, 'transactions', 'refund_last_error', 'TEXT'
        )
        cur.execute(
            '''CREATE INDEX IF NOT EXISTS idx_stars_refund_recovery
               ON transactions(method, status, refund_retry_at, id)'''
        )
        conn.commit()

def refund_stars_transaction(tx_id):
    """Refund one Stars transaction idempotently and persist retry state on failure."""
    try:
        tx_id = int(tx_id)
    except (TypeError, ValueError):
        return False, 'شناسه تراکنش نامعتبر است.'

    # This lock coordinates the admin action and recovery worker in this process.
    with stars_refund_lock:
        try:
            with get_db_connection() as conn:
                cur = conn.cursor()
                cur.execute('BEGIN IMMEDIATE')
                row = cur.execute(
                    '''SELECT user_id, amount, status, charge_id, stars_paid,
                              COALESCE(refund_attempts, 0),
                              COALESCE(refund_retry_at, 0)
                       FROM transactions
                       WHERE id = ? AND method = 'stars' ''',
                    (tx_id,)
                ).fetchone()

                if not row:
                    conn.rollback()
                    return False, 'تراکنش پیدا نشد.'

                (user_id, coins, status, charge_id, stars_paid,
                 attempts, retry_at) = row

                if status == 'refunded':
                    conn.rollback()
                    return False, 'این تراکنش قبلاً Refund شده است.'

                if status not in ('paid', 'invalid', 'refunding'):
                    conn.rollback()
                    return False, 'وضعیت این تراکنش قابل Refund نیست.'

                if not charge_id:
                    # Keep the record visible for manual correction, but avoid retrying it every minute.
                    cur.execute(
                        '''UPDATE transactions
                           SET refund_attempts = COALESCE(refund_attempts, 0) + 1,
                               refund_retry_at = ?,
                               refund_last_error = ?
                           WHERE id = ? AND method = 'stars'
                             AND status IN ('paid', 'invalid', 'refunding')''',
                        (time.time() + 86400, 'charge_id is missing', tx_id)
                    )
                    conn.commit()
                    return False, 'این تراکنش charge_id ندارد؛ برای بررسی دستی علامت‌گذاری شد.'

                # Admin-initiated paid/invalid rows are claimed exactly once.
                if status != 'refunding':
                    cur.execute(
                        '''UPDATE transactions
                           SET status = 'refunding',
                               refund_retry_at = 0,
                               refund_last_error = NULL
                           WHERE id = ? AND method = 'stars' AND status = ?''',
                        (tx_id, status)
                    )
                    if cur.rowcount != 1:
                        conn.rollback()
                        return False, 'تراکنش هم‌زمان توسط عملیات دیگری تغییر کرده است.'

                conn.commit()

        except Exception:
            logger.error('Could not prepare Stars refund tx=%s', tx_id, exc_info=True)
            return False, 'خطا در آماده‌سازی Refund؛ درخواست به تلگرام ارسال نشد.'

        # Telegram's refundStarPayment returns True on success. A repeated refund
        # may return CHARGE_ALREADY_REFUNDED, which means the external refund is done.
        telegram_refund_confirmed = False
        already_refunded = False
        api_error_text = None

        try:
            api_result = _refund_stars(user_id, charge_id)
            if api_result is True:
                telegram_refund_confirmed = True
            else:
                api_error_text = f'refundStarPayment returned non-True result: {api_result!r}'

        except Exception as exc:
            desc = getattr(exc, 'description', None) or str(exc)
            normalized_desc = str(desc).lower()
            if (
                'charge_already_refunded' in normalized_desc
                or 'already_refunded' in normalized_desc
                or 'already refunded' in normalized_desc
            ):
                already_refunded = True
                telegram_refund_confirmed = True
            else:
                api_error_text = str(desc)
                logger.warning(
                    'Stars refund API did not confirm tx=%s: %s',
                    tx_id, desc, exc_info=True
                )

        if not telegram_refund_confirmed:
            next_attempt = int(attempts or 0) + 1
            delay_seconds = min(3600, 60 * (2 ** min(next_attempt - 1, 6)))
            retry_at = time.time() + delay_seconds
            try:
                with get_db_connection() as conn:
                    cur = conn.cursor()
                    cur.execute(
                        '''UPDATE transactions
                           SET refund_attempts = ?, refund_retry_at = ?,
                               refund_last_error = ?
                           WHERE id = ? AND method = 'stars' AND status = 'refunding' ''',
                        (next_attempt, retry_at, (api_error_text or 'Unknown refund failure')[:500], tx_id)
                    )
                    conn.commit()
            except Exception:
                logger.critical(
                    'Could not schedule Stars refund retry tx=%s', tx_id,
                    exc_info=True
                )
            return False, (
                'تلگرام هنوز Refund را تأیید نکرده است. درخواست برای تلاش مجدد ذخیره شد.\n'
                f'فاصله تلاش بعدی: {delay_seconds} ثانیه.'
            )

        # Telegram confirmed the refund (or confirmed that it had already happened).
        deducted = 0
        coins_value = int(coins or 0)
        user_id_value = int(user_id)

        try:
            with get_db_connection() as conn:
                cur = conn.cursor()
                cur.execute('BEGIN IMMEDIATE')
                row = cur.execute(
                    '''SELECT user_id, amount, status, stars_paid
                       FROM transactions
                       WHERE id = ? AND method = 'stars' ''',
                    (tx_id,)
                ).fetchone()

                if not row:
                    conn.rollback()
                    return False, 'Refund تلگرام انجام شده، اما رکورد تراکنش پیدا نشد؛ رسیدگی دستی لازم است.'

                user_id_value, coins_value, current_status, stars_paid = row
                if current_status == 'refunded':
                    conn.rollback()
                    return True, f'Refund تراکنش #{tx_id} قبلاً نهایی شده است.'
                if current_status != 'refunding':
                    conn.rollback()
                    return False, 'Refund تلگرام انجام شده، اما وضعیت دیتابیس تغییر کرده است؛ رسیدگی دستی لازم است.'

                coins_value = int(coins_value or 0)
                if coins_value > 0:
                    user_row = cur.execute(
                        'SELECT COALESCE(coins, 0) FROM users WHERE user_id = ?',
                        (user_id_value,)
                    ).fetchone()
                    if not user_row:
                        conn.rollback()
                        return False, 'Refund تلگرام انجام شده، اما حساب کاربر پیدا نشد؛ رسیدگی دستی لازم است.'

                    have = max(0, int(user_row[0] or 0))
                    deducted = min(have, coins_value)
                    if deducted:
                        cur.execute(
                            '''UPDATE users
                               SET coins = MAX(0, COALESCE(coins, 0) - ?)
                               WHERE user_id = ?''',
                            (deducted, user_id_value)
                        )
                        if cur.rowcount != 1:
                            conn.rollback()
                            return False, 'Refund تلگرام انجام شده، اما کسر سکه در دیتابیس نهایی نشد.'

                cur.execute(
                    '''UPDATE transactions
                       SET status = 'refunded', refund_retry_at = 0,
                           refund_last_error = NULL
                       WHERE id = ? AND method = 'stars' AND status = 'refunding' ''',
                    (tx_id,)
                )
                if cur.rowcount != 1:
                    conn.rollback()
                    return False, 'ثبت نهایی Refund ناموفق بود؛ وضعیت برای بازیابی حفظ شد.'

                conn.commit()

        except Exception:
            logger.critical(
                'Stars refund finalization failed tx=%s; recovery will retry finalization',
                tx_id, exc_info=True
            )
            try:
                with get_db_connection() as conn:
                    cur = conn.cursor()
                    cur.execute(
                        '''UPDATE transactions
                           SET refund_retry_at = ?,
                               refund_last_error = ?
                           WHERE id = ? AND method = 'stars' AND status = 'refunding' ''',
                        (time.time() + 60, 'Telegram refund confirmed; database finalization failed', tx_id)
                    )
                    conn.commit()
            except Exception:
                logger.critical('Could not schedule Stars finalization recovery tx=%s', tx_id, exc_info=True)
            return False, 'Refund تلگرام انجام شده، اما ثبت نهایی دیتابیس ناموفق بود؛ بازیابی خودکار دوباره تلاش می‌کند.'

        try:
            extra = f'\n{deducted} سکه از حساب شما کسر شد.' if deducted else ''
            bot.send_message(
                user_id_value,
                f'↩️ پرداخت Stars شما ({stars_paid or "?"}⭐) توسط مدیریت بازگردانده شد.{extra}'
            )
        except Exception:
            pass

        info = (
            f'{deducted} از {coins_value} سکهٔ داده‌شده از حساب کاربر کسر شد.'
            if coins_value > 0 else 'سکه‌ای به این کاربر داده نشده بود.'
        )
        prefix = 'قبلاً در تلگرام Refund شده بود و وضعیت دیتابیس تکمیل شد.' if already_refunded else 'Refund با موفقیت انجام شد.'
        return True, (
            f'{prefix}\nتراکنش #{tx_id} ({stars_paid or "?"}⭐، کاربر {user_id_value}).\n{info}'
        )

def stars_refund_recovery_worker(batch_size=10, interval_seconds=45):
    """Resume due 'refunding' Stars transactions after startup and retry transient failures."""
    batch_size = max(1, min(int(batch_size), 50))
    interval_seconds = max(15, min(int(interval_seconds), 600))

    while True:
        try:
            now_ts = time.time()
            with get_db_connection() as conn:
                rows = conn.execute(
                    '''SELECT id
                       FROM transactions
                       WHERE method = 'stars'
                         AND status = 'refunding'
                         AND COALESCE(refund_retry_at, 0) <= ?
                       ORDER BY id ASC
                       LIMIT ?''',
                    (now_ts, batch_size)
                ).fetchall()

            for (tx_id,) in rows:
                try:
                    ok, result_text = refund_stars_transaction(tx_id)
                    if not ok:
                        logger.warning(
                            'Stars refund recovery deferred tx=%s: %s',
                            tx_id, result_text
                        )
                except Exception:
                    logger.error(
                        'Stars refund recovery failed tx=%s',
                        tx_id, exc_info=True
                    )
                time.sleep(0.5)

        except Exception:
            logger.error('Stars refund recovery worker iteration failed', exc_info=True)

        time.sleep(interval_seconds)

@bot.callback_query_handler(func=lambda call: call.data == 'sadm_list')
@admin_only
def stars_admin_list(call):
    """نمایش فهرست تراکنش‌های Stars فقط در گفت‌وگوی خصوصی ادمین."""
    admin_id = call.from_user.id
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message is not None else None

    # اطلاعات پرداخت و شناسه کاربران نباید در گروه یا کانال نمایش داده شود.
    if chat is None or getattr(chat, 'type', None) != 'private' or getattr(chat, 'id', None) != admin_id:
        try:
            bot.answer_callback_query(
                call.id,
                "⛔ فهرست پرداخت‌ها فقط در گفت‌وگوی خصوصی ربات قابل مشاهده است.",
                show_alert=True
            )
        except Exception:
            logger.warning("Could not answer non-private Stars admin-list callback", exc_info=True)
        return

    try:
        bot.answer_callback_query(call.id)
    except Exception:
        logger.warning("Could not acknowledge Stars admin-list callback", exc_info=True)

    try:
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute(
                "SELECT id, user_id, amount, stars_paid, status, created_at "
                "FROM transactions WHERE method = 'stars' ORDER BY id DESC LIMIT 10"
            )
            rows = cur.fetchall()
    except Exception:
        logger.error("Could not load Stars transactions for admin=%s", admin_id, exc_info=True)
        try:
            bot.send_message(admin_id, "❌ هنگام دریافت فهرست پرداخت‌ها خطایی رخ داد. لطفاً دوباره تلاش کنید.")
        except Exception:
            logger.error("Could not notify admin about Stars-list read failure", exc_info=True)
        return

    lines = ["💫 <b>آخرین پرداخت‌های Stars</b>\n"]
    markup = types.InlineKeyboardMarkup(row_width=1)

    if not rows:
        lines.append("هنوز پرداختی ثبت نشده است.")

    for tx_id, uid, coins, stars, status, created in rows:
        label = STARS_STATUS_LABELS.get(status, escape_html(str(status or 'نامشخص')))
        when = escape_html(str(created or '')[5:16])
        tx_id_text = escape_html(str(tx_id))
        uid_text = escape_html(str(uid))
        stars_text = escape_html(str(stars if stars is not None else '?'))
        coins_text = escape_html(str(coins if coins is not None else 0))

        lines.append(
            f"#{tx_id_text} | 👤 <code>{uid_text}</code> | "
            f"{stars_text}⭐ → {coins_text} سکه | {label} | {when}"
        )

        if status in ('paid', 'invalid'):
            markup.add(
                types.InlineKeyboardButton(
                    f"↩️ ریفاند #{tx_id_text} ({stars_text}⭐)",
                    callback_data=f"sadm_ask_{tx_id}"
                )
            )

    markup.add(types.InlineKeyboardButton("🔙 بازگشت به پنل", callback_data="admin_back"))
    text = "\n".join(lines)

    try:
        bot.edit_message_text(
            text,
            chat_id=admin_id,
            message_id=message.message_id,
            reply_markup=markup,
            parse_mode='HTML'
        )
    except Exception:
        try:
            bot.send_message(admin_id, text, reply_markup=markup, parse_mode='HTML')
        except Exception:
            logger.error("Could not display Stars transactions for admin=%s", admin_id, exc_info=True)

@bot.callback_query_handler(func=lambda call: bool(call.data) and call.data.startswith('sadm_ask_'))
@admin_only
def stars_admin_ask(call):
    """نمایش تأیید ریفاند فقط در گفت‌وگوی خصوصی همان ادمین و برای تراکنش معتبر."""
    admin_id = call.from_user.id
    callback_data = call.data or ''

    def answer(text='', show_alert=False):
        try:
            bot.answer_callback_query(call.id, text, show_alert=show_alert)
        except Exception:
            logger.warning('Could not answer Stars refund confirmation callback', exc_info=True)

    # این عملیات نباید از گروه/کانال یا پیام متعلق به گفت‌وگوی دیگری اجرا شود.
    message = getattr(call, 'message', None)
    message_chat = getattr(message, 'chat', None) if message else None
    if (message_chat is None
            or getattr(message_chat, 'type', None) != 'private'
            or getattr(message_chat, 'id', None) != admin_id):
        answer('⛔ این عملیات فقط از پیام خصوصی خود ادمین قابل انجام است.', show_alert=True)
        return

    # قالب callback باید دقیقاً sadm_ask_<positive integer> باشد.
    parts = callback_data.split('_')
    if len(parts) != 3 or parts[0] != 'sadm' or parts[1] != 'ask' or not parts[2].isdigit():
        answer('❌ درخواست نامعتبر است.', show_alert=True)
        return

    tx_id = int(parts[2])
    if tx_id <= 0:
        answer('❌ شناسه تراکنش نامعتبر است.', show_alert=True)
        return

    try:
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute(
                """SELECT user_id, amount, status, stars_paid
                   FROM transactions
                   WHERE id = ? AND method = 'stars'""",
                (tx_id,)
            )
            row = cur.fetchone()
    except Exception:
        logger.error('Stars refund confirmation lookup failed tx=%s', tx_id, exc_info=True)
        answer('❌ هنگام خواندن اطلاعات تراکنش خطایی رخ داد.', show_alert=True)
        return

    if not row:
        answer('❌ تراکنش پیدا نشد.', show_alert=True)
        return

    try:
        user_id = int(row[0])
        coins = int(row[1] or 0)
        status = str(row[2] or '')
        stars = int(row[3]) if row[3] is not None else None
    except (TypeError, ValueError, IndexError):
        logger.error('Malformed Stars transaction row tx=%s: %r', tx_id, row)
        answer('❌ اطلاعات این تراکنش ناقص یا نامعتبر است.', show_alert=True)
        return

    if user_id <= 0 or coins < 0 or (stars is not None and stars <= 0):
        logger.error('Invalid Stars transaction values tx=%s user=%s coins=%s stars=%s',
                     tx_id, user_id, coins, stars)
        answer('❌ اطلاعات مالی این تراکنش معتبر نیست.', show_alert=True)
        return

    if status not in ('paid', 'invalid'):
        status_label = escape_html(str(STARS_STATUS_LABELS.get(status, status or 'نامشخص')))
        answer(f'این تراکنش در وضعیت «{status_label}» است و قابل ریفاند نیست.', show_alert=True)
        return

    if status == 'paid' and coins <= 0:
        answer('❌ تعداد سکه‌های این تراکنش معتبر نیست؛ بررسی دستی لازم است.', show_alert=True)
        return

    if status == 'invalid' and coins != 0:
        # تراکنش invalid نباید سکه قابل کسر/شارژ داشته باشد؛ از نمایش تأیید خودکار جلوگیری می‌شود.
        logger.error('Invalid Stars transaction has non-zero coins tx=%s coins=%s', tx_id, coins)
        answer('❌ اطلاعات این پرداخت نیازمند بررسی دستی است.', show_alert=True)
        return

    if status == 'paid':
        effect = f'🪙 {coins} سکه‌ای که به کاربر داده شده بود (تا حد موجودی فعلی او) از حسابش کسر می‌شود.'
    else:
        effect = '🪙 برای این پرداخت سکه‌ای ثبت نشده است؛ فقط Stars بازگردانده می‌شود.'

    stars_label = str(stars) if stars is not None else '?'
    markup = types.InlineKeyboardMarkup(row_width=2)
    markup.add(
        types.InlineKeyboardButton('✅ تأیید ریفاند', callback_data=f'sadm_do_{tx_id}'),
        types.InlineKeyboardButton('❌ انصراف', callback_data='sadm_list')
    )
    confirmation_text = (
        '⚠️ <b>تأیید ریفاند Stars</b>\n\n'
        f'تراکنش: <code>#{tx_id}</code>\n'
        f'کاربر: <code>{user_id}</code>\n'
        f'مبلغ: <b>{stars_label}⭐</b>\n'
        f'{effect}\n\n'
        'این عملیات پس از تأیید به تلگرام ارسال می‌شود. وضعیت تراکنش هنگام اجرای مرحلهٔ بعد دوباره بررسی خواهد شد. مطمئنید؟'
    )

    answer()
    try:
        bot.edit_message_text(
            confirmation_text,
            chat_id=admin_id,
            message_id=message.message_id,
            reply_markup=markup,
            parse_mode='HTML'
        )
    except Exception:
        logger.warning('Could not edit Stars refund confirmation message; sending a new one',
                       exc_info=True)
        try:
            bot.send_message(admin_id, confirmation_text, reply_markup=markup, parse_mode='HTML')
        except Exception:
            logger.error('Could not send Stars refund confirmation message to admin=%s',
                         admin_id, exc_info=True)

@bot.callback_query_handler(func=lambda call: bool(call.data) and call.data.startswith('sadm_do_'))
@admin_only
def stars_admin_do(call):
    """اجرای ریفاند Stars فقط از پیام خصوصی ادمین و با شناسه معتبر."""
    admin_id = call.from_user.id
    callback_data = call.data or ''

    # این عملیات نباید از گروه، کانال یا inline message اجرا شود.
    message = getattr(call, 'message', None)
    chat = getattr(message, 'chat', None) if message is not None else None
    if (
        chat is None
        or getattr(chat, 'type', None) != 'private'
        or getattr(chat, 'id', None) != admin_id
    ):
        try:
            bot.answer_callback_query(
                call.id,
                "⛔ اجرای ریفاند فقط از گفت‌وگوی خصوصی ادمین مجاز است.",
                show_alert=True
            )
        except Exception:
            logger.warning('Could not answer non-private Stars refund callback', exc_info=True)
        return

    # قالب مجاز دقیقاً sadm_do_<positive transaction id> است.
    parts = callback_data.split('_')
    if (
        len(parts) != 3
        or parts[0] != 'sadm'
        or parts[1] != 'do'
        or not parts[2].isdigit()
    ):
        try:
            bot.answer_callback_query(call.id, "❌ درخواست ریفاند نامعتبر است.", show_alert=True)
        except Exception:
            logger.warning('Could not answer malformed Stars refund callback', exc_info=True)
        return

    try:
        tx_id = int(parts[2])
    except (TypeError, ValueError, OverflowError):
        tx_id = 0

    if tx_id <= 0:
        try:
            bot.answer_callback_query(call.id, "❌ شناسه تراکنش نامعتبر است.", show_alert=True)
        except Exception:
            logger.warning('Could not answer invalid Stars transaction id', exc_info=True)
        return

    # callback را پیش از عملیات شبکه پاسخ می‌دهیم تا دکمه در حالت انتظار نماند.
    try:
        bot.answer_callback_query(call.id, "⏳ درخواست ریفاند در حال بررسی است...")
    except Exception:
        logger.warning('Could not acknowledge Stars refund callback tx=%s', tx_id, exc_info=True)

    try:
        # تابع اصلی، وضعیت و charge_id را دوباره در دیتابیس بررسی می‌کند
        # و با قفل و وضعیت refunding جلوی اجرای تکراری را می‌گیرد.
        ok, result_text = refund_stars_transaction(tx_id)
        if not isinstance(ok, bool):
            raise TypeError('refund_stars_transaction returned an invalid result flag')
        result_text = str(result_text)
    except Exception:
        logger.critical('stars_admin_do failed unexpectedly for tx=%s admin=%s', tx_id, admin_id, exc_info=True)
        ok = False
        result_text = (
            f'نتیجهٔ ریفاند تراکنش #{tx_id} به‌طور قطعی مشخص نشد. '
            'وضعیت تراکنش را در پنل پرداخت‌ها بررسی کنید و بدون بررسی مجدد، عملیات دیگری انجام ندهید.'
        )

    markup = types.InlineKeyboardMarkup(row_width=1)
    markup.add(
        types.InlineKeyboardButton("📋 لیست پرداخت‌های Stars", callback_data="sadm_list")
    )
    result = ("✅ " if ok else "⚠️ ") + escape_html(result_text)

    # مقصد همیشه شناسه خود ادمین است؛ هرگز پیام نتیجه به گروه ارسال نمی‌شود.
    try:
        bot.edit_message_text(
            result,
            chat_id=admin_id,
            message_id=message.message_id,
            reply_markup=markup,
            parse_mode='HTML'
        )
    except Exception:
        try:
            bot.send_message(
                admin_id,
                result,
                reply_markup=markup,
                parse_mode='HTML'
            )
        except Exception:
            logger.error('Could not deliver Stars refund result tx=%s admin=%s', tx_id, admin_id, exc_info=True)

@bot.callback_query_handler(func=lambda call: call.data == 'store_select_method')
def store_select_method(call):
    """نمایش روش‌های پرداخت فقط در گفت‌وگوی خصوصی و برای صاحب حساب."""
    user = getattr(call, 'from_user', None)
    message = getattr(call, 'message', None)

    if user is None or message is None:
        try:
            bot.answer_callback_query(call.id, "❌ درخواست نامعتبر است.", show_alert=True)
        except Exception:
            logger.warning("Could not answer invalid store_select_method callback", exc_info=True)
        return

    try:
        user_id = int(user.id)
        chat_id = int(message.chat.id)
        chat_type = getattr(message.chat, 'type', None)
    except (AttributeError, TypeError, ValueError):
        try:
            bot.answer_callback_query(call.id, "❌ اطلاعات درخواست معتبر نیست.", show_alert=True)
        except Exception:
            logger.warning("Could not answer malformed store callback", exc_info=True)
        return

    if chat_type != 'private' or chat_id != user_id:
        try:
            bot.answer_callback_query(
                call.id,
                "⛔ فروشگاه فقط در گفت‌وگوی خصوصی خودتان قابل استفاده است.",
                show_alert=True
            )
        except Exception:
            logger.warning("Could not answer non-private store callback", exc_info=True)
        return

    try:
        if is_blocked(user_id) or get_user(user_id) is None:
            bot.answer_callback_query(
                call.id,
                "⛔ امکان استفاده از فروشگاه برای این حساب وجود ندارد.",
                show_alert=True
            )
            return
    except Exception:
        logger.error("Store account validation failed user=%s", user_id, exc_info=True)
        try:
            bot.answer_callback_query(
                call.id,
                "❌ بررسی حساب با خطا روبه‌رو شد. لطفاً دوباره تلاش کنید.",
                show_alert=True
            )
        except Exception:
            logger.warning("Could not answer store validation failure", exc_info=True)
        return

    markup = types.InlineKeyboardMarkup(row_width=1)
    markup.add(
        types.InlineKeyboardButton("💳 پرداخت کارت به کارت", callback_data="store_show_card"),
        types.InlineKeyboardButton("⭐️ پرداخت با ستاره تلگرام (آنی)", callback_data="store_show_stars"),
        types.InlineKeyboardButton("🔙 بازگشت", callback_data="back_to_levelup")
    )
    text = "🛒 <b>فروشگاه سکه</b>\n\nلطفاً متد پرداختی خود را انتخاب کنید:"

    try:
        bot.edit_message_text(
            text,
            chat_id=user_id,
            message_id=message.message_id,
            reply_markup=markup,
            parse_mode='HTML'
        )
    except Exception:
        # اگر پیام قبلی قابل ویرایش نبود، منوی جدید در همان گفت‌وگوی خصوصی فرستاده شود.
        logger.warning("Could not edit store method menu user=%s; sending a new one", user_id, exc_info=True)
        try:
            bot.send_message(user_id, text, reply_markup=markup, parse_mode='HTML')
        except Exception:
            logger.error("Could not send store method menu user=%s", user_id, exc_info=True)
            try:
                bot.answer_callback_query(
                    call.id,
                    "❌ نمایش فروشگاه ممکن نشد. کمی بعد دوباره تلاش کنید.",
                    show_alert=True
                )
            except Exception:
                logger.warning("Could not answer store menu delivery failure", exc_info=True)
            return

    try:
        bot.answer_callback_query(call.id)
    except Exception:
        logger.warning("Could not acknowledge store_select_method callback user=%s", user_id, exc_info=True)

@bot.callback_query_handler(func=lambda call: call.data in ('store_show_card', 'store_show_stars'))
def store_show_packages(call):
    """نمایش بسته‌های فروشگاه فقط در گفت‌وگوی خصوصی صاحب دکمه."""
    user_id = getattr(getattr(call, 'from_user', None), 'id', None)
    message = getattr(call, 'message', None)

    if not isinstance(user_id, int) or user_id <= 0 or message is None:
        try:
            bot.answer_callback_query(call.id, "❌ درخواست نامعتبر است.", show_alert=True)
        except Exception:
            logger.warning("Could not answer invalid store package callback", exc_info=True)
        return

    chat = getattr(message, 'chat', None)
    if (chat is None or getattr(chat, 'type', None) != 'private'
            or getattr(chat, 'id', None) != user_id):
        try:
            bot.answer_callback_query(
                call.id,
                "⛔️ فروشگاه فقط در گفت‌وگوی خصوصی خودتان قابل استفاده است.",
                show_alert=True
            )
        except Exception:
            logger.warning("Could not answer non-private store package callback", exc_info=True)
        return

    try:
        if is_blocked(user_id):
            bot.answer_callback_query(call.id, "⛔️ حساب شما مسدود است.", show_alert=True)
            return
        if not get_user(user_id):
            bot.answer_callback_query(
                call.id,
                "⚠️ ابتدا پروفایل خود را در ربات ایجاد کنید.",
                show_alert=True
            )
            return
    except Exception:
        logger.error("Store package account validation failed user=%s", user_id, exc_info=True)
        try:
            bot.answer_callback_query(call.id, "❌ بررسی حساب انجام نشد؛ دوباره تلاش کنید.", show_alert=True)
        except Exception:
            pass
        return

    method_map = {
        'store_show_card': ('card', PACKAGES_RIAL, 'تومان'),
        'store_show_stars': ('stars', PACKAGES_STARS, 'ستاره'),
    }
    config = method_map.get(getattr(call, 'data', None))
    if config is None:
        try:
            bot.answer_callback_query(call.id, "❌ روش پرداخت نامعتبر است.", show_alert=True)
        except Exception:
            pass
        return

    method, packages, unit = config
    if not isinstance(packages, dict) or not packages:
        logger.error("Store package list is empty or invalid method=%s", method)
        try:
            bot.answer_callback_query(call.id, "❌ در حال حاضر بسته‌ای برای این روش وجود ندارد.", show_alert=True)
        except Exception:
            pass
        return

    try:
        markup = types.InlineKeyboardMarkup(row_width=1)
        for coins, price in packages.items():
            try:
                coins_value = int(coins)
                if coins_value <= 0 or price is None:
                    continue
                if method == 'stars':
                    price_value = int(price)
                    if price_value <= 0:
                        continue
                    label = f"🪙 {coins_value} سکه - ⭐️ {price_value} ستاره"
                else:
                    price_text = str(price).strip()
                    if not price_text:
                        continue
                    label = f"🪙 {coins_value} سکه - {price_text} تومان"
                callback_data = f"buy_{method}_{coins_value}"
                markup.add(types.InlineKeyboardButton(label, callback_data=callback_data))
            except (TypeError, ValueError):
                logger.warning("Skipping invalid store package method=%s coins=%r", method, coins)

        if not getattr(markup, 'keyboard', None):
            bot.answer_callback_query(call.id, "❌ بستهٔ معتبری برای نمایش وجود ندارد.", show_alert=True)
            return

        markup.add(types.InlineKeyboardButton("🔙 بازگشت به روش‌ها", callback_data="store_select_method"))
        display_text = (
            "📦 <b>بسته‌های موجود:</b>\n\n"
            "لطفاً بستهٔ موردنظر خود را انتخاب کنید:"
        )

        bot.answer_callback_query(call.id)
        try:
            bot.edit_message_text(
                display_text,
                chat_id=user_id,
                message_id=message.message_id,
                reply_markup=markup,
                parse_mode='HTML'
            )
        except Exception:
            # در صورت غیرقابل‌ویرایش بودن پیام قبلی، همان منو در یک پیام خصوصی جدید نمایش داده می‌شود.
            bot.send_message(user_id, display_text, reply_markup=markup, parse_mode='HTML')

    except Exception:
        logger.error("store_show_packages failed user=%s method=%s", user_id, method, exc_info=True)
        try:
            bot.answer_callback_query(call.id, "❌ نمایش بسته‌ها با خطا مواجه شد.", show_alert=True)
        except Exception:
            pass

# --- بخش کارت به کارت ---
@bot.callback_query_handler(func=lambda call: bool(call.data) and call.data.startswith('buy_card_'))
def process_buy_card(call):
    """شروع امن خرید کارت‌به‌کارت؛ فقط در گفت‌وگوی خصوصی صاحب دکمه."""
    try:
        user = getattr(call, 'from_user', None)
        message = getattr(call, 'message', None)
        chat = getattr(message, 'chat', None)
        user_id = getattr(user, 'id', None)

        if user_id is None or chat is None or getattr(chat, 'type', None) != 'private' or chat.id != user_id:
            bot.answer_callback_query(
                call.id,
                "⛔️ خرید فقط از گفت‌وگوی خصوصی خودتان امکان‌پذیر است.",
                show_alert=True
            )
            return

        match = re.fullmatch(r'buy_card_([0-9]+)', str(getattr(call, 'data', '') or ''))
        if not match:
            bot.answer_callback_query(call.id, "❌ درخواست خرید نامعتبر است.", show_alert=True)
            return

        amount = int(match.group(1))
        price = PACKAGES_RIAL.get(amount)
        if amount <= 0 or price is None:
            bot.answer_callback_query(call.id, "❌ بستهٔ انتخاب‌شده معتبر نیست.", show_alert=True)
            return

        if is_blocked(user_id):
            bot.answer_callback_query(call.id, "⛔️ امکان خرید برای این حساب وجود ندارد.", show_alert=True)
            return

        if not get_user(user_id):
            bot.answer_callback_query(call.id, "⚠️ ابتدا حساب کاربری خود را در ربات ایجاد کنید.", show_alert=True)
            return

        markup = types.InlineKeyboardMarkup()
        markup.add(types.InlineKeyboardButton("❌ لغو خرید", callback_data="cancel_step"))

        prompt_text = (
            f"💳 <b>خرید کارت به کارت ({amount} سکه)</b>\n\n"
            f"مبلغ قابل پرداخت: <b>{price} تومان</b>\n\n"
            f"💳 <b>شماره کارت جهت واریز:</b>\n"
            f"<code>6219-8619-7689-8824</code>\n"
            f"به نام: <b>سبحان الهایی سحر</b>\n\n"
            f"⚠️ <b>هشدار مهم:</b> لطفاً پس از واریز، <b>عکس واضح رسید بانکی</b> را همینجا ارسال کنید. "
            f"ارسال رسید فیک یا دستکاری شده باعث مسدود شدن دائم حساب کاربری شما خواهد شد!\n\n"
            f"📸 <i>منتظر دریافت عکس رسید هستیم...</i>"
        )

        # ابتدا پیام و مرحلهٔ دریافت رسید ایجاد می‌شود؛ در صورت شکست، منوی قبلی حفظ می‌شود.
        prompt_message = bot.send_message(
            user_id,
            prompt_text,
            reply_markup=markup,
            parse_mode='HTML'
        )
        try:
            bot.register_next_step_handler(prompt_message, process_receipt_photo, amount)
        except Exception:
            logger.error('Could not register card receipt step for user=%s', user_id, exc_info=True)
            try:
                bot.delete_message(user_id, prompt_message.message_id)
            except Exception:
                pass
            bot.answer_callback_query(
                call.id,
                "❌ شروع فرایند خرید ناموفق بود. دوباره تلاش کنید.",
                show_alert=True
            )
            return

        try:
            bot.answer_callback_query(call.id, "✅ مرحلهٔ ارسال رسید آماده شد.")
        except Exception:
            logger.debug('Could not answer buy_card callback', exc_info=True)

        try:
            bot.delete_message(user_id, message.message_id)
        except Exception:
            # حذف منوی قبلی برای ادامهٔ خرید ضروری نیست.
            pass

    except Exception:
        logger.error('process_buy_card failed', exc_info=True)
        try:
            bot.answer_callback_query(
                call.id,
                "❌ در شروع خرید خطایی رخ داد. لطفاً دوباره تلاش کنید.",
                show_alert=True
            )
        except Exception:
            pass

def process_receipt_photo(message, amount):
    """دریافت و ثبت رسید کارت‌به‌کارت فقط در گفتگوی خصوصی خریدار."""
    user = getattr(message, 'from_user', None)
    chat = getattr(message, 'chat', None)
    if user is None or chat is None or not isinstance(getattr(user, 'id', None), int):
        return

    user_id = user.id

    # این مرحله فقط باید در پی‌وی همان شخصی اجرا شود که درخواست خرید را آغاز کرده است.
    if getattr(chat, 'type', None) != 'private' or getattr(chat, 'id', None) != user_id:
        try:
            bot.clear_step_handler_by_chat_id(chat.id)
        except Exception:
            pass
        return

    message_text = (getattr(message, 'text', None) or '').strip()
    command = message_text.split(maxsplit=1)[0].split('@', 1)[0].lower() if message_text else ''
    if command in ('/cancel', '/start') or message_text in ('لغو', 'بازگشت'):
        try:
            bot.clear_step_handler_by_chat_id(user_id)
        except Exception:
            pass
        bot.send_message(user_id, "❌ عملیات خرید لغو شد.")
        show_main_menu(user_id, user_id in ADMINS)
        return

    # مبلغ فقط از بسته‌های رسمی فروشگاه پذیرفته می‌شود؛ مقدار دلخواه از step handler مجاز نیست.
    try:
        if isinstance(amount, bool):
            raise ValueError
        amount = int(amount)
    except (TypeError, ValueError):
        bot.clear_step_handler_by_chat_id(user_id)
        bot.send_message(user_id, "❌ بستهٔ خرید نامعتبر است. لطفاً خرید را دوباره از فروشگاه آغاز کنید.")
        show_main_menu(user_id, user_id in ADMINS)
        return

    if amount not in PACKAGES_RIAL:
        bot.clear_step_handler_by_chat_id(user_id)
        bot.send_message(user_id, "❌ این بسته در فروشگاه وجود ندارد. لطفاً خرید را دوباره آغاز کنید.")
        show_main_menu(user_id, user_id in ADMINS)
        return

    if is_blocked(user_id):
        bot.clear_step_handler_by_chat_id(user_id)
        bot.send_message(user_id, "❌ امکان ثبت خرید برای این حساب وجود ندارد.")
        show_main_menu(user_id, user_id in ADMINS)
        return

    if getattr(message, 'content_type', None) != 'photo' or not getattr(message, 'photo', None):
        bot.clear_step_handler_by_chat_id(user_id)
        bot.send_message(
            user_id,
            "❌ لطفاً فقط عکس واضح رسید بانکی را ارسال کنید. عملیات خرید لغو شد.",
            parse_mode='HTML'
        )
        show_main_menu(user_id, user_id in ADMINS)
        return

    photo_id = message.photo[-1].file_id
    if not photo_id:
        bot.clear_step_handler_by_chat_id(user_id)
        bot.send_message(user_id, "❌ دریافت فایل رسید ناموفق بود. لطفاً خرید را دوباره آغاز کنید.")
        show_main_menu(user_id, user_id in ADMINS)
        return

    # وجود حساب و ایجاد تراکنش در یک تراکنش SQLite انجام می‌شود.
    try:
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('BEGIN IMMEDIATE')
            cur.execute('SELECT 1 FROM users WHERE user_id = ?', (user_id,))
            user_exists = cur.fetchone() is not None
            if not user_exists:
                conn.rollback()
            else:
                cur.execute(
                    "INSERT INTO transactions (user_id, amount, method, status, created_at) VALUES (?, ?, ?, ?, ?)",
                    (user_id, amount, 'card', 'pending', datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
                )
                db_id = cur.lastrowid
                conn.commit()
    except Exception:
        logger.error('Could not store card receipt transaction for user=%s', user_id, exc_info=True)
        try:
            bot.clear_step_handler_by_chat_id(user_id)
        except Exception:
            pass
        bot.send_message(user_id, "❌ ثبت رسید در پایگاه داده ناموفق بود. هیچ سفارش جدیدی ثبت نشد؛ لطفاً بعداً دوباره تلاش کنید.")
        show_main_menu(user_id, user_id in ADMINS)
        return

    if not user_exists:
        bot.clear_step_handler_by_chat_id(user_id)
        bot.send_message(user_id, "❌ حساب کاربری شما پیدا نشد؛ رسید ثبت نشد.")
        show_main_menu(user_id, user_id in ADMINS)
        return

    markup = types.InlineKeyboardMarkup(row_width=2)
    markup.add(
        types.InlineKeyboardButton("✅ تایید و واریز", callback_data=f"adm_c2c_app_{db_id}"),
        types.InlineKeyboardButton("❌ رد رسید", callback_data=f"adm_c2c_rej_{db_id}")
    )

    admin_msg = (
        f"🧾 <b>رسید پرداخت جدید (کارت به کارت)</b>\n\n"
        f"👤 آیدی کاربر: <code>{user_id}</code>\n"
        f"📦 بسته درخواستی: {amount} سکه\n"
        f"💰 مبلغ فاکتور: {PACKAGES_RIAL[amount]} تومان\n\n"
        f"شناسه تراکنش: <code>{db_id}</code>\n"
        f"لطفاً صحت رسید را بررسی کنید:"
    )

    sent_admin_messages = {}
    for admin_id in ADMINS:
        try:
            sent_msg = bot.send_photo(
                admin_id,
                photo_id,
                caption=admin_msg,
                reply_markup=markup,
                parse_mode='HTML'
            )
            sent_admin_messages[admin_id] = sent_msg.message_id
        except Exception:
            logger.warning(
                'Could not forward card receipt tx=%s to admin=%s',
                db_id,
                admin_id,
                exc_info=True
            )

    admin_receipt_messages[db_id] = {
        'timestamp': time.time(),
        'msgs': sent_admin_messages
    }

    if sent_admin_messages:
        bot.send_message(
            user_id,
            "✅ رسید شما ثبت و برای مدیریت ارسال شد. پس از بررسی و تأیید ادمین، سکه‌ها به حساب شما افزوده می‌شوند."
        )
    else:
        logger.error('Card receipt transaction #%s is pending but could not be delivered to any admin', db_id)
        notify_admins(
            f"⚠️ <b>رسید کارت‌به‌کارت برای ارسال عکس به ادمین‌ها ناموفق بود.</b>\n"
            f"تراکنش: <code>{db_id}</code>\n"
            f"کاربر: <code>{user_id}</code>\n"
            f"بسته: {amount} سکه | مبلغ: {PACKAGES_RIAL[amount]} تومان\n"
            "این تراکنش در وضعیت pending باقی مانده و نیازمند بررسی است."
        )
        bot.send_message(
            user_id,
            f"⚠️ رسید شما با شناسهٔ سفارش #{db_id} در ربات ثبت شد، اما ارسال آن برای ادمین‌ها موفق نبود. لطفاً فعلاً رسید را دوباره ارسال نکنید و برای پیگیری به پشتیبانی پیام دهید."
        )

    try:
        bot.clear_step_handler_by_chat_id(user_id)
    except Exception:
        pass
    show_main_menu(user_id, user_id in ADMINS)

@bot.callback_query_handler(func=lambda call: bool(call.data) and call.data.startswith('adm_c2c_'))
@admin_only
def handle_c2c_admin_action(call):
    """رسید کارت‌به‌کارت را فقط یک‌بار و در تراکنش اتمیک بررسی می‌کند."""
    admin_id = call.from_user.id
    message = getattr(call, 'message', None)

    # این دکمه‌ها فقط در پیام خصوصیِ تصویریِ رسید که خود ربات برای ادمین فرستاده معتبرند.
    if (
        message is None
        or getattr(getattr(message, 'chat', None), 'type', None) != 'private'
        or getattr(getattr(message, 'chat', None), 'id', None) != admin_id
        or not getattr(getattr(message, 'from_user', None), 'is_bot', False)
        or getattr(message, 'content_type', None) != 'photo'
    ):
        bot.answer_callback_query(
            call.id,
            '🚫 این دکمه فقط از پیام خصوصی رسید ارسالی ربات قابل استفاده است.',
            show_alert=True
        )
        return

    # قالب callback دقیقاً adm_c2c_app_<id> یا adm_c2c_rej_<id> است.
    parts = (call.data or '').split('_')
    if len(parts) != 4 or parts[:2] != ['adm', 'c2c'] or parts[2] not in ('app', 'rej'):
        bot.answer_callback_query(call.id, '❌ درخواست بررسی رسید نامعتبر است.', show_alert=True)
        return

    try:
        db_id = int(parts[3])
        if db_id <= 0:
            raise ValueError
    except (TypeError, ValueError):
        bot.answer_callback_query(call.id, '❌ شناسه رسید نامعتبر است.', show_alert=True)
        return

    # اطمینان از اینکه callback دقیقاً روی دکمه‌ای از همین پیام ربات قرار دارد.
    expected_callback = call.data
    markup = getattr(message, 'reply_markup', None)
    inline_keyboard = getattr(markup, 'inline_keyboard', None) or []
    has_matching_button = any(
        getattr(button, 'callback_data', None) == expected_callback
        for row in inline_keyboard
        for button in row
    )
    if not has_matching_button:
        bot.answer_callback_query(call.id, '🚫 دکمه به این پیام رسید تعلق ندارد یا منقضی شده است.', show_alert=True)
        return

    action = parts[2]
    new_status = 'approved' if action == 'app' else 'rejected'
    target_user_id = None
    amount = None
    outcome = None

    try:
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('BEGIN IMMEDIATE')
            cur.execute(
                '''SELECT user_id, amount, status
                   FROM transactions
                   WHERE id = ? AND method = 'card' ''',
                (db_id,)
            )
            row = cur.fetchone()

            if not row:
                conn.rollback()
                outcome = 'missing_transaction'
            else:
                target_user_id, amount, current_status = row

                if current_status != 'pending':
                    conn.rollback()
                    outcome = 'already_processed'
                elif not isinstance(amount, int) or amount not in PACKAGES_RIAL:
                    conn.rollback()
                    outcome = 'invalid_amount'
                elif action == 'app':
                    cur.execute('SELECT 1 FROM users WHERE user_id = ?', (target_user_id,))
                    if cur.fetchone() is None:
                        conn.rollback()
                        outcome = 'missing_user'
                    else:
                        # تغییر وضعیت و شارژ کیف پول در یک تراکنش واحد انجام می‌شوند.
                        cur.execute(
                            '''UPDATE transactions
                               SET status = 'approved'
                               WHERE id = ? AND method = 'card' AND status = 'pending' ''',
                            (db_id,)
                        )
                        if cur.rowcount != 1:
                            conn.rollback()
                            outcome = 'already_processed'
                        else:
                            cur.execute(
                                '''UPDATE users
                                   SET coins = COALESCE(coins, 0) + ?
                                   WHERE user_id = ?''',
                                (amount, target_user_id)
                            )
                            if cur.rowcount != 1:
                                conn.rollback()
                                outcome = 'missing_user'
                            else:
                                conn.commit()
                                outcome = 'approved'
                else:
                    cur.execute(
                        '''UPDATE transactions
                           SET status = 'rejected'
                           WHERE id = ? AND method = 'card' AND status = 'pending' ''',
                        (db_id,)
                    )
                    if cur.rowcount != 1:
                        conn.rollback()
                        outcome = 'already_processed'
                    else:
                        conn.commit()
                        outcome = 'rejected'
    except Exception:
        logger.error('card receipt admin action failed tx=%s admin=%s', db_id, admin_id, exc_info=True)
        bot.answer_callback_query(call.id, '❌ خطای دیتابیس؛ وضعیت رسید تغییر نکرد.', show_alert=True)
        return

    if outcome == 'missing_transaction':
        bot.answer_callback_query(call.id, '❌ رسید در دیتابیس پیدا نشد.', show_alert=True)
        return
    if outcome == 'already_processed':
        bot.answer_callback_query(call.id, '⚠️ این رسید قبلاً توسط ادمین بررسی شده است.', show_alert=True)
        try:
            bot.edit_message_reply_markup(message.chat.id, message.message_id, reply_markup=None)
        except Exception:
            pass
        return
    if outcome == 'invalid_amount':
        bot.answer_callback_query(call.id, '❌ مبلغ ثبت‌شده با بسته‌های معتبر فروشگاه مطابقت ندارد.', show_alert=True)
        return
    if outcome == 'missing_user':
        bot.answer_callback_query(call.id, '❌ حساب خریدار پیدا نشد؛ رسید برای بررسی دستی باز باقی ماند.', show_alert=True)
        return

    admin_name = escape_html(getattr(call.from_user, 'first_name', None) or 'ادمین')
    if outcome == 'approved':
        bot.answer_callback_query(call.id, '✅ رسید تأیید و سکه‌ها واریز شدند.')
        try:
            bot.send_message(
                target_user_id,
                f'🎉 <b>سفارش شما تأیید شد!</b>\n\nمبلغ <b>{amount} سکه</b> به حساب شما واریز شد. از خرید شما متشکریم 🌹',
                parse_mode='HTML'
            )
        except Exception:
            logger.warning('could not notify buyer after card approval tx=%s', db_id, exc_info=True)
        status_text = f'✅ <b>تأیید شده</b> توسط ادمین [{admin_name}]'
    else:
        bot.answer_callback_query(call.id, '❌ رسید رد شد.')
        try:
            bot.send_message(
                target_user_id,
                '❌ <b>رسید ارسالی شما توسط مدیریت رد شد!</b>\nاگر مبلغ از حساب شما کسر شده یا فکر می‌کنید اشتباهی رخ داده است، لطفاً به پشتیبانی پیام دهید.',
                parse_mode='HTML'
            )
        except Exception:
            logger.warning('could not notify buyer after card rejection tx=%s', db_id, exc_info=True)
        status_text = f'❌ <b>رد شده</b> توسط ادمین [{admin_name}]'

    # نسخه‌های پیام رسید نزد ادمین‌ها را بدون دکمه باقی‌مانده به‌روزرسانی کن.
    receipt_record = admin_receipt_messages.get(db_id)
    receipt_messages = {}
    if isinstance(receipt_record, dict):
        receipt_messages = dict(receipt_record.get('msgs', {}) or {})
    receipt_messages.setdefault(admin_id, message.message_id)

    final_caption = (
        f'🧾 <b>رسید بررسی شده</b>\n\n'
        f'وضعیت: {status_text}\n'
        f'بسته: {amount} سکه\n'
        f'آیدی خریدار: <code>{target_user_id}</code>'
    )
    for receipt_admin_id, receipt_message_id in receipt_messages.items():
        try:
            bot.edit_message_caption(
                caption=final_caption,
                chat_id=receipt_admin_id,
                message_id=receipt_message_id,
                parse_mode='HTML',
                reply_markup=None
            )
        except Exception:
            try:
                bot.edit_message_reply_markup(
                    chat_id=receipt_admin_id,
                    message_id=receipt_message_id,
                    reply_markup=None
                )
            except Exception:
                logger.warning(
                    'could not clear processed card receipt keyboard tx=%s admin=%s',
                    db_id, receipt_admin_id, exc_info=True
                )

    admin_receipt_messages.pop(db_id, None)

# ============================================================================
# Anonymous identity/contact filter — full replacement block
# Replace the existing PHONE_PATTERN / EMAIL_PATTERN / TG_SCHEME_PATTERN
# constants and contains_sensitive_contact() function with this entire block.
# ============================================================================

PHONE_PATTERN = re.compile(r'(?<!\d)(?:\+?\d[\d\s\-()]{8,}\d)(?!\d)')
EMAIL_PATTERN = re.compile(r'(?i)\b[\w.+-]+@[\w-]+\.[\w.-]+\b')
TG_SCHEME_PATTERN = re.compile(
    r'(?i)(tg://|mailto:|https?://|www\.|t\.me/|telegram\.me/|\.com\b|\.ir\b|\.net\b|\.org\b|\.co\b)'
)

# Telegram usernames are 5–32 characters, begin with a letter,
# and contain only letters, digits, and underscores.
TELEGRAM_USERNAME_PATTERN = re.compile(
    r'(?<![A-Za-z0-9_])@[A-Za-z][A-Za-z0-9_]{4,31}(?![A-Za-z0-9_])'
)

IDENTITY_ENTITY_TYPES = frozenset({
    'url',
    'text_link',
    'email',
    'mention',
    'text_mention',
})

def contains_sensitive_contact(text, entities=None):
    """Return True for contact details, Telegram IDs, links, or hidden user links."""
    # Hidden Telegram mentions can contain a user ID without showing a URL in text.
    for entity in entities or ():
        if getattr(entity, 'type', None) in IDENTITY_ENTITY_TYPES:
            return True

    if not text:
        return False

    # Normalize Persian and Arabic digits before checking phone numbers.
    digit_map = str.maketrans(
        '۰۱۲۳۴۵۶۷۸۹٠١٢٣٤٥٦٧٨٩',
        '01234567890123456789'
    )
    normalized = str(text).translate(digit_map).replace('ـ', '')

    return bool(
        TG_SCHEME_PATTERN.search(normalized)
        or EMAIL_PATTERN.search(normalized)
        or PHONE_PATTERN.search(normalized)
        or TELEGRAM_USERNAME_PATTERN.search(normalized)
    )

def _anon_rate_limited(user_id):
    """جلوگیری Thread-Safe از ارسال رگباری پیام و خواباندن سرور"""
    now = time.time()
    with rate_limit_lock:
        q = _anon_message_times[user_id]
        while q and now - q[0] > ANON_MESSAGE_WINDOW:
            q.popleft()
        if len(q) >= ANON_MESSAGE_LIMIT:
            return True
        q.append(now)
        return False

@bot.message_handler(
    func=lambda message: message.chat.type == 'private',
    content_types=[
        'text', 'photo', 'document', 'sticker', 'voice', 'video',
        'video_note', 'audio', 'animation', 'contact', 'location',
        'poll', 'venue'
    ]
)
@_chat_message_serialized
def handle_messages(message):
    """ارسال پیام ناشناس با فیلتر دائمی اطلاعات هویتی و مدیریت امن چت."""
    user = getattr(message, 'from_user', None)
    if user is None:
        return

    user_id = user.id
    update_last_seen(user_id)
    content_type = getattr(message, 'content_type', None)

    if content_type == 'text' and message.text:
        text_value = message.text.strip()

        if text_value in [
            'لغو ارسال ❌', '❌ لغو ارسال', 'لغو ❌', '/cancel', 'بازگشت'
        ]:
            return

        if text_value.startswith('/') and text_value not in ['/start', '/cancel', '/next']:
            return

        if text_value in [
            'شروع چت ناشناس 🎭', 'پروفایل من 👤', 'دوستان من 🤝',
            '🔗 لینک ناشناس من', 'سکه 🪙', 'پشتیبانی 🎧', 'پنل مدیریت 👨‍💻',
            '🎲 سوال تصادفی', 'افزودن به لیست دوستان 🤝'
        ]:
            bot.send_message(user_id, '⚠️ ابتدا از چت خارج شوید.')
            return

    # Poll عمداً در چت ناشناس مسدود است؛ سؤال و گزینه‌ها می‌توانند اطلاعات
    # تماس یا شناسه را حمل کنند و فرمت داخلی Poll نباید از فیلتر متن عبور کند.
    if content_type == 'poll':
        try:
            bot.send_message(
                user_id,
                '🚫 ارسال نظرسنجی در چت ناشناس مجاز نیست. لطفاً سؤال خود را به‌صورت متن معمولی ارسال کنید.'
            )
        except Exception:
            logger.warning('could not notify user about blocked poll user=%s', user_id, exc_info=True)
        return

    pair_data = active_pairs.get(user_id)
    if not pair_data:
        if content_type == 'text' and not (message.text or '').startswith('/'):
            bot.send_message(user_id, '⚠️ شما در حال حاضر در چتی نیستید.')
        return

    partner_id, chat_id, _via_link = pair_data

    # هنگام اجرای Fail-Safe اجازه ارسال پیام جدید نده.
    with _secure_tracker_shutdown_lock:
        shutting_down = chat_id in _secure_tracker_shutdown_chats
    if shutting_down:
        try:
            bot.send_message(
                user_id,
                '⚠️ چت امن در حال پایان است؛ لطفاً دوباره پیام ارسال نکنید.'
            )
        except Exception:
            pass
        return

    # کنترل مجدد سازگاری Pair؛ Decorator نیز این بررسی را انجام می‌دهد.
    partner_pair = active_pairs.get(partner_id)
    if (
        not partner_pair
        or partner_pair[0] != user_id
        or partner_pair[1] != chat_id
    ):
        logger.warning(
            'broken pair invariant user=%s partner=%s chat=%s',
            user_id, partner_id, chat_id
        )
        terminate_chat_session(chat_id, user_id, partner_id, send_feedback=False)
        try:
            bot.send_message(
                user_id,
                '⚠️ وضعیت چت ناسازگار بود و برای حفظ امنیت اتصال پاک شد.'
            )
        except Exception:
            pass
        return

    # آپدیتی که قبل از شروع این Pair ارسال شده، نباید به مخاطب جدید منتقل شود.
    try:
        message_date = getattr(message, 'date', None)
        pair_start_time = chat_start_times.get(chat_id)
        if (
            message_date is not None
            and pair_start_time is not None
            and float(message_date) < float(pair_start_time) - 1
        ):
            logger.info(
                'dropped stale pre-chat update user=%s chat=%s message=%s',
                user_id, chat_id, getattr(message, 'message_id', None)
            )
            return
    except (TypeError, ValueError):
        logger.warning('could not validate message timestamp user=%s chat=%s', user_id, chat_id)

    if _anon_rate_limited(user_id):
        try:
            bot.send_message(
                user_id,
                '🚫 سرعت ارسال شما بیش از حد مجاز است. کمی صبر کنید.'
            )
        except Exception:
            pass
        return

    # اطلاعات تماس ساختاریافته و موقعیت مکانی هرگز رله نمی‌شوند.
    if content_type in ('contact', 'location', 'venue'):
        try:
            bot.send_message(
                user_id,
                '🚫 ارسال مخاطب، شماره تماس و موقعیت مکانی در چت ناشناس مجاز نیست.'
            )
        except Exception:
            pass
        return

    out_text = getattr(message, 'text', None)
    out_caption = getattr(message, 'caption', None)
    raw_check_text = out_text if out_text else (out_caption or '')

    if content_type == 'text':
        entities = getattr(message, 'entities', None) or []
    else:
        entities = getattr(message, 'caption_entities', None) or []

    # هرگونه URL، ایمیل، شماره، منشن یا ارجاع مستقیم به کاربر مسدود می‌شود.
    identity_entity_types = {
        'url', 'text_link', 'email', 'phone_number', 'mention', 'text_mention'
    }
    has_identity_entity = any(
        getattr(entity, 'type', None) in identity_entity_types
        for entity in entities
    )

    # نام کاربری تلگرام حداقل ۵ و حداکثر ۳۲ نویسه دارد.
    # برخلاف نسخه قبل، این کنترل در تمام مدت چت فعال است، نه فقط ۶۰ ثانیه اول.
    username_pattern = re.compile(
        r'(?<![A-Za-z0-9_])@[A-Za-z0-9_]{5,32}(?![A-Za-z0-9_])'
    )
    contains_username = bool(username_pattern.search(str(raw_check_text or '')))

    if has_identity_entity or contains_username or contains_sensitive_contact(raw_check_text):
        try:
            bot.send_message(
                user_id,
                '🚫 ارسال آیدی، منشن، لینک، ایمیل یا شماره تماس در چت ناشناس مجاز نیست.'
            )
        except Exception:
            pass
        return

    is_secure = chat_id in secure_chats
    chat_last_activity[chat_id] = time.time()

    # نگاشت Reply فقط بعد از بررسی Pair خوانده می‌شود.
    reply_to = None
    if getattr(message, 'reply_to_message', None):
        with mapping_lock:
            reply_to = message_mapping.get(user_id, {}).get(
                message.reply_to_message.message_id
            )

    # در حالت امن، عکس و ویدئو ابتدا وارد انتخاب زمان حذف می‌شوند.
    if is_secure and content_type in ('photo', 'video'):
        unique_key = f'{user_id}_{message.message_id}'
        created_at = time.time()

        with pending_secure_media_lock:
            pending_secure_media[unique_key] = (
                message, created_at, None, chat_id, partner_id, False
            )

        try:
            bot.delete_message(user_id, message.message_id)
        except Exception:
            logger.warning(
                'could not delete original timed-media prompt message user=%s message=%s',
                user_id, message.message_id,
                exc_info=True
            )

        markup = types.InlineKeyboardMarkup()
        markup.add(
            types.InlineKeyboardButton(
                '3 ثانیه',
                callback_data=f'secmedia_3_{user_id}_{message.message_id}_{partner_id}_{chat_id}'
            ),
            types.InlineKeyboardButton(
                '10 ثانیه',
                callback_data=f'secmedia_10_{user_id}_{message.message_id}_{partner_id}_{chat_id}'
            ),
            types.InlineKeyboardButton(
                '30 ثانیه',
                callback_data=f'secmedia_30_{user_id}_{message.message_id}_{partner_id}_{chat_id}'
            )
        )

        try:
            prompt = bot.send_message(
                user_id,
                '⏳ زمان نمایش و حذف خودکار را انتخاب کنید:',
                reply_markup=markup
            )
            with pending_secure_media_lock:
                current = pending_secure_media.get(unique_key)
                if current and current[3] == chat_id and current[4] == partner_id:
                    pending_secure_media[unique_key] = (
                        message, created_at, prompt.message_id,
                        chat_id, partner_id, False
                    )
        except Exception:
            with pending_secure_media_lock:
                pending_secure_media.pop(unique_key, None)
            logger.error('could not send timed-media selection prompt user=%s', user_id, exc_info=True)
            try:
                bot.send_message(user_id, '⚠️ نمایش گزینه زمان‌دار ناموفق بود؛ مدیا ارسال نشد.')
            except Exception:
                pass
        return

    caption = out_caption if out_caption else None

    def _relay(reply_message_id):
        """ارسال انواع مجاز پیام؛ Poll و محتوای هویتی پیش‌تر رد شده‌اند."""
        common = {
            'reply_to_message_id': reply_message_id,
            'protect_content': is_secure
        }

        if content_type == 'text':
            return bot.send_message(partner_id, out_text, **common)
        if content_type == 'photo':
            return bot.send_photo(
                partner_id, message.photo[-1].file_id,
                caption=caption, **common
            )
        if content_type == 'document':
            return bot.send_document(
                partner_id, message.document.file_id,
                caption=caption, **common
            )
        if content_type == 'sticker':
            return bot.send_sticker(partner_id, message.sticker.file_id, **common)
        if content_type == 'voice':
            return bot.send_voice(partner_id, message.voice.file_id, **common)
        if content_type == 'video':
            return bot.send_video(
                partner_id, message.video.file_id,
                caption=caption, **common
            )
        if content_type == 'video_note':
            return bot.send_video_note(partner_id, message.video_note.file_id, **common)
        if content_type == 'audio':
            return bot.send_audio(
                partner_id, message.audio.file_id,
                caption=caption, **common
            )
        if content_type == 'animation':
            return bot.send_animation(
                partner_id, message.animation.file_id,
                caption=caption, **common
            )
        raise ValueError(f'unsupported anonymous message content_type: {content_type!r}')

    try:
        # اگر تنها مشکل Reply قدیمی/حذف‌شده باشد، یک بار بدون Reply ارسال می‌کنیم.
        try:
            sent_msg = _relay(reply_to)
        except telebot.apihelper.ApiTelegramException as reply_exc:
            description = (
                getattr(reply_exc, 'description', None) or str(reply_exc)
            ).lower()
            is_reply_error = any(token in description for token in (
                'reply message not found',
                'message to be replied not found',
                'replied message not found',
                'reply_to_message_id'
            ))
            if reply_to is None or getattr(reply_exc, 'error_code', None) != 400 or not is_reply_error:
                raise
            sent_msg = _relay(None)

        if not sent_msg:
            logger.error('Telegram returned empty relay result user=%s partner=%s', user_id, partner_id)
            return

        partner_msg_id = getattr(sent_msg, 'message_id', None)
        if not partner_msg_id:
            logger.error(
                'Telegram returned no message_id for relay user=%s partner=%s',
                user_id, partner_id
            )
            return

        if is_secure:
            try:
                echo_msg = bot.copy_message(
                    chat_id=user_id,
                    from_chat_id=user_id,
                    message_id=message.message_id,
                    protect_content=True
                )
                try:
                    bot.delete_message(user_id, message.message_id)
                except Exception:
                    logger.warning(
                        'could not remove original secure message user=%s message=%s',
                        user_id, message.message_id,
                        exc_info=True
                    )

                with mapping_lock:
                    chat_msg_tracker[chat_id][partner_id].append(partner_msg_id)
                    chat_msg_tracker[chat_id][user_id].append(echo_msg.message_id)
                    message_mapping.setdefault(partner_id, {})[partner_msg_id] = echo_msg.message_id
                    message_mapping.setdefault(user_id, {})[echo_msg.message_id] = partner_msg_id

            except SecureTrackerPersistenceError:
                raise
            except Exception:
                # اگر ساخت Echo شکست خورد، پیام تحویل‌شده به گیرنده همچنان Track شود.
                try:
                    bot.delete_message(user_id, message.message_id)
                except Exception:
                    pass
                with mapping_lock:
                    chat_msg_tracker[chat_id][partner_id].append(partner_msg_id)
                    message_mapping.setdefault(partner_id, {})[partner_msg_id] = None
        else:
            with mapping_lock:
                message_mapping.setdefault(partner_id, {})[partner_msg_id] = message.message_id
                message_mapping.setdefault(user_id, {})[message.message_id] = partner_msg_id

        # محدودسازی حافظه Reply mapping.
        with mapping_lock:
            for uid in (user_id, partner_id):
                user_mapping = message_mapping.get(uid, {})
                while len(user_mapping) > 100:
                    oldest_message_id = next(iter(user_mapping))
                    del user_mapping[oldest_message_id]

    except SecureTrackerPersistenceError:
        logger.critical(
            'secure chat stopped because message tracking failed user=%s chat=%s',
            user_id, chat_id,
            exc_info=True
        )
        try:
            bot.send_message(
                user_id,
                '⚠️ به دلیل خطای ذخیره‌سازی پیام امن، چت برای حفظ حریم خصوصی پایان داده می‌شود.'
            )
        except Exception:
            pass
        return

    except telebot.apihelper.ApiTelegramException as exc:
        description = (
            getattr(exc, 'description', None) or str(exc)
        ).lower()
        error_code = getattr(exc, 'error_code', None)
        logger.error(
            'chat relay API error code=%s %s->%s description=%s',
            error_code, user_id, partner_id, description,
            exc_info=True
        )

        # فقط خطاهایی که واقعاً نشان‌دهنده در دسترس نبودن مخاطب هستند چت را می‌بندند.
        recipient_unavailable = (
            error_code == 403
            or any(token in description for token in (
                'bot was blocked by the user',
                'user is deactivated',
                'chat not found',
                'bot can\'t initiate conversation with a user',
                'user not found'
            ))
        )

        if recipient_unavailable:
            force_end_chat(
                user_id,
                '⚠️ کاربر مقابل در دسترس نیست؛ چت پایان یافت.'
            )
        else:
            try:
                bot.send_message(
                    user_id,
                    '⚠️ ارسال این پیام ناموفق بود؛ چت باز می‌ماند. متن یا فایل را بررسی و دوباره تلاش کنید.'
                )
            except Exception:
                pass

    except Exception:
        logger.error(
            'chat relay unexpected error %s->%s', user_id, partner_id,
            exc_info=True
        )
        try:
            bot.send_message(
                user_id,
                '⚠️ یک خطای موقت در ارسال پیام رخ داد؛ چت قطع نشد.'
            )
        except Exception:
            pass

@bot.callback_query_handler(func=lambda call: call.data.startswith('admin_'))
def handle_admin_actions(call):
    user_id = call.from_user.id
    if user_id not in ADMINS:
        return
    update_last_seen(user_id)
    
    action = call.data.split('_')[1]
    
    if action == 'back':
        bot.answer_callback_query(call.id)
        try: bot.delete_message(call.message.chat.id, call.message.message_id)
        except: pass
        show_admin_panel(call.message.chat.id)
        return
        
    elif action == 'stats':
        stats = get_bot_stats()
        text = (
            "📊 آمار ربات:\n\n"
            f"👥 کاربران کل: {stats['total_users']}\n"
            f"👀 کاربران آنلاین: {stats['online_users']}\n"
            f"💬 چت‌های کل: {stats['total_chats']}\n"
            f"🔛 چت‌های فعال: {stats['active_chats']}"
        )
        markup = types.InlineKeyboardMarkup()
        markup.add(types.InlineKeyboardButton("🔙 بازگشت به پنل", callback_data="admin_back"))
        bot.edit_message_text(text, call.message.chat.id, call.message.message_id, reply_markup=markup)
        
    # ==========================================
    # 🌟 مانیتورینگ زنده صف‌های انتظار (جدید)
    # ==========================================
    elif action == 'queues':
        with queue_lock:
            pool = list(waiting_pool)

        adv_count = sum(1 for i in pool if i['mode'] == 'advanced')
        gen_count = sum(1 for i in pool if i['mode'] == 'general')
        pref_count = sum(1 for i in pool if i['mode'] in GENDER_MAP)
        total_gender = gen_count + pref_count
        top_c = sum(1 for i in pool if i['mode'] == 'pos_top')
        vers_c = sum(1 for i in pool if i['mode'] == 'pos_vers')
        bot_c = sum(1 for i in pool if i['mode'] == 'pos_bottom')
        pos_count = top_c + vers_c + bot_c
        loc_count = sum(1 for i in pool if i['mode'].startswith('nearby_'))
        total_waiting = len(pool)

        text = (
            f"🚦 <b>مانیتورینگ زنده صف‌های انتظار</b>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"👥 <b>کل افراد در صف:</b> <code>{total_waiting} نفر</code>\n\n"
            f"🎯 <b>جستجوی پیشرفته:</b> <code>{adv_count} نفر</code>\n"
            f"🚻 <b>گرایش/شانسی:</b> <code>{total_gender} نفر</code>\n"
            f"📍 <b>جستجوی اطراف:</b> <code>{loc_count} نفر</code>\n"
            f"🎭 <b>پوزیشن:</b> <code>{pos_count} نفر</code>\n"
            f"   └ <i>(تاپ: {top_c} | ورس: {vers_c} | بات: {bot_c})</i>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"<i>آخرین بروزرسانی: {datetime.now().strftime('%H:%M:%S')}</i>"
        )
        
        markup = types.InlineKeyboardMarkup()
        markup.add(types.InlineKeyboardButton("🔄 رفرش لحظه‌ای", callback_data="admin_queues"))
        markup.add(types.InlineKeyboardButton("🔙 بازگشت به پنل", callback_data="admin_back"))
        
        try: bot.edit_message_text(text, call.message.chat.id, call.message.message_id, reply_markup=markup, parse_mode='HTML')
        except Exception: bot.answer_callback_query(call.id, "وضعیت صف تغییری نکرده است ✅")

    elif action == 'online':
        with get_db_connection() as conn:
            cur = conn.cursor()
            five_min_ago = (datetime.now() - timedelta(minutes=5)).strftime("%Y-%m-%d %H:%M:%S")
            cur.execute('SELECT user_id, display_name, username FROM users WHERE last_seen > ? AND last_seen IS NOT NULL ORDER BY last_seen DESC LIMIT 50', (five_min_ago,))
            rows = cur.fetchall()
            
        if not rows:
            text = "⚪️ <b>در حال حاضر هیچ کاربری آنلاین نیست.</b>"
        else:
            text = f"🟢 <b>کاربران آنلاین در ۵ دقیقه اخیر ({len(rows)} نفر):</b>\n━━━━━━━━━━━━━━━━━━━\n"
            for uid, dname, uname in rows:
                safe_name = escape_html(dname or "کاربر ناشناس")
                link = f"@{uname}" if uname else f'<a href="tg://user?id={uid}">{safe_name}</a>'
                
                if uid in active_pairs:
                    status = "🔴 مشغول در چت"
                elif is_user_waiting(uid):
                    status = "🟡 در صف انتظار/جستجو"
                else:
                    status = "🟢 آزاد (در منوی ربات)"
                    
                text += f"👤 {link} | <code>{uid}</code>\n└ <b>وضعیت:</b> {status}\n\n"
                
        markup = types.InlineKeyboardMarkup()
        markup.add(types.InlineKeyboardButton("🔄 به‌روزرسانی لیست", callback_data="admin_online"))
        markup.add(types.InlineKeyboardButton("🔙 بازگشت به پنل", callback_data="admin_back"))
        
        try: bot.edit_message_text(text, call.message.chat.id, call.message.message_id, reply_markup=markup, parse_mode='HTML')
        except: bot.answer_callback_query(call.id, "لیست بروز است ✅")

    elif action == 'users' or action == 'userspage':
        page = 1
        if action == 'userspage':
            page = int(call.data.split('_')[2])
            
        users_per_page = 10
        offset = (page - 1) * users_per_page
        
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('SELECT COUNT(*) FROM users')
            total_users = cur.fetchone()[0]
            cur.execute('SELECT user_id, display_name, username, join_date FROM users ORDER BY join_date DESC LIMIT ? OFFSET ?', (users_per_page, offset))
            users = cur.fetchall()
            
        total_pages = (total_users + users_per_page - 1) // users_per_page
        if total_pages == 0: total_pages = 1
        
        text = f"👥 <b>لیست کاربران</b> (مجموع: {total_users})\n\n"
        for u in users:
            uid, dname, uname, jdate = u
            username_str = f"@{uname}" if uname else dname or "نامشخص"
            text += f"🔹 {username_str} | <code>{uid}</code>\n📅 <i>{jdate}</i>\n\n"
            
        markup = types.InlineKeyboardMarkup(row_width=3)
        
        prev_btn = types.InlineKeyboardButton("⬅️ قبلی", callback_data=f"admin_userspage_{page-1}") if page > 1 else types.InlineKeyboardButton(" ", callback_data="admin_noop")
        page_btn = types.InlineKeyboardButton(f"{page} / {total_pages}", callback_data="admin_noop")
        next_btn = types.InlineKeyboardButton("بعدی ➡️", callback_data=f"admin_userspage_{page+1}") if page < total_pages else types.InlineKeyboardButton(" ", callback_data="admin_noop")
        
        markup.row(prev_btn, page_btn, next_btn)
        markup.add(types.InlineKeyboardButton("🔙 بازگشت به پنل", callback_data="admin_back"))
        
        bot.edit_message_text(text, call.message.chat.id, call.message.message_id, reply_markup=markup, parse_mode='HTML')
        
    elif action == 'noop':
        bot.answer_callback_query(call.id)
        return

    elif action == 'broadcast':
        bot.answer_callback_query(call.id)
        
        # 🌟 استفاده از دکمه شیشه‌ای برای لغو (بدون دستکاری منوی اصلی کاربر)
        markup = types.InlineKeyboardMarkup()
        markup.add(types.InlineKeyboardButton("❌ لغو", callback_data="admin_cancel_step"))
        
        try: bot.delete_message(call.message.chat.id, call.message.message_id)
        except: pass
        
        msg = bot.send_message(
            user_id, 
            "📢 پیام همگانی خود را ارسال کنید (متن، عکس، ویدیو و...):", 
            reply_markup=markup
        )
        bot.register_next_step_handler(msg, process_broadcast)
    
    elif action == 'channels':
        channels = get_required_channels()
        markup = types.InlineKeyboardMarkup()
        if channels:
            text = "کانال‌های اجباری فعلی:\n\n"
            for channel in channels:
                text += f"🔹 {channel}\n"
                markup.add(types.InlineKeyboardButton(
                    f"حذف {channel}",
                    callback_data=f"remove_channel_{channel}"
                ))
        else:
            text = "هیچ کانال اجباری تنظیم نشده است."
        
        markup.add(types.InlineKeyboardButton("➕ افزودن کانال", callback_data="add_channel"))
        markup.add(types.InlineKeyboardButton("🔙 بازگشت به پنل", callback_data="admin_back"))
        bot.edit_message_text(text, call.message.chat.id, call.message.message_id, reply_markup=markup)
    
    elif action == 'block':
        bot.answer_callback_query(call.id)
        
        markup = types.InlineKeyboardMarkup()
        markup.add(types.InlineKeyboardButton("❌ لغو", callback_data="admin_cancel_step"))
        
        try: bot.delete_message(call.message.chat.id, call.message.message_id)
        except: pass
        
        msg = bot.send_message(user_id, "ID کاربر را برای مسدود کردن وارد کنید:", reply_markup=markup)
        bot.register_next_step_handler(msg, process_block)
    
    elif action == 'blocked':
        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('''SELECT u.user_id, u.display_name FROM users u 
                              INNER JOIN blocked_users b ON u.user_id = b.user_id 
                              ORDER BY u.join_date DESC LIMIT 30''')
            rows = cur.fetchall()
            
        markup = types.InlineKeyboardMarkup(row_width=1)
        if rows:
            text = "📋 لیست کاربران مسدود شده:\nبرای رفع مسدودی روی دکمه مربوطه کلیک کنید:"
            for row in rows:
                name = row[1] or 'نامشخص'
                markup.add(types.InlineKeyboardButton(f"🔓 آزادسازی: {name} ({row[0]})", callback_data=f"unblock_user_{row[0]}"))
        else:
            text = "هیچ کاربری مسدود نشده است."
            
        markup.add(types.InlineKeyboardButton("🔙 بازگشت به پنل", callback_data="admin_back"))
        bot.edit_message_text(text, call.message.chat.id, call.message.message_id, reply_markup=markup)

    elif action in ('tickets', 'ticketspage'):
        page = 1
        if action == 'ticketspage':
            try:
                page = max(1, int(call.data.split('_')[2]))
            except (IndexError, TypeError, ValueError):
                page = 1

        per_page = 8
        offset = (page - 1) * per_page

        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute("SELECT COUNT(*) FROM support_tickets WHERE status = 'open'")
            open_count = cur.fetchone()[0]
            cur.execute("SELECT COUNT(*) FROM support_tickets WHERE status = 'answered'")
            answered_count = cur.fetchone()[0]
            cur.execute("SELECT COUNT(*) FROM support_tickets WHERE status = 'closed'")
            closed_count = cur.fetchone()[0]

            cur.execute(
                '''SELECT t.ticket_id, t.user_id, t.category, t.status, t.updated_at, u.display_name
                   FROM support_tickets t
                   LEFT JOIN users u ON t.user_id = u.user_id
                   WHERE t.status IN ('open', 'answered')
                   ORDER BY CASE WHEN t.status = 'open' THEN 0 ELSE 1 END, t.updated_at DESC
                   LIMIT ? OFFSET ?''',
                (per_page, offset)
            )
            tickets = cur.fetchall()

        total_active = open_count + answered_count
        total_pages = max(1, (total_active + per_page - 1) // per_page)

        text = (
            f"🎧 <b>مرکز مدیریت تیکت‌های پشتیبانی</b>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"🟡 <b>در انتظار پاسخ:</b> <code>{open_count}</code> | "
            f"🟢 <b>پاسخ داده شده:</b> <code>{answered_count}</code> | "
            f"🔒 <b>بسته:</b> <code>{closed_count}</code>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
        )
        markup = types.InlineKeyboardMarkup(row_width=1)
        if not tickets:
            text += "✅ <i>در حال حاضر هیچ تیکت باز یا در انتظار پاسخی وجود ندارد!</i>"
        else:
            text += "👇 <i>برای مشاهده پرونده و پاسخ مستقیم، روی تیکت کلیک کنید:</i>"
            for t_id, u_id, cat, st, upd, dname in tickets:
                badge = '🟡 بی‌پاسخ' if st == 'open' else '🟢 پاسخ‌داده‌شده'
                cat_short = SUPPORT_CATEGORIES.get(cat, 'عمومی').split()[0]
                safe_name = (dname or f'User {u_id}')[:16]
                markup.add(
                    types.InlineKeyboardButton(
                        f"{badge} | #T{t_id} - {safe_name} ({cat_short})",
                        callback_data=f"sup_adm_hist_{t_id}"
                    )
                )

        if total_pages > 1:
            prev_btn = (
                types.InlineKeyboardButton("⬅️ قبلی", callback_data=f"admin_ticketspage_{page-1}")
                if page > 1 else types.InlineKeyboardButton(" ", callback_data="admin_noop")
            )
            page_btn = types.InlineKeyboardButton(f"{page} / {total_pages}", callback_data="admin_noop")
            next_btn = (
                types.InlineKeyboardButton("بعدی ➡️", callback_data=f"admin_ticketspage_{page+1}")
                if page < total_pages else types.InlineKeyboardButton(" ", callback_data="admin_noop")
            )
            markup.row(prev_btn, page_btn, next_btn)

        markup.add(types.InlineKeyboardButton("🔄 بروزرسانی لیست", callback_data=f"admin_ticketspage_{page}"))
        markup.add(types.InlineKeyboardButton("🔙 بازگشت به پنل", callback_data="admin_back"))

        try:
            bot.edit_message_text(text, call.message.chat.id, call.message.message_id, reply_markup=markup, parse_mode='HTML')
        except Exception:
            bot.answer_callback_query(call.id, "لیست تیکت‌ها بروز است ✅")
        
@bot.callback_query_handler(func=lambda call: call.data.startswith('unblock_user_'))
@admin_only
def unblock_user_action(call):
    target_id = int(call.data.split('_')[2])
    unblock_user(target_id)
    bot.answer_callback_query(call.id, f"✅ کاربر {target_id} با موفقیت از مسدودی خارج شد!", show_alert=True)
    show_admin_panel(call.message.chat.id)

@bot.callback_query_handler(func=lambda call: call.data.startswith('remove_channel_'))
@admin_only
def remove_channel(call):
    update_last_seen(call.from_user.id)
    channel_username = call.data.split('_', 2)[2]
    if remove_required_channel(channel_username):
        invalidate_channels_cache()
        bot.answer_callback_query(call.id, f"کانال {channel_username} حذف شد")
    else:
        bot.answer_callback_query(call.id, "خطا در حذف کانال")
    call.data = 'admin_channels'
    handle_admin_actions(call)

@bot.callback_query_handler(func=lambda call: call.data == 'add_channel')
@admin_only
def add_channel(call):
    update_last_seen(call.from_user.id)
    bot.answer_callback_query(call.id)
    markup = types.InlineKeyboardMarkup()
    markup.add(types.InlineKeyboardButton("❌ لغو", callback_data="admin_cancel_step"))
    msg = bot.send_message(call.from_user.id, "آیدی کانال را وارد کنید (مثال: @channel_name):", reply_markup=markup)
    bot.register_next_step_handler(msg, process_new_channel)

@admin_only
def process_new_channel(message):
    user_id = message.from_user.id
    update_last_seen(user_id)
    if message.content_type != 'text':
        bot.clear_step_handler_by_chat_id(message.chat.id)
        bot.send_message(user_id, "❌ عملیات لغو شد. لطفاً فقط متن ارسال کنید.")
        show_admin_panel(user_id)
        return

    text = message.text.strip()
    if text in ['/cancel', 'بازگشت', '/start']:
        show_admin_panel(user_id)
        return

    if not re.fullmatch(r'@[A-Za-z][A-Za-z0-9_]{3,31}', text):
        bot.send_message(user_id, "❌ فرمت آیدی کانال نامعتبر است. باید با @ شروع شود (مثلاً @my_channel).")
        show_admin_panel(user_id)
        return

    try:
        bot_member = bot.get_chat_member(text, get_bot_id())
        if bot_member.status not in ('administrator', 'creator'):
            bot.send_message(user_id, "⚠️ ربات در این کانال ادمین نیست؛ بدون ادمین بودن امکان بررسی عضویت وجود ندارد.\nابتدا ربات را ادمین کنید و دوباره اضافه کنید.")
            show_admin_panel(user_id)
            return
    except Exception:
        bot.send_message(user_id, "❌ کانال پیدا نشد یا ربات به آن دسترسی ندارد. ابتدا ربات را در کانال ادمین کنید.")
        show_admin_panel(user_id)
        return

    if add_required_channel(text):
        invalidate_channels_cache()
        bot.send_message(user_id, f"✅ کانال {text} با موفقیت اضافه شد")
    else:
        bot.send_message(user_id, "⚠️ این کانال قبلاً اضافه شده است")
    show_admin_panel(user_id)

@admin_only
def process_block(message):
    admin_id = message.from_user.id
    update_last_seen(admin_id)
    if message.content_type != 'text' or message.text.strip() in ['لغو', '/cancel', 'بازگشت', '/start']:
        bot.clear_step_handler_by_chat_id(message.chat.id)
        show_admin_panel(admin_id)
        return
        
    try:
        target_id = int(message.text.strip())
        if target_id == admin_id:
            bot.clear_step_handler_by_chat_id(message.chat.id)
            bot.send_message(admin_id, "نمی‌توانید خود را بلاک کنید!")
            show_admin_panel(admin_id)
            return
        block_user(target_id)
        bot.send_message(admin_id, f"✅ کاربر {target_id} با موفقیت مسدود شد.")
        show_admin_panel(admin_id)
    except ValueError:
        bot.clear_step_handler_by_chat_id(message.chat.id) # 🌟 فیکس باگ گیر کردن
        bot.send_message(admin_id, "❌ ID نامعتبر! لطفا فقط عدد وارد کنید.")
        show_admin_panel(admin_id)

# ============================================================================
# 24) Broadcast: handle Telegram 429 + do not hold DB connection during network
# ============================================================================

def advanced_background_broadcast(message, total_users, admin_id, status_msg_id, is_text_msg):
    success = fail = processed = 0
    last_id = 0

    while processed < total_users:
        with get_db_connection() as conn:
            rows = conn.execute('''SELECT user_id FROM users WHERE user_id > ? ORDER BY user_id LIMIT 500''',
                                (last_id,)).fetchall()
        if not rows:
            break

        for (target_id,) in rows:
            processed += 1
            last_id = target_id
            try:
                bot.copy_message(chat_id=target_id, from_chat_id=admin_id, message_id=message.message_id)
                success += 1
            except telebot.apihelper.ApiTelegramException as exc:
                if exc.error_code == 429:
                    retry_after = 1
                    try:
                        retry_after = int((getattr(exc, 'result_json', {}) or {}).get('parameters', {}).get('retry_after', 1))
                    except Exception:
                        pass
                    time.sleep(min(max(retry_after, 1), 60))
                    try:
                        bot.copy_message(chat_id=target_id, from_chat_id=admin_id, message_id=message.message_id)
                        success += 1
                    except Exception:
                        fail += 1
                else:
                    fail += 1
            except Exception:
                fail += 1

            if processed % 100 == 0 or processed == total_users:
                percent = int((processed / total_users) * 100) if total_users else 100
                bar = '█' * int(percent / 10) + '▒' * (10 - int(percent / 10))
                progress = (f'🚀 <b>ارسال همگانی</b>\n\n'
                            f'📦 کل: {total_users}\n✅ موفق: {success}\n❌ ناموفق: {fail}\n'
                            f'📊 {percent}%\n<code>[{bar}]</code>')
                try:
                    if is_text_msg:
                        bot.edit_message_text(progress, admin_id, status_msg_id, parse_mode='HTML')
                    else:
                        bot.edit_message_caption(progress, admin_id, status_msg_id, parse_mode='HTML')
                except Exception:
                    pass

            time.sleep(0.08)

    final_text = (f'🎉 <b>ارسال همگانی پایان یافت!</b>\n\n'
                  f'📦 کل: {total_users}\n✅ موفق: {success}\n❌ ناموفق: {fail}')
    markup = types.InlineKeyboardMarkup()
    markup.add(types.InlineKeyboardButton('🔙 بازگشت به پنل مدیریت', callback_data='admin_back'))
    try:
        if is_text_msg:
            bot.edit_message_text(final_text, admin_id, status_msg_id, reply_markup=markup, parse_mode='HTML')
        else:
            bot.edit_message_caption(final_text, admin_id, status_msg_id, reply_markup=markup, parse_mode='HTML')
    except Exception:
        pass

def process_broadcast(message):
    admin_id = message.from_user.id
    if admin_id not in ADMINS: return
    update_last_seen(admin_id)
    
    system_commands = [
        'شروع چت ناشناس 🎭', 'پروفایل من 👤', '🔗 لینک ناشناس من', 
        'سکه 🪙', 'پنل مدیریت 👨‍💻', '🎲 سوال تصادفی', 
        '/cancel', 'لغو', 'بازگشت', '/start'
    ]
    if message.content_type == 'text' and message.text.strip() in system_commands:
        bot.clear_step_handler_by_chat_id(message.chat.id)
        bot.send_message(admin_id, "❌ عملیات ارسال همگانی لغو شد.")
        show_admin_panel(admin_id)
        return
    
    bot.clear_step_handler_by_chat_id(message.chat.id)
    pending_broadcasts[admin_id] = (message, time.time())
    
    markup = types.InlineKeyboardMarkup(row_width=2)
    markup.add(
        types.InlineKeyboardButton("✅ تایید و ارسال", callback_data="bc_confirm"),
        types.InlineKeyboardButton("❌ لغو عملیات", callback_data="bc_cancel")
    )
    footer_text = "\n\n➖➖➖➖➖➖➖\n❓ آیا از ارسال این پیام مطمئن هستید؟"
    
    if message.content_type == 'text':
        preview_text = f"👀 پیش‌نمایش پیام شما:\n\n{message.text}{footer_text}"
        bot.send_message(admin_id, preview_text, reply_markup=markup)
    else:
        original_caption = message.caption if message.caption else ""
        new_caption = f"👀 پیش‌نمایش:\n\n{original_caption}{footer_text}"
        try:
            bot.copy_message(chat_id=admin_id, from_chat_id=admin_id, message_id=message.message_id, caption=new_caption, reply_markup=markup)
        except Exception:
            bot.copy_message(chat_id=admin_id, from_chat_id=admin_id, message_id=message.message_id, reply_markup=markup)

@bot.callback_query_handler(func=lambda call: call.data in ['bc_confirm', 'bc_cancel'])
def handle_broadcast_preview(call):
    admin_id = call.from_user.id
    if admin_id not in ADMINS: return

    if call.data == 'bc_cancel':
        pending_broadcasts.pop(admin_id, None)
        try: 
            if call.message.content_type == 'text':
                bot.edit_message_text("❌ ارسال همگانی لغو شد.", call.message.chat.id, call.message.message_id)
            else:
                bot.edit_message_caption("❌ ارسال همگانی لغو شد.", call.message.chat.id, call.message.message_id)
        except: pass
        show_admin_panel(admin_id)
        return

    if call.data == 'bc_confirm':
        broadcast_data = pending_broadcasts.pop(admin_id, None)
        if not broadcast_data:
            bot.answer_callback_query(call.id, "❌ پیام یافت نشد یا منقضی شده است.", show_alert=True)
            return
            
        msg_to_broadcast = broadcast_data[0]

        with get_db_connection() as conn:
            cur = conn.cursor()
            cur.execute('SELECT COUNT(*) FROM users')
            total_users = cur.fetchone()[0]

        is_text_msg = (msg_to_broadcast.content_type == 'text')
        start_text = f"🚀 در حال آماده‌سازی موتور ارسال برای {total_users} کاربر...\n<i>(ارسال به صورت دسته‌ای انجام می‌شود تا سرور ایمن بماند)</i>"
        
        try:
            if is_text_msg:
                bot.edit_message_text(start_text, call.message.chat.id, call.message.message_id, parse_mode='HTML')
            else:
                bot.edit_message_caption(start_text, call.message.chat.id, call.message.message_id, parse_mode='HTML')
        except: pass

        threading.Thread(target=advanced_background_broadcast, args=(msg_to_broadcast, total_users, admin_id, call.message.message_id, is_text_msg)).start()

# ============================================================================
# 23) Memory cleanup & Auto-Matcher (زباله‌روب و موتور اتصال خودکار بهینه‌شده)
# ============================================================================

def _expire_pending_secure_media(key):
    """مدیای انتخاب‌نشده را از RAM خارج و حذف پایدار اصل پیام و پیام تایمر را صف می‌کند."""
    # ابتدا رکورد را claim می‌کنیم تا callback انتخاب تایمر هم‌زمان شروع نشود.
    with pending_secure_media_lock:
        data = pending_secure_media.get(key)
        if not data:
            return False

        # اگر callback در حال ارسال مدیاست، انقضا نباید عملیات ارسال را قطع کند.
        processing = bool(data[5]) if len(data) >= 6 else False
        if processing:
            return False

        orig_msg = data[0] if len(data) >= 1 else None
        created_at = data[1] if len(data) >= 2 else time.time()
        prompt_msg_id = data[2] if len(data) >= 3 else None
        stored_chat_id = data[3] if len(data) >= 4 else None

        try:
            user_id = int(orig_msg.chat.id)
            chat_id = int(stored_chat_id)
            original_message_id = int(orig_msg.message_id)
            created_at = float(created_at)
            prompt_message_id = int(prompt_msg_id) if prompt_msg_id is not None else None
        except (AttributeError, TypeError, ValueError):
            logger.error(
                'invalid pending secure media record during expiry key=%s',
                key,
                exc_info=True
            )
            # داده خراب را بی‌صدا حذف نمی‌کنیم تا امکان بررسی و retry باقی بماند.
            return False

        # قفل processing با callback مشترک است؛ اگر callback زودتر claim کرده باشد
        # در بالا خارج شده‌ایم. از این لحظه تنها همین مسیر مجاز به جمع‌کردن رکورد است.
        pending_secure_media[key] = (
            orig_msg,
            created_at,
            prompt_message_id,
            chat_id,
            data[4] if len(data) >= 5 else None,
            True
        )

    # پیش از حذف رکورد RAM، شناسه‌ها را در صف پایدار SQLite ثبت می‌کنیم.
    # اگر ذخیره DB شکست بخورد، رکورد RAM آزاد می‌شود تا worker در دور بعد retry کند.
    message_ids = [original_message_id]
    if prompt_message_id is not None and prompt_message_id != original_message_id:
        message_ids.append(prompt_message_id)

    try:
        with secure_tracker_lock:
            with get_db_connection() as conn:
                cur = conn.cursor()
                for message_id in message_ids:
                    cur.execute(
                        '''INSERT INTO secure_chat_messages
                           (chat_id, user_id, message_id, created_at,
                            delete_requested, retry_at, attempts)
                           VALUES (?, ?, ?, ?, 1, 0, 0)
                           ON CONFLICT(chat_id, user_id, message_id)
                           DO UPDATE SET delete_requested = 1, retry_at = 0''',
                        (chat_id, user_id, message_id, created_at)
                    )
                conn.commit()
    except Exception:
        logger.critical(
            'could not persist expired secure media deletion chat=%s user=%s key=%s',
            chat_id, user_id, key,
            exc_info=True
        )
        with pending_secure_media_lock:
            current = pending_secure_media.get(key)
            if current and len(current) >= 6 and current[0] is orig_msg:
                pending_secure_media[key] = (
                    orig_msg,
                    created_at,
                    prompt_message_id,
                    chat_id,
                    current[4],
                    False
                )
        return False

    # فقط پس از commit موفق SQLite رکورد RAM برداشته می‌شود.
    with pending_secure_media_lock:
        current = pending_secure_media.get(key)
        if current and len(current) >= 6 and current[0] is orig_msg:
            pending_secure_media.pop(key, None)

    # حذف فوری را امتحان می‌کنیم؛ خطاهای موقت در صف SQLite باقی می‌مانند
    # و _process_secure_chat_message_deletions با backoff دوباره تلاش خواهد کرد.
    try:
        _process_secure_chat_message_deletions(chat_id=chat_id)
    except Exception:
        logger.error(
            'immediate expired secure media cleanup failed chat=%s user=%s',
            chat_id, user_id,
            exc_info=True
        )

    return True

def _process_due_secure_media_deletions(now_ts=None, limit=100):
    """پردازش صف حذف مدیای زمان‌دار؛ فقط نتیجه True به‌عنوان حذف موفق پذیرفته می‌شود."""
    now_ts = time.time() if now_ts is None else float(now_ts)
    limit = max(1, min(int(limit), 500))

    # Bot API پیام‌های قدیمی‌تر از پنجره مجاز را حذف نمی‌کند.
    cutoff = now_ts - SECURE_MESSAGE_DELETE_WINDOW - 30

    try:
        with get_db_connection() as conn:
            cur = conn.cursor()

            # رکوردهایی که دیگر در بازه مجاز حذف نیستند قابل retry نیستند.
            cur.execute(
                'DELETE FROM secure_media_deletions WHERE delete_at <= ?',
                (cutoff,)
            )

            cur.execute(
                '''SELECT id, receiver_id, sent_message_id, sender_id,
                          original_message_id, status_chat_id, status_message_id,
                          COALESCE(attempts, 0)
                   FROM secure_media_deletions
                   WHERE delete_at <= ? AND COALESCE(retry_at, 0) <= ?
                   ORDER BY delete_at ASC
                   LIMIT ?''',
                (now_ts, now_ts, limit)
            )
            rows = cur.fetchall()
            conn.commit()

    except Exception:
        logger.error('secure media deletion queue read failed', exc_info=True)
        return

    already_deleted_markers = (
        'message to delete not found',
        'message_id_invalid',
        'message not found',
    )

    for row in rows:
        (
            record_id,
            receiver_id,
            sent_id,
            sender_id,
            original_id,
            status_chat_id,
            status_id,
            attempts,
        ) = row

        delete_ok = True

        # هر دو سمت باید حذف شوند؛ False یا None موفقیت محسوب نمی‌شود.
        for target_chat, message_id in (
            (receiver_id, sent_id),
            (sender_id, original_id),
        ):
            try:
                result = bot.delete_message(target_chat, message_id)
                if result is not True:
                    delete_ok = False
                    logger.warning(
                        'secure media delete returned non-True result=%r record=%s chat=%s message=%s',
                        result, record_id, target_chat, message_id,
                    )

            except telebot.apihelper.ApiTelegramException as exc:
                description = (
                    getattr(exc, 'description', None) or str(exc)
                ).lower()

                # پیام از قبل حذف شده یا دیگر وجود ندارد؛ هدف حذف محقق شده است.
                if any(marker in description for marker in already_deleted_markers):
                    continue

                delete_ok = False
                logger.warning(
                    'secure media deletion API error record=%s chat=%s message=%s: %s',
                    record_id, target_chat, message_id, description,
                )

            except Exception:
                delete_ok = False
                logger.warning(
                    'secure media deletion failed record=%s chat=%s message=%s',
                    record_id, target_chat, message_id,
                    exc_info=True,
                )

        if delete_ok:
            # پیام وضعیت صرفاً اطلاع‌رسانی است؛ شکست ویرایش آن مانع حذف رکورد اصلی نیست.
            if status_chat_id and status_id:
                try:
                    bot.edit_message_text(
                        '🔥 <i>مدیای زمان‌دار حذف شد.</i>',
                        chat_id=status_chat_id,
                        message_id=status_id,
                        parse_mode='HTML',
                    )
                except Exception:
                    pass

            try:
                with get_db_connection() as conn:
                    cur = conn.cursor()
                    cur.execute(
                        'DELETE FROM secure_media_deletions WHERE id = ?',
                        (record_id,),
                    )
                    conn.commit()
            except Exception:
                # حذف دوباره پیام در اجرای بعدی بی‌خطر است؛ not-found موفق تلقی می‌شود.
                logger.error(
                    'could not remove completed secure media deletion record=%s',
                    record_id,
                    exc_info=True,
                )

        else:
            next_attempt = int(attempts or 0) + 1
            # ۳۰، ۶۰، ۱۲۰، ... تا سقف یک ساعت
            delay_seconds = min(30 * (2 ** min(next_attempt - 1, 7)), 3600)
            retry_at = now_ts + delay_seconds

            try:
                with get_db_connection() as conn:
                    cur = conn.cursor()
                    cur.execute(
                        '''UPDATE secure_media_deletions
                           SET attempts = ?, retry_at = ?
                           WHERE id = ?''',
                        (next_attempt, retry_at, record_id),
                    )
                    conn.commit()
            except Exception:
                logger.error(
                    'could not schedule secure media retry record=%s',
                    record_id,
                    exc_info=True,
                )

            logger.warning(
                'secure media deletion postponed record=%s attempt=%s retry_in=%ss',
                record_id, next_attempt, delay_seconds,
            )


def _process_secure_chat_message_deletions(now_ts=None, limit=500, chat_id=None):
    """حذف پیام‌های چت امن؛ فقط True یا خطای not-found موفق محسوب می‌شود."""
    now_ts = time.time() if now_ts is None else float(now_ts)
    limit = max(1, min(int(limit), 2000))
    cutoff = now_ts - SECURE_MESSAGE_DELETE_WINDOW

    try:
        with get_db_connection() as conn:
            cur = conn.cursor()

            # پس از ۴۸ ساعت Telegram Bot API دیگر اجازه حذف این پیام‌ها را نمی‌دهد.
            cur.execute(
                'DELETE FROM secure_chat_messages WHERE created_at <= ?',
                (cutoff,),
            )

            if chat_id is None:
                cur.execute(
                    '''SELECT chat_id, user_id, message_id, attempts
                       FROM secure_chat_messages
                       WHERE delete_requested = 1
                         AND retry_at <= ?
                         AND created_at > ?
                       ORDER BY created_at ASC
                       LIMIT ?''',
                    (now_ts, cutoff, limit),
                )
            else:
                cur.execute(
                    '''SELECT chat_id, user_id, message_id, attempts
                       FROM secure_chat_messages
                       WHERE chat_id = ?
                         AND delete_requested = 1
                         AND retry_at <= ?
                         AND created_at > ?
                       ORDER BY created_at ASC
                       LIMIT ?''',
                    (int(chat_id), now_ts, cutoff, limit),
                )

            rows = cur.fetchall()
            conn.commit()

    except Exception:
        logger.error('secure chat deletion queue read failed', exc_info=True)
        return

    if not rows:
        return

    already_deleted_markers = (
        'message to delete not found',
        'message_id_invalid',
        'message not found',
        'message identifier is not specified',
    )

    grouped = defaultdict(list)
    attempts_by_key = {}
    for row_chat_id, user_id, message_id, attempts in rows:
        key = (int(row_chat_id), int(user_id), int(message_id))
        grouped[int(user_id)].append((int(row_chat_id), int(message_id)))
        attempts_by_key[key] = int(attempts or 0)

    def schedule_retry(key, error_text=''):
        row_chat_id, user_id, message_id = key
        old_attempts = attempts_by_key.get(key, 0)
        new_attempts = old_attempts + 1
        retry_delay = min(3600, 30 * (2 ** min(new_attempts - 1, 7)))

        try:
            with get_db_connection() as conn:
                cur = conn.cursor()
                cur.execute(
                    '''UPDATE secure_chat_messages
                       SET attempts = ?, retry_at = ?
                       WHERE chat_id = ? AND user_id = ? AND message_id = ?''',
                    (
                        new_attempts,
                        now_ts + retry_delay,
                        row_chat_id,
                        user_id,
                        message_id,
                    ),
                )
                conn.commit()
        except Exception:
            logger.error(
                'could not schedule secure message deletion retry chat=%s user=%s message=%s',
                row_chat_id, user_id, message_id,
                exc_info=True,
            )

        logger.warning(
            'secure message deletion deferred chat=%s user=%s message=%s attempt=%s retry_in=%ss error=%s',
            row_chat_id, user_id, message_id, new_attempts, retry_delay, error_text,
        )

    for user_id, message_rows in grouped.items():
        # Telegram deleteMessages در هر درخواست حداکثر ۱۰۰ شناسه می‌پذیرد.
        for offset in range(0, len(message_rows), 100):
            batch = message_rows[offset:offset + 100]
            message_ids = [message_id for _row_chat_id, message_id in batch]
            deleted_keys = []

            try:
                result = bot.delete_messages(user_id, message_ids)
                if result is not True:
                    raise RuntimeError(
                        f'Telegram delete_messages returned non-True result: {result!r}'
                    )

                deleted_keys = [
                    (row_chat_id, user_id, message_id)
                    for row_chat_id, message_id in batch
                ]

            except Exception as bulk_exc:
                # اگر حذف گروهی شکست خورد، تک‌تک امتحان می‌کنیم تا یک پیام خراب
                # مانع حذف بقیه پیام‌های همان دسته نشود.
                logger.warning(
                    'bulk secure message deletion failed user=%s; falling back to single deletes: %s',
                    user_id, bulk_exc,
                )

                for row_chat_id, message_id in batch:
                    key = (row_chat_id, user_id, message_id)

                    try:
                        result = bot.delete_message(user_id, message_id)
                        if result is True:
                            deleted_keys.append(key)
                        else:
                            schedule_retry(key, f'delete_message returned {result!r}')

                    except Exception as exc:
                        description = (
                            getattr(exc, 'description', None) or str(exc)
                        ).lower()

                        if any(marker in description for marker in already_deleted_markers):
                            deleted_keys.append(key)
                            continue

                        schedule_retry(key, description)

            if deleted_keys:
                try:
                    with get_db_connection() as conn:
                        cur = conn.cursor()
                        cur.executemany(
                            '''DELETE FROM secure_chat_messages
                               WHERE chat_id = ? AND user_id = ? AND message_id = ?''',
                            deleted_keys,
                        )
                        conn.commit()
                except Exception:
                    logger.error(
                        'could not remove completed secure deletion records',
                        exc_info=True,
                    )

def memory_cleanup_worker():
    while True:
        try:
            time.sleep(15)
            current_time = time.time()

            # ۰. پاک‌سازی کش‌های موقت و منقضی‌شده

            # پاک‌سازی کش عضویت
            with cache_lock:
                for uid, cached in list(membership_cache.items()):
                    try:
                        expires_at = float(cached[1])
                    except (TypeError, ValueError, IndexError, KeyError):
                        membership_cache.pop(uid, None)
                        continue

                    if expires_at <= current_time:
                        membership_cache.pop(uid, None)

            # پاک‌سازی لینک‌های /start معلق پس از یک ساعت
            with pending_start_links_lock:
                for uid, data in list(pending_start_links.items()):
                    try:
                        created_at = float(data[1])
                    except (TypeError, ValueError, IndexError, KeyError):
                        pending_start_links.pop(uid, None)
                        continue

                    if current_time - created_at >= 3600:
                        pending_start_links.pop(uid, None)

            # ۱. بررسی خودکار صف برای اتصال افراد منتظر
            waiting_users = []

            with queue_lock:
                if len(waiting_pool) >= 2:
                    waiting_users = list(waiting_pool)

            retries_this_tick = 0

            for item in waiting_users:
                if retries_this_tick >= 20:
                    break

                uid = item['user_id']

                if REMATCH_RELAX_AFTER_SECONDS is not None:
                    waited = current_time - item.get('time', current_time)
                    since_last_retry = current_time - item.get('last_retry', 0.0)

                    if waited >= REMATCH_RELAX_AFTER_SECONDS and since_last_retry >= 45:

                        is_valid_to_retry = False

                        with queue_lock:
                            if any(x['user_id'] == uid for x in waiting_pool):
                                if uid in search_reservations or item['mode'] == 'general':
                                    is_valid_to_retry = True

                        if is_valid_to_retry and not is_user_in_chat(uid):
                            retries_this_tick += 1

                            try:
                                try_pairing(uid, item['mode'])
                            except Exception:
                                logger.error(
                                    'Automatic pairing retry failed',
                                    exc_info=True
                                )

            # ۲. پاک‌سازی تایم‌اوت‌ها و تاریخچه کلیک‌های اسپم
            with spam_lock:
                for uid, timeout in list(user_timeouts.items()):
                    if current_time > timeout:
                        user_timeouts.pop(uid, None)

                for uid, presses in list(user_next_presses.items()):
                    valid = [
                        t for t in presses
                        if current_time - t <= 10
                    ]

                    if valid:
                        user_next_presses[uid] = valid
                    else:
                        user_next_presses.pop(uid, None)

            # ۳. پاک‌سازی مدیاهای امنیتی خام
            with pending_secure_media_lock:
                pending_snapshot = list(pending_secure_media.items())

            for key, data in pending_snapshot:
                if current_time - data[1] > 300:
                    _expire_pending_secure_media(key)

            # پردازش صف حذف مدیای زمان‌دار
            _process_due_secure_media_deletions(current_time)

            # پردازش صف حذف پیام‌های چت امن
            # فقط در صورت نصب بودن Patch مربوطه اجرا می‌شود.
            secure_chat_cleanup = globals().get(
                "_process_secure_chat_message_deletions"
            )

            if callable(secure_chat_cleanup):
                try:
                    secure_chat_cleanup(current_time)
                except Exception:
                    logger.error(
                        'Secure chat message cleanup failed',
                        exc_info=True
                    )

            # ۴. پاک‌سازی پیش‌نمایش ارسال همگانی
            for admin_id, data in list(pending_broadcasts.items()):
                if current_time - data[1] > 3600:
                    pending_broadcasts.pop(admin_id, None)

            # ۵. پاک‌سازی رزروهای مالی معلق و صف‌های منقضی شده
            expired_queue_uids = []

            with queue_lock:
                for uid, reservation in list(search_reservations.items()):
                    if current_time - reservation.get('created_at', current_time) > 1800:
                        if not is_user_waiting(uid) and uid not in active_pairs:
                            release_search_reservation(uid)

                for uid, timestamp in list(user_queue_times.items()):
                    if current_time - timestamp > 1800:
                        remove_from_waiting(uid)
                        release_search_reservation(uid)
                        expired_queue_uids.append(uid)

            for uid in expired_queue_uids:
                try:
                    bot.send_message(
                        uid,
                        '⏳ زمان جستجو تمام شد؛ سهمیه/هزینه مربوطه بازگردانده شد.',
                        reply_markup=types.ReplyKeyboardRemove()
                    )

                    show_main_menu(uid, uid in ADMINS)

                except Exception:
                    pass

            # ۶. آزادسازی درخواست‌های چت منقضی‌شده
            cutoff_req_str = (
                datetime.now() -
                timedelta(seconds=CHAT_REQUEST_TTL_SECONDS)
            ).strftime('%Y-%m-%d %H:%M:%S')

            with get_db_connection() as conn:
                cur = conn.cursor()
                cur.execute('BEGIN IMMEDIATE')

                cur.execute(
                    '''
                    SELECT sender_id, receiver_id, COALESCE(fee_paid, 0)
                    FROM chat_requests
                    WHERE status = 'pending'
                    AND last_action_time <= ?
                    ''',
                    (cutoff_req_str,)
                )

                expired_reqs = cur.fetchall()

                for s_id, r_id, f_paid in expired_reqs:
                    cur.execute(
                        '''
                        UPDATE chat_requests
                        SET status = 'expired', fee_paid = 0
                        WHERE sender_id = ?
                        AND receiver_id = ?
                        AND status = 'pending'
                        ''',
                        (s_id, r_id)
                    )

                    if cur.rowcount == 1 and f_paid:
                        cur.execute(
                            '''
                            UPDATE users
                            SET coins = COALESCE(coins, 0) + ?
                            WHERE user_id = ?
                            ''',
                            (CHAT_REQUEST_COST, s_id)
                        )

                conn.commit()

            # ۷. پاک‌سازی تایمر بازی‌های گروهی و درخواست‌های دوستی منقضی‌شده
            with queue_lock:
                active_lobbies_snapshot = list(active_group_games.items())
                for cid, data in list(game_reveal_choices.items()):
                    if current_time - data.get('timestamp', current_time) > 1800:
                        game_reveal_choices.pop(cid, None)
                        game_chat_groups.pop(cid, None)
                for cid in list(game_chat_groups.keys()):
                    if cid not in game_sessions and cid not in game_reveal_choices:
                        game_chat_groups.pop(cid, None)
            for gid, gdata in active_lobbies_snapshot:
                if current_time - float(gdata.get('created_at', current_time)) > (BLIND_DATE_LOBBY_SECONDS + 30):
                    _cancel_group_lobby(gid, expected_game=gdata)
                elif not _is_lobby_message_alive(gid, gdata):
                    _cancel_group_lobby(gid, expected_game=gdata)
            with _group_lobby_edit_lock:
                for gid, ts in list(_group_lobby_last_edit.items()):
                    if current_time - ts > 1800:
                        _group_lobby_last_edit.pop(gid, None)

            with get_db_connection() as conn:
                conn.execute(
                    '''UPDATE friend_requests
                       SET status = 'expired', updated_at = ?
                       WHERE status = 'pending' AND updated_at <= ?''',
                    (current_time, current_time - FRIEND_REQUEST_TTL_SECONDS)
                )
                conn.execute(
                    '''UPDATE support_ticket_messages
                       SET locked_by_admin_id = NULL, locked_at = 0
                       WHERE locked_by_admin_id IS NOT NULL AND locked_at <= ?''',
                    (current_time - SUPPORT_ADMIN_LOCK_TTL_SECONDS,)
                )
                conn.commit()

            # ۸. قطع چت‌های غیرفعال
            for cid, last_ts in list(chat_last_activity.items()):
                if current_time - last_ts > 3600:
                    pair_users_found = []

                    with queue_lock:
                        for uid, data in list(active_pairs.items()):
                            if data[1] == cid:
                                pair_users_found.append(uid)

                    if len(pair_users_found) >= 2:
                        u1, u2 = pair_users_found[:2]

                        terminate_chat_session(
                            cid,
                            u1,
                            u2,
                            send_feedback=True
                        )

                        timeout_msg = (
                            '⏳ <b>چت پایان یافت!</b> '
                            'به‌دلیل عدم فعالیت بیش از ۶۰ دقیقه.'
                        )

                        for u in (u1, u2):
                            try:
                                bot.send_message(
                                    u,
                                    timeout_msg,
                                    parse_mode='HTML',
                                    reply_markup=types.ReplyKeyboardRemove()
                                )

                                show_main_menu(u, u in ADMINS)

                            except Exception:
                                pass

            # ۹. پاک‌سازی کش آخرین بازدید
            with last_seen_lock:
                for uid, ts in list(_last_seen_write.items()):
                    if current_time - ts > 3600:
                        _last_seen_write.pop(uid, None)

            # ۱۰. پاک‌سازی تاریخچه نرخ پیام‌های ناشناس
            with rate_limit_lock:
                for uid, q in list(_anon_message_times.items()):
                    while q and current_time - q[0] > ANON_MESSAGE_WINDOW:
                        q.popleft()

                    if not q:
                        _anon_message_times.pop(uid, None)

            # ۱۱. پاک‌سازی رسیدهای منقضی‌شده ادمین
            for db_id, data in list(admin_receipt_messages.items()):
                if current_time - data.get('timestamp', current_time) > 86400:

                    for a_id, m_id in data.get('msgs', {}).items():
                        try:
                            bot.edit_message_caption(
                                caption="⚠️ <i>این رسید منقضی شد (بیش از ۲۴ ساعت گذشته).</i>",
                                chat_id=a_id,
                                message_id=m_id,
                                parse_mode='HTML',
                                reply_markup=None
                            )
                        except Exception:
                            pass

                    admin_receipt_messages.pop(db_id, None)

        except Exception:
            logger.error(
                'memory_cleanup_worker crashed',
                exc_info=True
            )

# ==========================================
# 🌟 موتور هوشمند بازگردانی کاربران خاموش (Retention System)
# ==========================================
def retention_notification_worker():
    while True:
        try:
            current_time = datetime.now()
            three_days_ago = (current_time - timedelta(days=3)).strftime("%Y-%m-%d %H:%M:%S")
            ten_days_ago = (current_time - timedelta(days=10)).strftime("%Y-%m-%d %H:%M:%S")
            
            with get_db_connection() as conn:
                cur = conn.cursor()
                cur.execute('SELECT COUNT(*) FROM users WHERE last_seen >= ?', (three_days_ago,))
                active_users_3_days = cur.fetchone()[0]
                
                seven_days_ago_date = (current_time - timedelta(days=7)).strftime("%Y-%m-%d")
                
                cur.execute('''SELECT user_id, display_name, location 
                               FROM users 
                               WHERE last_seen <= ? AND last_seen >= ? 
                               AND (last_retention_date IS NULL OR last_retention_date <= ?)''', 
                            (three_days_ago, ten_days_ago, seven_days_ago_date))
                
                inactive_users = cur.fetchall()
            
            if inactive_users:
                today_date = current_time.strftime("%Y-%m-%d")
                for user in inactive_users:
                    uid, dname, loc = user
                    safe_name = escape_html(dname or "دوست من")
                    
                    province_txt = ""
                    if loc and loc != "نامشخص":
                        province = loc.split(' - ')[0] if ' - ' in loc else loc
                        province_txt = f"از همشهری‌هات در <b>{province}</b> و "
                    
                    text = (
                        f"دلمون برات تنگ شده بود {safe_name}! 🥺🤍\n\n"
                        f"چند روزیه که به ربات سر نزدی...\n"
                        f"جالبه بدونی توی همین ۳ روزی که نبودی، بیش از <b>{active_users_3_days} نفر</b> {province_txt}سراسر ایران به ربات سر زدن و کلی چت جدید شکل گرفته! 💬✨\n\n"
                        f"🎁 <b>یه خبر خوب:</b> برای اینکه برگشتی، <b>۱۰ سکه هدیه رایگان</b> برات کنار گذاشتیم! فقط کافیه روی دکمه زیر کلیک کنی.\n\n"
                        f"💡 <i>(راستی یادت نره با معرفی هر دوستت با لینک اختصاصی خودت، می‌تونی ۱۰ سکه دیگه هم بگیری)</i>\n\n"
                        f"منتظر چی هستی؟ بزن روی دکمه و برگرد 👇"
                    )
                    
                    markup = types.InlineKeyboardMarkup(row_width=1)
                    markup.add(types.InlineKeyboardButton("🎁 دریافت ۱۰ سکه هدیه بازگشت", callback_data="claim_return_reward"))
                    
                    try:
                        bot.send_message(uid, text, reply_markup=markup, parse_mode='HTML')
                        with get_db_connection() as conn:
                            cur = conn.cursor()
                            cur.execute('UPDATE users SET last_retention_date = ?, pending_return_reward = 1 WHERE user_id = ?', (today_date, uid))
                            conn.commit()
                    except telebot.apihelper.ApiTelegramException as e:
                        if e.error_code == 403:
                            pass # کاربر ربات را بلاک کرده است، فقط رد می‌شویم
                    except Exception:
                        pass
                    time.sleep(0.1)
                    
        except Exception:
            logger.error("Retention Worker Crashed", exc_info=True)
            
        time.sleep(43200)

if __name__ == '__main__':
    init_db()
    configure_sqlite()
    apply_hardening_migrations()
    # Migration بازیابی Refund های Stars
    apply_stars_refund_recovery_migration()
    ensure_identity_token_indexes()
    load_blocked_cache()

    logger.info("Starting background memory cleaner...")

    cleaner_thread = threading.Thread(
        target=memory_cleanup_worker,
        daemon=True
    )
    cleaner_thread.start()

    retention_thread = threading.Thread(
        target=retention_notification_worker,
        daemon=True
    )
    retention_thread.start()

    # Worker بازیابی تراکنش‌های Stars
    stars_refund_thread = threading.Thread(
        target=stars_refund_recovery_worker,
        daemon=True
    )
    stars_refund_thread.start()

    logger.info("Bot is running securely...")

    try:
        bot.infinity_polling(
            timeout=10,
            long_polling_timeout=5
        )
    except Exception:
        logger.critical(
            "Bot stopped abruptly!",
            exc_info=True
        )