# -*- coding: utf-8 -*-
# ============================================================================
#  bot.py  |  فایل ۱ از ۲  (پایتون)  —  ربات تلگرام + آمار کامل گروه + پنل مدیریت
# ============================================================================
#
#  امکانات:
#    • شمارش پیام‌ها، ورودها (لگین)، اعضای اضافه‌شده، امتیازها
#    • تشخیص خودکار ربات‌ها (هیچ‌جا شمرده نمی‌شوند)
#    • زیرمجموعه‌گیری با لینک اختصاصی (لینک ربات + لینک دعوت گروه)
#    • جوین اجباری کانال‌ها با امتیاز (بررسی واقعی عضویت، لف بده = امتیاز نده)
#    • «امتیاز آمار» به ۳ نفر اول پیام روزانه — به وقت ایران بعد از ۰۰:۰۰ (۵۰/۳۰/۲۰)
#    • فروشگاه: شارژ / اینترنت / گیفت  +  سفارش → پنل مالک + دکمه تایید
#    • عدم امتیاز به کاربران غیرفعال (کسی که خیلی وقت پیش تلگرام آمده و فعالیت ندارد)
#    • پنل مدیریت کامل داخل ربات (تنظیمات، فروشگاه، کانال‌ها، ماموریت‌ها، آمار، ادمین‌ها)
#
#  اجرا روی cPanel / VPS:
#      python bot.py
#  اجرای دائمی:
#      tmux new -s gapbot  &&  python bot.py     (خروج: Ctrl+B بعد D)
#
#  نیازمندی: فقط پایتون ۳.۸+ (هیچ pip install لازم نیست — فقط کتابخانه استاندارد)
# ============================================================================

import os
import re
import json
import time
import html
import random
import sqlite3
import threading
import traceback
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timedelta, timezone

# ════════════════════════════════════════════════════════════════════════════
#  ⚙️  تنظیمات اصلی  ← فقط همین چند خط را پر کن
# ════════════════════════════════════════════════════════════════════════════

BOT_TOKEN = "8298479150:AAHrh0uQT0lVxN17kn_exSJdq6_kjTkad88"

# 👇👇👇  جای لینک مینی‌اپ  👇👇👇
#   آدرس فایل miniapp.php خودت را اینجا بگذار (باید https باشد)
#   مثال: "https://alidarks.s16.viptelbot.top/miniapp.php"
MINI_APP_URL = "https://alidark.s16.viptelbot.top/miniapp.php"          # ←←← لینک مینی‌اپ را داخل این دو کوتیشن بگذار

# آیدی عددی مالک (پنل مدیریت، سفارش‌ها، گزارش شبانه)
OWNER_ID = 7439723759

# ════════════════════════════════════════════════════════════════════════════
#  ✨ تریگر مخفی «dark» و دکمه‌های مینی‌اپ
# ════════════════════════════════════════════════════════════════════════════

# متن دکمه‌ی کنار چت در پیوی ربات (همان دکمه‌ای که قبلاً «مینی‌اپ» بود)
MENU_BTN_TEXT = "Khoy"

# متن دکمه‌ای که زیر پیام معرفی (در گروه) نشان داده می‌شود
APP_BTN_TEXT = "ربات خوی اکیپ"

# کلمه‌های تریگر — وقتی کسی در گروه بنویسد «@آیدی‌ربات dark» این پیام فرستاده می‌شود
DARK_WORDS = ("dark", "دارک")

# فاصله‌ی مجاز بین دو ارسال در یک گروه (ثانیه) — برای جلوگیری از اسپم
DARK_COOLDOWN = 25

# ⚠️ آیدی گروه را اینجا نمی‌گذاریم!
#    گروه‌ها از داخل خود ربات مدیریت می‌شوند:  /panel  →  👥 افزودن / مدیریت گروه‌ها
#    هر گروهی که ربات در آن باشد خودکار شناسایی می‌شود و می‌توانی فعال/غیرفعالش کنی.

# مسیر دیتابیس مشترک با miniapp.php
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "gapbot.db")

# منطقه زمانی
TIMEZONE = "Asia/Tehran"

# ════════════════════════════════════════════════════════════════════════════
#  کد اصلی  (از اینجا به بعد نیازی به تغییر نیست)
# ════════════════════════════════════════════════════════════════════════════

try:
    from zoneinfo import ZoneInfo
    TZ = ZoneInfo(TIMEZONE)
except Exception:                                    # پایتون قدیمی
    TZ = timezone(timedelta(hours=3, minutes=30))

API = "https://api.telegram.org/bot%s/" % BOT_TOKEN
_lock = threading.RLock()
_conn = None
FSM = {}                        # گفتگوی مرحله‌ای پنل مدیریت
BOT_USERNAME = ""
ME = {}


# ───────────────────────────── ابزار زمان ─────────────────────────────

def now():
    return datetime.now(TZ)


def now_iso():
    return now().strftime("%Y-%m-%d %H:%M:%S")


def today_str():
    return now().strftime("%Y-%m-%d")


# ───────────────────────────── دیتابیس ─────────────────────────────

SCHEMA = """
CREATE TABLE IF NOT EXISTS users(
  user_id        INTEGER PRIMARY KEY,
  username       TEXT    DEFAULT '',
  first_name     TEXT    DEFAULT '',
  last_name      TEXT    DEFAULT '',
  is_bot         INTEGER DEFAULT 0,
  is_premium     INTEGER DEFAULT 0,
  points         INTEGER DEFAULT 0,
  total_points   INTEGER DEFAULT 0,
  spent_points   INTEGER DEFAULT 0,
  messages       INTEGER DEFAULT 0,
  chars          INTEGER DEFAULT 0,
  mini_opens     INTEGER DEFAULT 0,
  added_count    INTEGER DEFAULT 0,
  invited_count  INTEGER DEFAULT 0,
  referred_by    INTEGER DEFAULT 0,
  first_seen     TEXT    DEFAULT '',
  last_seen      TEXT    DEFAULT '',
  last_mini_open TEXT    DEFAULT '',
  banned         INTEGER DEFAULT 0,
  note           TEXT    DEFAULT '',
  photo_file_id  TEXT    DEFAULT '',
  photo_path     TEXT    DEFAULT '',
  photo_at       TEXT    DEFAULT ''
);
CREATE TABLE IF NOT EXISTS daily(
  user_id  INTEGER,
  day      TEXT,
  messages INTEGER DEFAULT 0,
  chars    INTEGER DEFAULT 0,
  PRIMARY KEY(user_id, day)
);
CREATE TABLE IF NOT EXISTS msg_log(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER, chat_id INTEGER, ts INTEGER, day TEXT, chars INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS invites(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  inviter_id INTEGER, invited_id INTEGER, kind TEXT,
  points INTEGER DEFAULT 0, status TEXT DEFAULT 'pending',
  created_at TEXT, due_at TEXT, decided_at TEXT, note TEXT,
  UNIQUE(inviter_id, invited_id, kind)
);
CREATE TABLE IF NOT EXISTS ref_links(
  user_id    INTEGER PRIMARY KEY,
  link       TEXT,
  link_name  TEXT,
  joins      INTEGER DEFAULT 0,
  created_at TEXT
);
CREATE TABLE IF NOT EXISTS point_logs(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER, amount INTEGER, reason TEXT, ref TEXT, ts TEXT
);
CREATE TABLE IF NOT EXISTS groups(
  chat_id       TEXT PRIMARY KEY,
  title         TEXT    DEFAULT '',
  username      TEXT    DEFAULT '',
  is_active     INTEGER DEFAULT 1,
  members       INTEGER DEFAULT 0,
  msgs          INTEGER DEFAULT 0,
  added_at      TEXT    DEFAULT '',
  last_activity TEXT    DEFAULT ''
);
CREATE TABLE IF NOT EXISTS channels(
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  chat_id     TEXT,
  title       TEXT,
  username    TEXT,
  invite_link TEXT,
  points      INTEGER DEFAULT 5,
  is_active   INTEGER DEFAULT 1,
  created_at  TEXT
);
CREATE TABLE IF NOT EXISTS channel_joins(
  user_id INTEGER, channel_id INTEGER, verified_at TEXT, points INTEGER DEFAULT 0,
  PRIMARY KEY(user_id, channel_id)
);
CREATE TABLE IF NOT EXISTS shop_items(
  id           INTEGER PRIMARY KEY AUTOINCREMENT,
  code         TEXT UNIQUE,
  category     TEXT,
  title        TEXT,
  subtitle     TEXT DEFAULT '',
  price_points INTEGER DEFAULT 0,
  contact_kind TEXT DEFAULT 'phone',
  image        TEXT DEFAULT '4.jpg',
  is_active    INTEGER DEFAULT 1,
  sort         INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS orders(
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id       INTEGER,
  item_id       INTEGER,
  item_title    TEXT,
  category      TEXT,
  price_points  INTEGER,
  contact_kind  TEXT,
  contact_value TEXT,
  status        TEXT DEFAULT 'pending',
  admin_note    TEXT DEFAULT '',
  created_at    TEXT,
  updated_at    TEXT
);
CREATE TABLE IF NOT EXISTS missions(
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  code        TEXT UNIQUE,
  title       TEXT,
  description TEXT DEFAULT '',
  kind        TEXT,
  target      INTEGER DEFAULT 1,
  reward      INTEGER DEFAULT 10,
  image       TEXT DEFAULT '6.jpg',
  is_active   INTEGER DEFAULT 1,
  sort        INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS mission_progress(
  user_id INTEGER, mission_id INTEGER, claimed_at TEXT,
  PRIMARY KEY(user_id, mission_id)
);
CREATE TABLE IF NOT EXISTS settings(k TEXT PRIMARY KEY, v TEXT);
CREATE TABLE IF NOT EXISTS admins(user_id INTEGER PRIMARY KEY, added_at TEXT);
CREATE TABLE IF NOT EXISTS logs(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  kind TEXT, user_id INTEGER, text TEXT, ts TEXT
);
CREATE TABLE IF NOT EXISTS state(k TEXT PRIMARY KEY, v TEXT);
CREATE TABLE IF NOT EXISTS night_rewards(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  day TEXT, kind TEXT, user_id INTEGER, rank INTEGER, points INTEGER, ts TEXT
);
CREATE INDEX IF NOT EXISTS ix_daily_day   ON daily(day, messages DESC);
CREATE INDEX IF NOT EXISTS ix_msglog_ts   ON msg_log(ts);
CREATE INDEX IF NOT EXISTS ix_inv_inviter ON invites(inviter_id, status);
CREATE INDEX IF NOT EXISTS ix_orders_st   ON orders(status, id);
"""

DEFAULTS = {
    "owner_id": str(OWNER_ID),
    "mini_app_url": MINI_APP_URL,
    "group_chat_id": "",             # گروه اصلی (برای لینک دعوت) — از پنل تعیین می‌شود
    "auto_activate_groups": "1",     # گروه جدید خودکار فعال شود؟
    "group_username": "",
    "group_title": "",
    "bot_username": "",
    # ── امتیازها (نسخه ۲): پیام امتیاز ندارد، فقط دعوت و اد امتیاز دارند ──
    "points_per_message": "0",
    "points_per_member": "15",       # هر کسی که خودت او را به گروه اد کنی
    "points_per_referral": "25",     # هر کسی که با لینک اختصاصی تو عضو شود
    "min_msg_len": "1",
    "daily_msg_cap": "100000",
    "invite_grace_hours": "12",
    "require_activity": "1",
    "night_label": "امتیاز آمار",
    "night_chat_awards": "50,30,20",   # نفرات اول/دوم/سوم پیام روزانه
    "night_invite_awards": "0,0,0",    # اختیاری (پیش‌فرض خاموش)
    "night_hour": "0",
    "post_nightly_report": "1",
    "count_commands": "0",
    "start_text": (
        "👋 سلام {name} عزیز\n\n"
        "به ربات آمار و امتیاز گروه خوش آمدی 🎉\n\n"
        "📊 آمار کامل گروه، رتبه‌بندی پیام‌ها، فروشگاه امتیازی، ماموریت‌ها و زیرمجموعه‌گیری\n"
        "همه در مینی‌اپ 👇"
    ),
    "welcome_text": "🎉 {name} به گروه خوش آمدی!",
    "shop_note": "برای ثبت سفارش، امتیاز کافی لازم است. پس از ثبت، منتظر تایید مدیریت بمان.",
    "miniapp_note": "پس از ثبت سفارش، برای تایید منتظر بمان ✅",
}

# مقادیر نسخه ۱ → نسخه ۲ (فقط اگر دست‌نخورده مانده باشند عوض می‌شوند)
MIG_V1_VALUES = {
    "points_per_message": "1", "points_per_referral": "15", "points_per_member": "20",
    "night_chat_awards": "30,20,10", "night_invite_awards": "30,20,10",
}

SHOP_SEED = [
    # code,               category,  title,                     subtitle,             price, contact,       image
    ("charge_10",   "charge",   "شارژ ۱۰ هزار تومانی",   "ایرانسل / همراه اول", 100,  "phone",       "8.jpg"),
    ("charge_20",   "charge",   "شارژ ۲۰ هزار تومانی",   "ایرانسل / همراه اول", 200,  "phone",       "8.jpg"),
    ("charge_30",   "charge",   "شارژ ۳۰ هزار تومانی",   "ایرانسل / همراه اول", 300,  "phone",       "8.jpg"),
    ("charge_40",   "charge",   "شارژ ۴۰ هزار تومانی",   "ایرانسل / همراه اول", 400,  "phone",       "8.jpg"),
    ("charge_50",   "charge",   "شارژ ۵۰ هزار تومانی",   "ایرانسل / همراه اول", 500,  "phone",       "8.jpg"),
    ("charge_100",  "charge",   "شارژ ۱۰۰ هزار تومانی",  "ایرانسل / همراه اول", 1000, "phone",       "8.jpg"),
    ("net_1500_1",  "internet", "اینترنت ۱.۵ گیگ یک‌روزه",  "بسته یک روزه",      150,  "phone",       "9.jpg"),
    ("net_2500_3",  "internet", "اینترنت ۲.۵ گیگ سه‌روزه",  "بسته سه روزه",      250,  "phone",       "9.jpg"),
    ("net_4000_7",  "internet", "اینترنت ۴ گیگ هفتگی",     "بسته هفت روزه",     400,  "phone",       "9.jpg"),
    ("net_5000_30", "internet", "اینترنت ۵ گیگ یک‌ماهه",    "بسته سی روزه",      500,  "phone",       "9.jpg"),
    ("gift_teddy",  "gift",     "گیفت خرس (تدی)",          "هدیه تلگرامی",       80,  "telegram_id", "10.jpg"),
    ("gift_heart",  "gift",     "گیفت قلب",                "هدیه تلگرامی",       60,  "telegram_id", "11.jpg"),
    ("gift_flower", "gift",     "گیفت گل",                 "هدیه تلگرامی",       50,  "telegram_id", "12.jpg"),
    ("gift_cake",   "gift",     "گیفت کیک",                "هدیه تلگرامی",       70,  "telegram_id", "13.jpg"),
    ("gift_ring",   "gift",     "گیفت انگشتر",             "هدیه تلگرامی",      120,  "telegram_id", "14.jpg"),
]

MISSION_SEED = [
    ("invite_5",   "دعوت ۵ عضو فعال",      "۵ نفر را با لینک اختصاصی‌ات به گروه بیاور (فقط اعضای فعال امتیاز دارند)", "invite",     5, 125, "7.jpg"),
    ("msg_100",    "۱۰۰ پیام در گروه",     "در گروه چت کن و ۱۰۰ پیام مفید بفرست",                                  "messages", 100,  50, "5.jpg"),
    ("daily_top1", "قهرمان آمار روز",      "در آمار پیام‌های روزانه نفر اول شو",                                    "daily_top",   1,  50, "17.jpg"),
    ("join_all",   "عضویت در کانال‌ها",    "در همه کانال‌های جوین اجباری عضو شو",                                    "join_all",    1,  20, "6.jpg"),
]


def db():
    global _conn
    if _conn is None:
        os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
        _conn = sqlite3.connect(DB_PATH, check_same_thread=False, timeout=30)
        _conn.row_factory = sqlite3.Row
        _conn.execute("PRAGMA journal_mode=WAL")
        _conn.execute("PRAGMA busy_timeout=15000")
        _conn.execute("PRAGMA synchronous=NORMAL")
    return _conn


def q(sql, args=(), one=False, write=False):
    """اجرای کوئری با قفل (چون miniapp.php هم به همین فایل وصل می‌شود)"""
    with _lock:
        cur = db().execute(sql, args)
        if write:
            db().commit()
        rows = cur.fetchall() if cur.description else []
        cur.close()
    if one:
        return dict(rows[0]) if rows else None
    return [dict(r) for r in rows]


def exe(sql, args=()):
    with _lock:
        cur = db().execute(sql, args)
        db().commit()
        last = cur.lastrowid
        cur.close()
    return last


def init_db():
    with _lock:
        db().executescript(SCHEMA)
        db().commit()
    for k, v in DEFAULTS.items():
        if not setting_raw(k):
            exe("INSERT OR REPLACE INTO settings(k,v) VALUES(?,?)", (k, str(v)))
    if not q("SELECT 1 FROM shop_items LIMIT 1"):
        for i, (code, cat, title, sub, price, contact, img) in enumerate(SHOP_SEED):
            exe("INSERT OR IGNORE INTO shop_items(code,category,title,subtitle,price_points,"
                "contact_kind,image,sort) VALUES(?,?,?,?,?,?,?,?)",
                (code, cat, title, sub, price, contact, img, i))
    if not q("SELECT 1 FROM missions LIMIT 1"):
        for i, (code, title, desc, kind, target, reward, img) in enumerate(MISSION_SEED):
            exe("INSERT OR IGNORE INTO missions(code,title,description,kind,target,reward,image,sort)"
                " VALUES(?,?,?,?,?,?,?,?)", (code, title, desc, kind, target, reward, img, i))
    exe("INSERT OR IGNORE INTO admins(user_id,added_at) VALUES(?,?)", (OWNER_ID, now_iso()))

    # ── ستون‌های عکس پروفایل (برای دیتابیس‌های قدیمی) ──
    for col, decl in (("photo_file_id", "TEXT DEFAULT ''"), ("photo_path", "TEXT DEFAULT ''"),
                      ("photo_at", "TEXT DEFAULT ''")):
        try:
            exe("ALTER TABLE users ADD COLUMN %s %s" % (col, decl))
        except Exception:
            pass

    # ── گروه قبلی (اگر در تنظیمات بود) به لیست گروه‌ها منتقل شود ──
    legacy = setting_raw("group_chat_id")
    if legacy and not group_row(legacy):
        exe("INSERT OR IGNORE INTO groups(chat_id,title,username,is_active,added_at) VALUES(?,?,?,?,?)",
            (str(legacy), setting_raw("group_title"), setting_raw("group_username"), 1, now_iso()))
        print("ℹ️  گروه قبلی (%s) به لیست گروه‌ها اضافه شد" % legacy)

    # ── مهاجرت نسخه ۱ → ۲ (فقط مقادیری که دست‌نخورده مانده‌اند) ──    if setting_raw("mig_v2") != "1":
        for k, oldv in MIG_V1_VALUES.items():
            if k in DEFAULTS and setting_raw(k) == oldv:
                exe("INSERT OR REPLACE INTO settings(k,v) VALUES(?,?)", (k, str(DEFAULTS[k])))
        exe("INSERT OR REPLACE INTO settings(k,v) VALUES('mig_v2','1')")
        print("ℹ️  امتیازها به قواعد نسخه ۲ به‌روزرسانی شد (پیام بدون امتیاز، دعوت ۲۵، اد ۱۵، امتیاز آمار ۵۰/۳۰/۲۰)")

    _cache["t"] = 0
    _cache["d"] = {}


_cache = {"t": 0.0, "d": {}}


def setting_raw(k):
    # کش ۲۰ ثانیه‌ای برای کم کردن فشار روی دیتابیس در گروه‌های شلوغ
    if time.time() - _cache["t"] > 20:
        rows = q("SELECT k,v FROM settings")
        _cache["d"] = {r["k"]: r["v"] for r in rows}
        _cache["t"] = time.time()
    return _cache["d"].get(k, "")


def setting(k, default=""):
    v = setting_raw(k)
    return v if v != "" else str(DEFAULTS.get(k, default))


def set_setting(k, v):
    exe("INSERT OR REPLACE INTO settings(k,v) VALUES(?,?)", (k, str(v)))
    _cache["d"][k] = str(v)
    _cache["t"] = time.time()


def setting_int(k, default=0):
    try:
        return int(float(setting(k, default)))
    except Exception:
        return default


def state_get(k, default=""):
    r = q("SELECT v FROM state WHERE k=?", (k,), one=True)
    return r["v"] if r else default


def state_set(k, v):
    exe("INSERT OR REPLACE INTO state(k,v) VALUES(?,?)", (k, str(v)))


def log(kind, user_id, text):
    exe("INSERT INTO logs(kind,user_id,text,ts) VALUES(?,?,?,?)", (kind, int(user_id or 0), text, now_iso()))


def is_admin(uid):
    if int(uid) == int(setting("owner_id", OWNER_ID)):
        return True
    return bool(q("SELECT 1 FROM admins WHERE user_id=?", (int(uid),), one=True))


# ───────────────────────────── Telegram API ─────────────────────────────

def api(method, **params):
    data = json.dumps(params, ensure_ascii=False).encode("utf-8")
    req = urllib.request.Request(API + method, data=data,
                                 headers={"Content-Type": "application/json; charset=utf-8"})
    try:
        with urllib.request.urlopen(req, timeout=70) as r:
            return json.loads(r.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        try:
            return json.loads(e.read().decode("utf-8"))
        except Exception:
            return {"ok": False, "description": "HTTP %s" % e.code}
    except Exception as e:
        return {"ok": False, "description": str(e)}


def send(chat_id, text, kb=None, parse="HTML", disable_preview=True, reply_to=None):
    p = {"chat_id": chat_id, "text": text, "parse_mode": parse,
         "link_preview_options": {"is_disabled": bool(disable_preview)}}
    if kb:
        p["reply_markup"] = kb
    if reply_to:
        p["reply_parameters"] = {"message_id": reply_to, "allow_sending_without_reply": True}
    return api("sendMessage", **p)


def edit(chat_id, message_id, text, kb=None, parse="HTML"):
    p = {"chat_id": chat_id, "message_id": message_id, "text": text, "parse_mode": parse,
         "link_preview_options": {"is_disabled": True}}
    if kb:
        p["reply_markup"] = kb
    return api("editMessageText", **p)


def answer_cb(cb_id, text="", alert=False):
    return api("answerCallbackQuery", callback_query_id=cb_id, text=text, show_alert=alert)


def esc(t):
    return html.escape(str(t or ""))


def btn(text, **kw):
    return dict(text=text, **kw)


def kb(rows):
    return {"inline_keyboard": rows}


def fmt(n):
    try:
        return "{:,}".format(int(n))
    except Exception:
        return str(n)


def user_link(uid, name=""):
    return '<a href="tg://user?id=%d">%s</a>' % (int(uid), esc(name or str(uid)))


# ───────────────────────────── کاربران ─────────────────────────────

def upsert_user(u, count_mini=False):
    """ثبت/به‌روزرسانی کاربر. ربات‌ها ذخیره نمی‌شوند."""
    if not u or u.get("is_bot"):
        return None
    uid = int(u["id"])
    row = q("SELECT * FROM users WHERE user_id=?", (uid,), one=True)
    uname = (u.get("username") or "").strip()
    fn = (u.get("first_name") or "").strip()
    ln = (u.get("last_name") or "").strip()
    prem = 1 if u.get("is_premium") else 0
    if not row:
        exe("INSERT INTO users(user_id,username,first_name,last_name,is_bot,is_premium,first_seen,last_seen)"
            " VALUES(?,?,?,?,0,?,?,?)", (uid, uname, fn, ln, prem, now_iso(), now_iso()))
        log("user_new", uid, "کاربر جدید: %s @%s" % (fn, uname))
        return {"user_id": uid, "is_new": True}
    exe("UPDATE users SET username=?,first_name=?,last_name=?,is_premium=?,last_seen=? WHERE user_id=?",
        (uname, fn, ln, prem, now_iso(), uid))
    return {"user_id": uid, "is_new": False}


def get_user(uid):
    return q("SELECT * FROM users WHERE user_id=?", (int(uid),), one=True)


def add_points(uid, amount, reason, ref="", notify=False):
    uid = int(uid)
    amount = int(amount)
    if amount == 0:
        return 0
    upsert_user({"id": uid, "first_name": ""}) if not get_user(uid) else None
    if amount > 0:
        exe("UPDATE users SET points=points+?, total_points=total_points+? WHERE user_id=?",
            (amount, amount, uid))
    else:
        exe("UPDATE users SET points=points+?, spent_points=spent_points+? WHERE user_id=?",
            (amount, -amount, uid))
    exe("INSERT INTO point_logs(user_id,amount,reason,ref,ts) VALUES(?,?,?,?,?)",
        (uid, amount, reason, str(ref), now_iso()))
    if notify and amount > 0:
        try:
            send(uid, "🎁 <b>%s امتیاز</b> گرفتی!\nدلیل: %s\nموجودی: <b>%s</b>"
                 % (fmt(amount), esc(reason), fmt(get_user(uid)["points"])))
        except Exception:
            pass
    return amount


def count_message(u, chat_id, text):
    """شمارش پیام برای آمار و امتیاز"""
    if not u or u.get("is_bot"):
        return
    uid = int(u["id"])
    upsert_user(u)
    me = get_user(uid)
    if me and me["banned"]:
        return
    day = today_str()
    n = len(text or "")
    exe("UPDATE users SET messages=messages+1, chars=chars+? WHERE user_id=?", (n, uid))
    exe("INSERT INTO daily(user_id,day,messages,chars) VALUES(?,?,1,?)"
        " ON CONFLICT(user_id,day) DO UPDATE SET messages=messages+1, chars=chars+excluded.chars",
        (uid, day, n))
    exe("INSERT INTO msg_log(user_id,chat_id,ts,day,chars) VALUES(?,?,?,?,?)",
        (uid, int(chat_id or 0), int(time.time()), day, n))
    # آمار خود گروه
    exe("UPDATE groups SET msgs=msgs+1, last_activity=? WHERE chat_id=?", (now_iso(), str(chat_id)))
    # امتیاز پیام
    if n >= setting_int("min_msg_len", 3):
        cap = setting_int("daily_msg_cap", 200)
        r = q("SELECT messages FROM daily WHERE user_id=? AND day=?", (uid, day), one=True)
        if r and r["messages"] <= cap:
            add_points(uid, setting_int("points_per_message", 1), "پیام در گروه")
    # ماموریت‌های پیام‌محور خودکار بررسی می‌شوند (در مینی‌اپ)


def record_invite(inviter_id, invited_id, kind, note=""):
    """ثبت زیرمجموعه (هر نفر فقط یک‌بار برای یک نفر امتیاز می‌آورد)"""
    inviter_id, invited_id = int(inviter_id), int(invited_id)
    if inviter_id == invited_id or not inviter_id or not invited_id or inviter_id < 0:
        return
    if get_user(invited_id) and get_user(invited_id)["is_bot"]:
        return
    # اگر همین کاربر قبلاً به حساب کسی ثبت شده (در انتظار یا تاییدشده) دوباره ثبت نشود
    dup = q("SELECT 1 FROM invites WHERE invited_id=? AND status IN ('pending','awarded') "
            "AND kind IN ('added','link','botref')", (invited_id,), one=True)
    if dup:
        return
    if q("SELECT 1 FROM invites WHERE inviter_id=? AND invited_id=? AND kind=?",
         (inviter_id, invited_id, kind), one=True):
        return
    grace = setting_int("invite_grace_hours", 12)
    pts = setting_int("points_per_member" if kind == "added" else "points_per_referral", 15)
    due = (now() + timedelta(hours=grace)).strftime("%Y-%m-%d %H:%M:%S")
    exe("INSERT OR IGNORE INTO invites(inviter_id,invited_id,kind,points,status,created_at,due_at,note)"
        " VALUES(?,?,?,?,'pending',?,?,?)",
        (inviter_id, invited_id, kind, pts, now_iso(), due, note))
    exe("UPDATE users SET referred_by=? WHERE user_id=? AND (referred_by IS NULL OR referred_by=0)",
        (inviter_id, invited_id))
    log("invite", invited_id, "ثبت %s برای %s (امتیاز %s)" % (kind, inviter_id, pts))


def invite_link_for(uid, force_new=False):
    """ساخت/برگرداندن لینک اختصاصی کاربر (لینک ربات + لینک دعوت اختصاصی گروه)"""
    uid = int(uid)
    uname = setting("bot_username") or BOT_USERNAME
    bot_link = ("https://t.me/%s?start=ref_%d" % (uname, uid)) if uname else ""
    row = q("SELECT * FROM ref_links WHERE user_id=?", (uid,), one=True)
    group_link = row["link"] if (row and row.get("link")) else ""

    gid = setting("group_chat_id")
    # اگر لینک گروه نداریم یا لینک قبلی از کار افتاده، لینک تازه می‌سازیم
    if gid and (not group_link or force_new):
        r = api("createChatInviteLink", chat_id=gid, name="ref_%d" % uid, creates_join_request=False)
        if r.get("ok"):
            group_link = r["result"]["invite_link"]
        else:
            group_link = ""      # ربات ادمین نیست یا دسترسی ندارد
    if group_link:
        exe("INSERT INTO ref_links(user_id,link,link_name,created_at) VALUES(?,?,?,?)"
            " ON CONFLICT(user_id) DO UPDATE SET link=excluded.link, link_name=excluded.link_name",
            (uid, group_link, "ref_%d" % uid, now_iso()))
    return {"bot_link": bot_link, "group_link": group_link, "link": group_link or bot_link}


def bot_id():
    return ME.get("id", 0)


def sync_me():
    global BOT_USERNAME, ME
    r = api("getMe")
    if r.get("ok"):
        ME = r["result"]
        BOT_USERNAME = ME.get("username", "")
        set_setting("bot_username", BOT_USERNAME)
        log("bot", 0, "getMe ok @%s" % BOT_USERNAME)
    else:
        log("bot", 0, "getMe خطا: %s" % r.get("description"))
    apply_menu_button()
    api("setMyCommands", commands=[
        {"command": "start", "description": "شروع / مینی‌اپ"},
        {"command": "me", "description": "پروفایل و امتیاز من"},
        {"command": "link", "description": "لینک اختصاصی من"},
        {"command": "top", "description": "برترین‌های گروه"},
        {"command": "stats", "description": "آمار گروه"},
        {"command": "dark", "description": "معرفی ربات و ورود به مینی‌اپ"},
        {"command": "diag", "description": "بررسی وضعیت آمار (مدیر)"},
        {"command": "panel", "description": "پنل مدیریت (فقط مدیران)"},
    ])


def apply_menu_button():
    """دکمه‌ی کنار چت در پیوی ربات → متنش «Khoy» است"""
    url = setting("mini_app_url")
    if not url:
        return
    api("setChatMenuButton", menu_button={
        "type": "web_app", "text": MENU_BTN_TEXT, "web_app": {"url": url}})


# ───────────────────────────── تریگر «dark» + دکمه مینی‌اپ ─────────────────────────────

_dark_last = {}          # chat_id -> زمان آخرین ارسال (ضد اسپم)

DARK_TEXT = (
    "<b>✨ ربات خوی اکیپ ✨</b>\n"
    "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\n"
    "👋 سلام! من ربات رسمی آمار و امتیاز همین گروه هستم 🤖\n\n"
    "📊 <b>آمار زنده گروه</b> — ۱۰ نفر اول پیام، روزانه و کلی\n"
    "🛒 <b>فروشگاه امتیازی</b> — شارژ، اینترنت و گیفت\n"
    "🎯 <b>ماموریت‌ها</b> — با جایزه و امتیاز\n"
    "🔗 <b>عضوگیری</b> — لینک اختصاصی دعوت خودت\n"
    "⭐️ <b>امتیاز آمار</b> — هر شب به ۳ نفر اول\n\n"
    "👇 برای ورود به مینی‌اپ، دکمه‌ی پایین را بزن:"
)


def mini_app_button(for_private=False):
    """دکمه ورود به مینی‌اپ — در پیوی web_app و در گروه لینک مستقیم"""
    url = setting("mini_app_url")
    bu = setting("bot_username") or BOT_USERNAME
    if for_private and url:
        return btn(APP_BTN_TEXT, web_app={"url": url})
    # در گروه دکمه web_app مجاز نیست → از لینک مستقیم مینی‌اپ استفاده می‌کنیم
    if bu:
        if url:
            return btn(APP_BTN_TEXT, url="https://t.me/%s?startapp=dark" % bu)
        return btn(APP_BTN_TEXT, url="https://t.me/%s" % bu)
    if url:
        return btn(APP_BTN_TEXT, url=url)
    return None


def is_dark_trigger(text):
    """آیا پیام «@آیدی‌ربات dark» است؟"""
    if not text:
        return False
    t = text.lower()
    uname = (setting("bot_username") or BOT_USERNAME or "").lower()
    if not uname:
        return False
    words = "|".join(re.escape(w) for w in DARK_WORDS)
    # @username dark   — با هر فاصله/نقطه/ویرگول بین‌شان
    if re.search(r"@" + re.escape(uname) + r"\s*[:،,.\-]*\s*(?:" + words + r")\b", t):
        return True
    # اگر ربات ریپلای شده باشد و فقط «dark» نوشته شود
    return False


def send_dark_promo(chat_id, force=False, for_private=False, reply_to=None):
    """ارسال پیام معرفی + دکمه ورود به مینی‌اپ"""
    cid = str(chat_id)
    if not force:
        last = _dark_last.get(cid, 0)
        if time.time() - last < DARK_COOLDOWN:
            return False
    _dark_last[cid] = time.time()
    b = mini_app_button(for_private=for_private)
    send(chat_id, DARK_TEXT, kb([[b]]) if b else None, reply_to=reply_to)
    log("dark", 0, "تریگر dark در %s" % cid)
    return True


# ───────────────────────────── گروه‌ها ─────────────────────────────
# آمار فقط از گروه‌هایی شمرده می‌شود که در جدول groups و وضعیتشان «فعال» باشد.
# گروه‌ها از پنل مدیریت (👥 مدیریت گروه‌ها) یا خودکار شناسایی می‌شوند.

def groups_list(active_only=False):
    sql = "SELECT * FROM groups" + (" WHERE is_active=1" if active_only else "")
    return q(sql + " ORDER BY msgs DESC, added_at")


def group_row(chat_id):
    return q("SELECT * FROM groups WHERE chat_id=?", (str(chat_id),), one=True)


def upsert_group(chat, notify=True):
    cid = str(chat.get("id"))
    title = chat.get("title") or ""
    uname = chat.get("username") or ""
    row = group_row(cid)
    if row:
        if title and (row["title"] != title or (row["username"] or "") != uname):
            exe("UPDATE groups SET title=?,username=? WHERE chat_id=?", (title, uname, cid))
            row = group_row(cid)
        return row
    auto = setting("auto_activate_groups", "1") == "1"
    exe("INSERT OR IGNORE INTO groups(chat_id,title,username,is_active,added_at) VALUES(?,?,?,?,?)",
        (cid, title, uname, 1 if auto else 0, now_iso()))
    log("group", 0, "گروه جدید: %s (%s)" % (title, cid))
    if not setting("group_chat_id"):
        set_setting("group_chat_id", cid)
        set_setting("group_title", title)
        set_setting("group_username", uname)
    if notify:
        try:
            send(setting("owner_id", OWNER_ID),
                 "🆕 <b>گروه جدید شناسایی شد</b>\n\n📛 <b>%s</b>\n🆔 <code>%s</code>\n"
                 "📊 وضعیت آمار: %s\n\nاز پنل مدیریت می‌توانی روشن/خاموشش کنی."
                 % (esc(title or cid), cid, "فعال ✅" if auto else "غیرفعال ⛔️"),
                 kb([[btn("✅ فعال کن", callback_data="g:on:" + cid),
                      btn("⛔️ غیرفعال کن", callback_data="g:off:" + cid)],
                     [btn("👥 مدیریت گروه‌ها", callback_data="p:grp")]]))
        except Exception:
            pass
    return group_row(cid)


def group_allowed(chat_id):
    """آیا پیام‌های این گروه شمرده شود؟"""
    r = group_row(chat_id)
    return bool(r and r["is_active"])


def group_title_of(chat_id):
    r = group_row(chat_id)
    return (r["title"] if r and r["title"] else str(chat_id))


def add_group_by_id(cid, title=""):
    """افزودن گروه با آیدی/@username/لینک از پنل"""
    cid = (cid or "").strip()
    if not cid:
        return None, "آیدی خالی است"
    info = api("getChat", chat_id=cid)
    if not info.get("ok"):
        return None, "پیدا نشد: %s" % info.get("description", "?")
    r = info["result"]
    if r.get("type") not in ("group", "supergroup"):
        return None, "این آیدی مربوط به گروه نیست (نوع: %s)" % r.get("type")
    g = upsert_group(r, notify=False)
    exe("UPDATE groups SET is_active=1,title=?,username=? WHERE chat_id=?",
        (r.get("title", title), r.get("username", "") or "", str(r["id"])))
    if not setting("group_chat_id"):
        set_setting("group_chat_id", str(r["id"]))
        set_setting("group_title", r.get("title", ""))
        set_setting("group_username", r.get("username", "") or "")
    log("group", 0, "گروه دستی اضافه شد: %s" % r.get("title"))
    return group_row(str(r["id"])), None


def group_members(cid):
    r = api("getChatMemberCount", chat_id=cid)
    if r.get("ok"):
        try:
            n = int(r["result"])
            exe("UPDATE groups SET members=? WHERE chat_id=?", (n, str(cid)))
            return n
        except Exception:
            return None
    return None


# ───────────────────── ضد شمارش تکراری پیام‌ها ─────────────────────

_seen_msgs = {}          # chat_id -> [message_id, ...] (آخرین ۳۰۰ پیام هر گروه)


def is_dup_message(chat_id, message_id):
    """جلوگیری از شمردن دوباره یک پیام (مثلاً بعد از ری‌استارت ربات)"""
    if not message_id:
        return False
    key = str(chat_id)
    lst = _seen_msgs.setdefault(key, [])
    if message_id in lst:
        return True
    lst.append(message_id)
    if len(lst) > 300:
        del lst[:100]
    return False


# ───────────────────────────── عکس پروفایل کاربران ─────────────────────────────

PHOTO_TTL_HOURS = 12


def refresh_photo(uid):
    """گرفتن عکس پروفایل کاربر از تلگرام و ذخیره مسیر فایل در دیتابیس"""
    uid = int(uid)
    path = ""
    r = api("getUserProfilePhotos", user_id=uid, limit=1)
    if r.get("ok") and (r.get("result") or {}).get("photos"):
        sizes = r["result"]["photos"][0]
        best = sizes[-1] if sizes else None          # بزرگ‌ترین سایز
        if best and best.get("file_id"):
            f = api("getFile", file_id=best["file_id"])
            if f.get("ok"):
                path = ((f.get("result") or {}).get("file_path") or "")
            exe("UPDATE users SET photo_file_id=? WHERE user_id=?", (best.get("file_id", ""), uid))
    exe("UPDATE users SET photo_path=?, photo_at=? WHERE user_id=?", (path, now_iso(), uid))
    return path


def photos_loop():
    """پر کردن تدریجی عکس پروفایل کاربران فعال (۱۵ نفر در هر دقیقه، محترمانه)"""
    while True:
        try:
            cut = (now() - timedelta(hours=PHOTO_TTL_HOURS)).strftime("%Y-%m-%d %H:%M:%S")
            rows = q("SELECT user_id FROM users WHERE is_bot=0 AND banned=0 "
                     "AND (photo_at IS NULL OR photo_at='' OR photo_at<?) "
                     "ORDER BY last_seen DESC LIMIT 15", (cut,))
            for r in rows:
                try:
                    refresh_photo(r["user_id"])
                except Exception:
                    pass
                time.sleep(0.35)
        except Exception:
            traceback.print_exc()
        time.sleep(60)


# ───────────────────────────── جوین اجباری ─────────────────────────────

def channels_list(active_only=True):
    sql = "SELECT * FROM channels" + (" WHERE is_active=1" if active_only else "") + " ORDER BY id"
    return q(sql)


def is_member_of(chat_id, uid):
    r = api("getChatMember", chat_id=chat_id, user_id=int(uid))
    if not r.get("ok"):
        return None                                  # نامشخص → امتیاز نمی‌دهیم
    st = (r.get("result") or {}).get("status", "")
    return st in ("creator", "administrator", "member", "restricted")


def missing_channels(uid):
    miss = []
    for ch in channels_list():
        ok = is_member_of(ch["chat_id"], uid)
        if ok is False:
            miss.append(ch)
    return miss


def check_channel_rewards(uid):
    """بررسی عضویت و دادن امتیاز (یک‌بار برای هر کانال)"""
    given = []
    for ch in channels_list():
        if q("SELECT 1 FROM channel_joins WHERE user_id=? AND channel_id=?", (int(uid), ch["id"]), one=True):
            continue
        if is_member_of(ch["chat_id"], uid) is True:
            exe("INSERT OR IGNORE INTO channel_joins(user_id,channel_id,verified_at,points) VALUES(?,?,?,?)",
                (int(uid), ch["id"], now_iso(), ch["points"]))
            add_points(uid, ch["points"], "عضویت در %s" % ch["title"], ref="ch:%s" % ch["id"])
            given.append(ch["title"])
    return given


# ───────────────────────────── آمار ─────────────────────────────

def top_chatters(day=None, limit=10):
    if day:
        rows = q("SELECT u.user_id,u.first_name,u.username,u.is_bot,d.messages,d.chars "
                 "FROM daily d JOIN users u ON u.user_id=d.user_id "
                 "WHERE d.day=? AND u.is_bot=0 AND u.banned=0 ORDER BY d.messages DESC LIMIT ?",
                 (day, int(limit)))
    else:
        rows = q("SELECT user_id,first_name,username,is_bot,messages AS messages,chars FROM users "
                 "WHERE is_bot=0 AND banned=0 ORDER BY messages DESC LIMIT ?", (int(limit),))
    return rows


def top_inviters(day=None, limit=10):
    if day:
        rows = q("SELECT u.user_id,u.first_name,u.username,COUNT(*) AS c FROM invites i "
                 "JOIN users u ON u.user_id=i.inviter_id "
                 "WHERE i.status='awarded' AND substr(i.decided_at,1,10)=? AND u.is_bot=0 "
                 "GROUP BY i.inviter_id ORDER BY c DESC LIMIT ?", (day, int(limit)))
    else:
        rows = q("SELECT u.user_id,u.first_name,u.username,COUNT(*) AS c FROM invites i "
                 "JOIN users u ON u.user_id=i.inviter_id "
                 "WHERE i.status='awarded' AND u.is_bot=0 "
                 "GROUP BY i.inviter_id ORDER BY c DESC LIMIT ?", (int(limit),))
    return rows


def stats_text(short=True):
    total = q("SELECT COUNT(*) c FROM users WHERE is_bot=0", one=True)["c"]
    today = today_str()
    act_today = q("SELECT COUNT(*) c FROM daily WHERE day=?", (today,), one=True)["c"]
    msgs_today = q("SELECT COALESCE(SUM(messages),0) s FROM daily WHERE day=?", (today,), one=True)["s"]
    msgs_all = q("SELECT COALESCE(SUM(messages),0) s FROM users WHERE is_bot=0", one=True)["s"]
    opens = q("SELECT COALESCE(SUM(mini_opens),0) s FROM users WHERE is_bot=0", one=True)["s"]
    invited = q("SELECT COUNT(*) c FROM invites WHERE status='awarded'", one=True)["c"]
    pend = q("SELECT COUNT(*) c FROM orders WHERE status='pending'", one=True)["c"]
    t = ("📊 <b>آمار ربات</b>\n"
         "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\n"
         "👥 کل کاربران: <b>%s</b>\n"
         "🟢 فعال امروز: <b>%s</b>\n"
         "💬 پیام امروز: <b>%s</b>\n"
         "🗂 کل پیام‌ها: <b>%s</b>\n"
         "📱 ورود به مینی‌اپ: <b>%s</b>\n"
         "🤝 زیرمجموعه موفق: <b>%s</b>\n"
         "🛒 سفارش در انتظار: <b>%s</b>"
         % (fmt(total), fmt(act_today), fmt(msgs_today), fmt(msgs_all), fmt(opens), fmt(invited), fmt(pend)))
    if short:
        return t
    ch = top_chatters(today, 10)
    iv = top_inviters(today, 10)
    t += "\n\n👥 <b>آمار گروه‌ها</b>\n"
    t += "\n".join("%s %s — 💬 %s پیام • 👤 %s عضو" % ("✅" if g["is_active"] else "⛔️",
                                                      esc(g["title"] or g["chat_id"]),
                                                      fmt(g["msgs"]), fmt(g["members"]))
                   for g in groups_list()) or "—"
    t += "\n\n🏆 <b>۱۰ نفر اول پیام امروز</b>\n"
    t += "\n".join("%d. %s — %s پیام" % (i + 1, user_link(r["user_id"], r["first_name"]), fmt(r["messages"]))
                   for i, r in enumerate(ch)) or "—"
    t += "\n\n🥇 <b>۱۰ نفر اول دعوت امروز</b>\n"
    t += "\n".join("%d. %s — %s نفر" % (i + 1, user_link(r["user_id"], r["first_name"]), fmt(r["c"]))
                   for i, r in enumerate(iv)) or "—"
    return t


def resolve_rank(uid):
    me = get_user(uid)
    if not me:
        return 0, 0
    rank = q("SELECT COUNT(*)+1 c FROM users WHERE is_bot=0 AND (points>? OR (points=? AND user_id<?))",
             (me["points"], me["points"], int(uid)), one=True)["c"]
    total = q("SELECT COUNT(*) c FROM users WHERE is_bot=0", one=True)["c"]
    return rank, total


# ───────────────────────────── امتیاز آمار (شبانه) ─────────────────────────────

def nightly_job(manual_day=None):
    """امتیاز آمار — هر شب به وقت ایران بعد از ساعت ۰۰:۰۰ به ۳ نفر اول پیام روزانه"""
    day = manual_day or (now() - timedelta(days=1)).strftime("%Y-%m-%d")
    chat_awards = [int(x) for x in re.findall(r"\d+", setting("night_chat_awards", "50,30,20"))] or [50, 30, 20]
    inv_awards = [int(x) for x in re.findall(r"\d+", setting("night_invite_awards", "0,0,0"))]
    label = setting("night_label", "امتیاز آمار")
    lines = ["📈 <b>%s — گزارش %s</b>" % (label, day),
             "🕐 به وقت ایران، بعد از ساعت ۰۰:۰۰", "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄"]

    lines.append("\n💬 <b>برترین‌های پیام روزانه</b>")
    for i, r in enumerate(top_chatters(day, max(len(chat_awards), 3))):
        pts = chat_awards[i] if i < len(chat_awards) else 0
        if pts:
            add_points(r["user_id"], pts, "%s رتبه %d (%s)" % (label, i + 1, day))
            exe("INSERT INTO night_rewards(day,kind,user_id,rank,points,ts) VALUES(?,?,?,?,?,?)",
                (day, "chat", r["user_id"], i + 1, pts, now_iso()))
        lines.append("%s %s — %s پیام — <b>+%s</b> امتیاز"
                     % (["🥇", "🥈", "🥉"][i] if i < 3 else "▫️",
                        user_link(r["user_id"], r["first_name"]), fmt(r["messages"]), fmt(pts)))

    if any(inv_awards):
        lines.append("\n🤝 <b>برترین‌های دعوت</b>")
        for i, r in enumerate(top_inviters(day, len(inv_awards))):
            pts = inv_awards[i] if i < len(inv_awards) else 0
            if pts:
                add_points(r["user_id"], pts, "%s دعوت رتبه %d (%s)" % (label, i + 1, day))
                exe("INSERT INTO night_rewards(day,kind,user_id,rank,points,ts) VALUES(?,?,?,?,?,?)",
                    (day, "invite", r["user_id"], i + 1, pts, now_iso()))
            lines.append("%s %s — %s نفر — <b>+%s</b> امتیاز"
                         % (["🥇", "🥈", "🥉"][i] if i < 3 else "▫️",
                            user_link(r["user_id"], r["first_name"]), fmt(r["c"]), fmt(pts)))

    txt = "\n".join(lines)
    owner = setting("owner_id", OWNER_ID)
    send(owner, txt)
    if setting("post_nightly_report", "1") == "1" and setting("group_chat_id"):
        send(setting("group_chat_id"), txt)
    for r in top_chatters(day, 3):
        try:
            send(r["user_id"], "🎉 تبریک! در <b>%s</b> روز <b>%s</b> جزو ۳ نفر اول شدی و امتیاز گرفتی." % (label, day))
        except Exception:
            pass
    log("nightly", 0, "%s %s اجرا شد" % (label, day))
    return txt


def process_pending_invites():
    """
    بررسی زیرمجموعه‌های در انتظار:
      • اگر کاربری که آورده شده «فعال» شود → امتیاز بلافاصله ثبت می‌شود (بدون انتظار)
      • اگر تا پایان مهلت هیچ فعالیتی نکرد → امتیاز نمی‌گیرد و به کاربر اطلاع داده می‌شود
    """
    rows = q("SELECT * FROM invites WHERE status='pending' ORDER BY id LIMIT 300")
    nows = now_iso()
    for r in rows:
        inv = get_user(r["invited_id"])
        active = False
        if inv:
            # کاربر فعال = پیام فرستاده یا مینی‌اپ را باز کرده
            if (inv["messages"] or 0) > 0 or (inv["mini_opens"] or 0) > 0:
                active = True
        if setting("require_activity", "1") != "1":
            active = True

        if active:
            exe("UPDATE invites SET status='awarded',decided_at=? WHERE id=?", (nows, r["id"]))
            if r["kind"] == "added":
                exe("UPDATE users SET added_count=added_count+1 WHERE user_id=?", (r["inviter_id"],))
            else:
                exe("UPDATE users SET invited_count=invited_count+1 WHERE user_id=?", (r["inviter_id"],))
            add_points(r["inviter_id"], r["points"],
                       "زیرمجموعه فعال (%s)" % ("افزودن عضو" if r["kind"] == "added" else "دعوت با لینک"),
                       ref="inv:%s" % r["invited_id"])
            try:
                send(r["inviter_id"],
                     "✅ <b>%s امتیاز</b> گرفتی!\nکاربری که آورده بودی فعال شد: %s"
                     % (fmt(r["points"]), user_link(inv["user_id"], inv["first_name"])))
            except Exception:
                pass
        elif r["due_at"] and r["due_at"] <= nows:
            exe("UPDATE invites SET status='rejected',decided_at=?,note=? WHERE id=?",
                (nows, "کاربر غیرفعال", r["id"]))
            try:
                send(r["inviter_id"],
                     "⚠️ برای کاربری که آوردی (<code>%s</code>) امتیاز ثبت نشد.\n"
                     "دلیل: <b>کاربر غیرفعال</b> است — کاربرانی که خیلی وقت پیش تلگرام آمده‌اند و "
                     "هیچ فعالیتی ندارند امتیاز نمی‌گیرند." % r["invited_id"])
            except Exception:
                pass


def cleanup_logs():
    cut = int(time.time()) - 120 * 86400
    exe("DELETE FROM msg_log WHERE ts<?", (cut,))


# ───────────────────────────── هندلر پیام ─────────────────────────────

def handle_message(m):
    chat = m.get("chat", {})
    chat_type = chat.get("type", "")
    text = m.get("text") or m.get("caption") or ""
    frm = m.get("from") or {}

    # شناسایی/به‌روزرسانی گروه (چند گروهی — از پنل مدیریت کنترل می‌شود)
    g_row = None
    if chat_type in ("group", "supergroup"):
        g_row = upsert_group(chat)

    # اعضای جدید (افزودن دستی یا ورود با لینک)
    for nm in (m.get("new_chat_members") or []):
        if nm.get("is_bot"):
            continue
        upsert_user(nm)
        adder = frm
        if adder and int(adder.get("id", 0) or 0) != int(nm.get("id", 0)) and not adder.get("is_bot") \
                and int(adder.get("id", 0) or 0) > 0:
            # کسی که عضو را اضافه (اد) کرده
            record_invite(int(adder["id"]), nm["id"], "added")
            log("join", nm["id"], "%s توسط %s اد شد" % (nm.get("first_name"), adder.get("first_name")))
        if setting("welcome_text"):
            send(chat["id"], setting("welcome_text").replace("{name}", user_link(nm["id"], nm.get("first_name"))))

    if not frm or frm.get("is_bot"):
        return
    # پیام‌های ارسالی از طرف کانال یا ادمین ناشناس شمرده نمی‌شوند
    if int(frm.get("id", 0) or 0) <= 0 or m.get("sender_chat"):
        return
    # اگر همین پیام قبلاً شمرده شده (بعد از ری‌استارت/تکرار آپدیت) دوباره شمرده نشود
    if is_dup_message(chat.get("id"), m.get("message_id")):
        return

    # ── ✨ تریگر مخفی: کسی بنویسد «@آیدی‌ربات dark» ──
    if chat_type in ("group", "supergroup"):
        if is_dark_trigger(text):
            send_dark_promo(chat.get("id"), reply_to=m.get("message_id"))
    elif chat_type == "private":
        # در پیوی: «dark» یا «خوی» یا «khoy» هم پیام مینی‌اپ را می‌فرستد
        if text.strip().lower() in ("dark", "دارک", "khoy", "خوی", "مینی اپ", "مینی‌اپ"):
            send_dark_promo(chat.get("id"), force=True, for_private=True)
            return

    # دستورات
    if text.startswith("/"):
        handle_command(m)
        if chat_type == "private" or not setting_int("count_commands", 0):
            return

    if chat_type in ("group", "supergroup"):
        # فقط گروه‌های فعال شمرده می‌شوند
        if g_row is None or g_row["is_active"]:
            # همه پیام‌ها شمرده می‌شوند: متن، عکس بدون کپشن، استیکر، ویس، فایل و…
            count_message(frm, chat.get("id"), text)


def handle_chat_member(upd):
    """ورود با لینک اختصاصی → ثبت زیرمجموعه برای صاحب لینک"""
    chat = upd.get("chat", {})
    nm = upd.get("new_chat_member") or {}
    old = upd.get("old_chat_member") or {}
    u = nm.get("user") or {}
    if u.get("is_bot"):
        return
    joined = nm.get("status") in ("member", "restricted") and old.get("status") in ("left", "kicked")
    if not joined:
        return
    upsert_user(u)
    link = (upd.get("invite_link") or {}).get("invite_link", "")
    inviter = 0
    if link:
        r = q("SELECT user_id FROM ref_links WHERE link=?", (link,), one=True)
        if r:
            inviter = r["user_id"]
    if inviter and inviter != int(u.get("id", 0)):
        record_invite(inviter, u["id"], "link", note=link)
        exe("UPDATE ref_links SET joins=joins+1 WHERE user_id=?", (inviter,))


def handle_my_chat_member(upd):
    chat = upd.get("chat", {})
    nm = upd.get("new_chat_member") or {}
    if nm.get("status") in ("member", "administrator"):
        if chat.get("type") in ("group", "supergroup"):
            upsert_group(chat)
        send(setting("owner_id", OWNER_ID),
             "🤖 ربات به <b>%s</b> اضافه شد.\n\n"
             "برای شمردن پیام‌ها: ۱) در BotFather پرایوسی را <b>Disable</b> کن "
             "۲) ربات را ادمین گروه کن ۳) داخل گروه <code>/addgroup</code> را بزن.\n"
             "وضعیت را با <code>/diag</code> ببین." % esc(chat.get("title")))
    elif nm.get("status") in ("left", "kicked"):
        if chat.get("type") in ("group", "supergroup"):
            exe("UPDATE groups SET is_active=0 WHERE chat_id=?", (str(chat.get("id")),))
        if str(chat.get("id")) == str(setting("group_chat_id")):
            send(setting("owner_id", OWNER_ID),
                 "⚠️ ربات از گروه <b>%s</b> حذف شد! (آمار این گروه خودکار غیرفعال شد)" % esc(chat.get("title")))


# ───────────────────────────── دستورات ─────────────────────────────

def main_kb(uid):
    url = setting("mini_app_url")
    rows = []
    if url:
        rows.append([btn("🚀 باز کردن مینی‌اپ", web_app={"url": url})])
    rows.append([btn("👤 پروفایل من", callback_data="u:profile"),
                 btn("🏆 برترین‌ها", callback_data="u:top")])
    rows.append([btn("🔗 لینک اختصاصی من", callback_data="u:link"),
                 btn("🛒 فروشگاه", callback_data="u:shop")])
    if is_admin(uid):
        rows.append([btn("⚙️ پنل مدیریت", callback_data="p:home")])
    return kb(rows)


def handle_command(m):
    chat = m.get("chat", {})
    frm = m.get("from") or {}
    uid = int(frm["id"])
    text = (m.get("text") or "").strip()
    parts = text.split()
    cmd = parts[0].split("@")[0].lower()
    payload = parts[1] if len(parts) > 1 else ""
    is_group = chat.get("type") in ("group", "supergroup")

    if cmd == "/start":
        is_new = upsert_user(frm)
        if payload.startswith("ref_"):
            try:
                inviter = int(payload[4:])
                if inviter != uid:
                    record_invite(inviter, uid, "botref", note="deep-link")
                    send(inviter, "🔔 یک نفر با لینک اختصاصی‌ات ربات را استارت کرد.\n"
                                  "پس از فعال شدن او، امتیاز به حسابت اضافه می‌شود.")
            except Exception:
                pass
        miss = missing_channels(uid)
        if miss:
            rows = [[btn("📣 " + (c["title"] or "کانال"), url=(c["invite_link"] or "https://t.me/" + (c["username"] or "")))]
                    for c in miss]
            rows.append([btn("✅ عضو شدم، امتیاز بده", callback_data="u:joined")])
            send(uid, "🔒 برای استفاده از امکانات، اول در این کانال‌ها عضو شو:", kb(rows))
            return
        got = check_channel_rewards(uid)
        txt = setting("start_text").replace("{name}", esc(frm.get("first_name") or "دوست من"))
        if got:
            txt += "\n\n🎁 امتیاز عضویت کانال‌ها اضافه شد: <b>+%s</b>" % fmt(
                sum(c["points"] for c in channels_list() if c["title"] in got))
        if is_group:
            return
        send(uid, txt, main_kb(uid))

    elif cmd == "/me":
        me = get_user(uid)
        if not me:
            send(uid, "اول /start را بزن.")
            return
        rank, total = resolve_rank(uid)
        send(chat["id"], "👤 <b>%s</b>\nرتبه: <b>%s</b> از %s\nامتیاز: <b>%s</b>\n"
                         "پیام‌ها: <b>%s</b>\nاعضای اضافه‌شده: <b>%s</b>\nزیرمجموعه: <b>%s</b>\n"
                         "ورود به مینی‌اپ: <b>%s</b>"
             % (esc(me["first_name"]), fmt(rank), fmt(total), fmt(me["points"]), fmt(me["messages"]),
                fmt(me["added_count"]), fmt(me["invited_count"]), fmt(me["mini_opens"])),
             reply_to=m.get("message_id"))

    elif cmd == "/link":
        L = invite_link_for(uid)
        send(uid, "🔗 <b>لینک اختصاصی تو</b>\n\n"
                  "<b>لینک دعوت گروه:</b>\n<code>%s</code>\n\n<b>لینک ربات:</b>\n<code>%s</code>\n\n"
                  "هر کسی با این لینک بیاید و فعال باشد، امتیازش برای تو ثبت می‌شود."
             % (esc(L["group_link"] or "—"), esc(L["bot_link"] or "—")))

    elif cmd == "/top":
        rows = top_chatters(today_str(), 10)
        t = "🏆 <b>۱۰ نفر اول چت امروز</b>\n" + "\n".join(
            "%d. %s — %s" % (i + 1, user_link(r["user_id"], r["first_name"]), fmt(r["messages"]))
            for i, r in enumerate(rows))
        iv = top_inviters(today_str(), 5)
        if iv:
            t += "\n\n🤝 <b>برترین دعوت‌کننده‌های امروز</b>\n" + "\n".join(
                "%d. %s — %s نفر" % (i + 1, user_link(r["user_id"], r["first_name"]), fmt(r["c"]))
                for i, r in enumerate(iv))
        send(chat["id"], t, reply_to=m.get("message_id"))

    elif cmd == "/stats":
        send(chat["id"], stats_text(short=True))

    elif cmd == "/id":
        send(chat["id"], "🆔 آیدی عددی تو: <code>%d</code>" % uid, reply_to=m.get("message_id"))

    elif cmd == "/setgroup":
        if not is_admin(uid):
            return
        if is_group:
            set_setting("group_chat_id", str(chat["id"]))
            set_setting("group_title", chat.get("title", ""))
            send(chat["id"], "✅ این گروه به عنوان گروه اصلی ثبت شد.")
        else:
            fwd = m.get("reply_to_message") or {}
            if fwd.get("sender_chat"):
                set_setting("group_chat_id", str(fwd["sender_chat"]["id"]))
                send(uid, "✅ گروه ثبت شد: %s" % esc(fwd["sender_chat"].get("title")))
            else:
                send(uid, "این دستور را داخل گروه بزن یا روی یک پیام فورواردشده از گروه ریپلای کن.")

    elif cmd == "/setmenu":
        if not is_admin(uid):
            return
        url = payload or setting("mini_app_url")
        if not url:
            send(uid, "لینک بده: <code>/setmenu https://example.com/miniapp.php</code>")
            return
        set_setting("mini_app_url", url)
        r = api("setChatMenuButton", menu_button={"type": "web_app", "text": MENU_BTN_TEXT,
                                                 "web_app": {"url": url}})
        send(uid, "✅ لینک مینی‌اپ ثبت شد.\n%s" % ("دکمه منو تنظیم شد." if r.get("ok") else r.get("description")))

    elif cmd in ("/panel", "/admin"):
        if not is_admin(uid):
            send(uid, "⛔️ دسترسی نداری.")
            return
        t, k = panel_home()
        send(uid, t, k)

    elif cmd == "/night":
        if not is_admin(uid):
            return
        send(uid, "در حال اجرای امتیاز آمار… 📈")
        threading.Thread(target=nightly_job, daemon=True).start()

    elif cmd == "/dark":
        send_dark_promo(chat["id"], force=True, for_private=(chat.get("type") == "private"))

    elif cmd == "/addgroup":
        if not is_admin(uid):
            return
        if is_group:
            upsert_group(chat, notify=False)
            exe("UPDATE groups SET is_active=1,title=? WHERE chat_id=?",
                (chat.get("title", ""), str(chat["id"])))
            if not setting("group_chat_id"):
                set_setting("group_chat_id", str(chat["id"]))
                set_setting("group_title", chat.get("title", ""))
            send(chat["id"], "✅ آمار این گروه فعال شد.\n\n📛 <b>%s</b>\n🆔 <code>%s</code>"
                 % (esc(chat.get("title")), chat["id"]))
            return
        fw = (m.get("reply_to_message") or {}).get("forward_from_chat") or \
             (m.get("reply_to_message") or {}).get("sender_chat") or m.get("forward_from_chat") or {}
        if fw.get("id"):
            g, err = add_group_by_id(str(fw["id"]))
            send(uid, ("✅ گروه فعال شد:\n<b>%s</b>" % esc(g["title"])) if not err else ("❌ " + esc(err)))
            return
        ask(uid, "grp_add",
            "➕ <b>افزودن گروه</b>\n\n"
            "بهترین راه: دستور <code>/addgroup</code> را <b>داخل خود گروه</b> بزن.\n\n"
            "یا اینجا آیدی عددی گروه را بفرست (مثال <code>-1001234567890</code>) یا @username گروه.\n"
            "(لغو: /cancel)")

    elif cmd == "/groups":
        if not is_admin(uid):
            return
        t, k = panel_section(uid, "grp")
        send(chat["id"], t, k)

    elif cmd == "/diag":
        if not is_admin(uid):
            return
        today = today_str()
        row = q("SELECT COALESCE(SUM(messages),0) m, COUNT(*) u FROM daily WHERE day=?", (today,), one=True)
        users_cnt = q("SELECT COUNT(*) c FROM users WHERE is_bot=0", one=True)["c"]
        msgs_all = q("SELECT COALESCE(SUM(messages),0) s FROM users WHERE is_bot=0", one=True)["s"]
        ofs = int(state_get("last_update_id", "0") or 0)
        gs = groups_list()
        glines = []
        for g in gs:
            r = api("getChatMember", chat_id=g["chat_id"], user_id=ME.get("id", 0))
            st = ((r.get("result") or {}).get("status", "?") if r.get("ok") else "?")
            glines.append("%s <b>%s</b>\n     🆔 <code>%s</code>\n     🤖 ربات: <b>%s</b> • 💬 %s پیام%s"
                          % ("✅" if g["is_active"] else "⛔️", esc(g["title"] or g["chat_id"]),
                             g["chat_id"], esc(st), fmt(g["msgs"]),
                             "" if g["is_active"] else " <i>(غیرفعال — شمرده نمی‌شود)</i>"))
        last = q("SELECT user_id,first_name,messages,last_seen FROM users WHERE is_bot=0 "
                 "ORDER BY last_seen DESC LIMIT 5")
        t = ("🔍 <b>وضعیت آمار ربات</b>\n┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\n"
             "📅 پیام امروز (%s): <b>%s</b> از <b>%s</b> کاربر\n"
             "🗂 کل پیام‌های ثبت‌شده: <b>%s</b>\n👤 کل کاربران: <b>%s</b>\n"
             "🔢 آخرین آپدیت پردازش‌شده: <code>%s</code>\n\n"
             "👥 <b>گروه‌ها:</b>\n%s\n\n"
             "🕐 آخرین کاربران:\n%s\n\n"
             "<i>اگر «ربات» در گروهی administrator نیست یا پرایوسی روشن است، پیام‌ها شمرده نمی‌شوند.</i>") % (
            today, fmt(row["m"]), fmt(row["u"]), fmt(msgs_all), fmt(users_cnt), ofs,
            "\n".join(glines) or "— هیچ گروهی ثبت نشده —",
            "\n".join("• %s — %s پیام — %s" % (user_link(r["user_id"], r["first_name"]),
                                               fmt(r["messages"]), esc(r["last_seen"])) for r in last) or "—")
        send(chat["id"], t, kb([[btn("👥 مدیریت گروه‌ها", callback_data="p:grp")]]))

    elif cmd == "/help":
        send(chat["id"], "📖 دستورات:\n/start — شروع و مینی‌اپ\n/me — پروفایل\n/link — لینک اختصاصی\n"
                         "/top — برترین‌ها\n/stats — آمار\n/addgroup — افزودن این گروه به آمار\n"
                         "/groups — مدیریت گروه‌ها (مدیر)\n/diag — بررسی وضعیت آمار (مدیر)\n"
                         "/id — آیدی عددی تو", reply_to=m.get("message_id"))


# ───────────────────────────── پنل مدیریت ─────────────────────────────

def panel_home():
    gs = groups_list()
    act = [g for g in gs if g["is_active"]]
    t = ("⚙️ <b>پنل مدیریت ربات</b>\n"
         "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\n"
         "👤 مالک: <code>%s</code>\n🌐 مینی‌اپ: %s\n"
         "👥 گروه‌ها: <b>%s</b> فعال از %s\n\n"
         "یک بخش را انتخاب کن:"
         % (setting("owner_id", OWNER_ID),
            "✅ ثبت شده" if setting("mini_app_url") else "❌ ثبت نشده (با /setmenu بگذار)",
            fmt(len(act)), fmt(len(gs))))
    k = kb([
        [btn("👥 مدیریت گروه‌ها", callback_data="p:grp"), btn("➕ افزودن گروه", callback_data="p:grpadd")],
        [btn("📊 آمار کامل", callback_data="p:stats"), btn("👤 کاربران", callback_data="p:users:0")],
        [btn("🛒 فروشگاه", callback_data="p:shop"), btn("📦 سفارش‌ها", callback_data="p:orders")],
        [btn("📣 کانال‌های اجباری", callback_data="p:ch"), btn("🎯 ماموریت‌ها", callback_data="p:mis")],
        [btn("⚙️ تنظیمات امتیاز", callback_data="p:set"), btn("👮 ادمین‌ها", callback_data="p:adm")],
        [btn("📤 پیام همگانی", callback_data="p:bc"), btn("🧾 لاگ‌ها", callback_data="p:log")],
        [btn("📈 اجرای دستی امتیاز آمار", callback_data="p:night")],
    ])
    return t, k


SETTINGS_MENU = [
    ("points_per_message", "امتیاز هر پیام (۰ = بدون امتیاز)"),
    ("points_per_member", "امتیاز افزودن عضو (اد)"),
    ("points_per_referral", "امتیاز دعوت با لینک"),
    ("min_msg_len", "حداقل طول پیام"),
    ("daily_msg_cap", "سقف پیام روزانه"),
    ("invite_grace_hours", "مهلت بررسی فعال بودن (ساعت)"),
    ("require_activity", "فقط کاربر فعال امتیاز بگیرد (1/0)"),
    ("night_label", "نام بخش امتیاز شبانه"),
    ("night_chat_awards", "امتیاز آمار شبانه پیام (مثال 50,30,20)"),
    ("night_invite_awards", "امتیاز آمار شبانه دعوت (مثال 0,0,0)"),
    ("post_nightly_report", "ارسال گزارش شبانه در گروه (1/0)"),
    ("count_commands", "شمارش دستورات به عنوان پیام (1/0)"),
    ("auto_activate_groups", "فعال‌سازی خودکار گروه جدید (1/0)"),
    ("mini_app_url", "لینک مینی‌اپ"),
]


def panel_section(uid, sec, arg=""):
    if sec == "home":
        return panel_home()

    # ─────────── مدیریت گروه‌ها ───────────
    if sec == "grp":
        rows = groups_list()
        main = setting("group_chat_id")
        t = ("👥 <b>مدیریت گروه‌ها</b>\n"
             "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\n"
             "آمار فقط از گروه‌های <b>فعال ✅</b> شمرده می‌شود.\n"
             "⭐️ گروه اصلی (برای لینک دعوت): <b>%s</b>\n\n" % esc(group_title_of(main) if main else "تعیین نشده"))
        if not rows:
            t += "هنوز گروهی ثبت نشده. ربات را در گروه ادمین کن و یک پیام بفرست، یا «➕ افزودن گروه» را بزن."
        krows = []
        for g in rows:
            t += "%s <b>%s</b>\n   🆔 <code>%s</code> • 💬 %s پیام • 👤 %s عضو%s\n" % (
                "✅" if g["is_active"] else "⛔️", esc(g["title"] or g["chat_id"]), g["chat_id"],
                fmt(g["msgs"]), fmt(g["members"]),
                " • ⭐️ اصلی" if str(g["chat_id"]) == str(main) else "")
            krows.append([btn("%s %s — %s پیام" % ("✅" if g["is_active"] else "⛔️",
                                                   (g["title"] or g["chat_id"])[:22], fmt(g["msgs"])),
                              callback_data="p:g:%s" % g["chat_id"])])
        krows.append([btn("➕ افزودن گروه", callback_data="p:grpadd")])
        krows.append([btn("⬅️ بازگشت", callback_data="p:home")])
        return t, kb(krows)

    if sec == "g":
        g = group_row(arg)
        if not g:
            return "گروه پیدا نشد.", kb([[btn("⬅️ بازگشت", callback_data="p:grp")]])
        group_members(g["chat_id"])                      # بروزرسانی تعداد اعضا
        g = group_row(arg)
        r = api("getChatMember", chat_id=g["chat_id"], user_id=ME.get("id", 0))
        st = ((r.get("result") or {}).get("status", "?") if r.get("ok")
              else ("خطا: %s" % r.get("description")))
        t = ("👥 <b>%s</b>\n┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\n"
             "🆔 <code>%s</code>\n🔗 @%s\n"
             "📊 آمار این گروه: %s\n🤖 وضعیت ربات در گروه: <b>%s</b>\n"
             "💬 پیام‌های ثبت‌شده: <b>%s</b>\n👤 تعداد اعضا: <b>%s</b>\n"
             "🕐 آخرین فعالیت: %s\n📅 افزوده شده: %s\n\n"
             "<i>اگر وضعیت ربات administrator نباشد، یا در BotFather پرایوسی روشن باشد، "
             "پیام‌ها شمرده نمی‌شوند.</i>") % (
            esc(g["title"] or g["chat_id"]), g["chat_id"], esc(g["username"] or "—"),
            "فعال ✅" if g["is_active"] else "غیرفعال ⛔️", esc(st), fmt(g["msgs"]),
            fmt(g["members"]), esc(g["last_activity"] or "—"), esc(g["added_at"]))
        k = kb([
            [btn("⛔️ غیرفعال کن" if g["is_active"] else "✅ فعال کن",
                 callback_data="g:%s:%s" % ("off" if g["is_active"] else "on", g["chat_id"]))],
            [btn("⭐️ گروه اصلی شود", callback_data="p:gmain:%s" % g["chat_id"])],
            [btn("🔄 بروزرسانی", callback_data="p:g:%s" % g["chat_id"]),
             btn("🗑 حذف از لیست", callback_data="p:gdel:%s" % g["chat_id"])],
            [btn("⬅️ بازگشت", callback_data="p:grp")],
        ])
        return t, k

    if sec == "stats":
        t = stats_text(short=False)
        return t, kb([[btn("🔄 بروزرسانی", callback_data="p:stats")], [btn("⬅️ بازگشت", callback_data="p:home")]])

    if sec == "users":
        page = int(arg or 0)
        per = 8
        rows = q("SELECT * FROM users WHERE is_bot=0 ORDER BY last_seen DESC LIMIT ? OFFSET ?", (per, page * per))
        total = q("SELECT COUNT(*) c FROM users WHERE is_bot=0", one=True)["c"]
        t = "👥 <b>کاربران</b> (صفحه %d از %d — کل %s)\n┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\n" % (
            page + 1, max(1, (total + per - 1) // per), fmt(total))
        krows = []
        for r in rows:
            t += "• %s | ⭐️%s | 💬%s | 📱%s\n" % (
                user_link(r["user_id"], r["first_name"]), fmt(r["points"]), fmt(r["messages"]), fmt(r["mini_opens"]))
            krows.append([btn("%s — %s ⭐️" % (r["first_name"] or r["user_id"], fmt(r["points"])),
                              callback_data="p:user:%d" % r["user_id"])])
        nav = []
        if page > 0:
            nav.append(btn("⬅️ قبلی", callback_data="p:users:%d" % (page - 1)))
        if (page + 1) * per < total:
            nav.append(btn("بعدی ➡️", callback_data="p:users:%d" % (page + 1)))
        if nav:
            krows.append(nav)
        krows.append([btn("🔎 جستجوی کاربر", callback_data="p:usearch")])
        krows.append([btn("⬅️ بازگشت", callback_data="p:home")])
        return t, kb(krows)

    if sec == "user":
        r = get_user(int(arg))
        if not r:
            return "کاربر پیدا نشد.", kb([[btn("⬅️", callback_data="p:users:0")]])
        rank, tot = resolve_rank(r["user_id"])
        t = ("👤 <b>%s</b> (<code>%s</code>)\n┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\n"
             "⭐️ امتیاز: <b>%s</b>\n🏅 رتبه: <b>%s</b> از %s\n"
             "💬 پیام‌ها: <b>%s</b>\n📱 ورود به مینی‌اپ: <b>%s</b>\n"
             "➕ اعضای اضافه‌شده: <b>%s</b>\n🔗 زیرمجموعه: <b>%s</b>\n"
             "🕐 آخرین فعالیت: %s\n🚫 بن: %s"
             % (esc(r["first_name"] or r["user_id"]), r["user_id"], fmt(r["points"]), fmt(rank), fmt(tot),
                fmt(r["messages"]), fmt(r["mini_opens"]), fmt(r["added_count"]), fmt(r["invited_count"]),
                esc(r["last_seen"]), "بله" if r["banned"] else "خیر"))
        k = kb([[btn("➕ دادن امتیاز", callback_data="p:addp:%d" % r["user_id"]),
                 btn("➖ کم کردن", callback_data="p:subp:%d" % r["user_id"])],
                [btn("🚫 بن" if not r["banned"] else "✅ آنبن", callback_data="p:ban:%d" % r["user_id"])],
                [btn("⬅️ بازگشت", callback_data="p:users:0")]])
        return t, k

    if sec == "shop":
        items = q("SELECT * FROM shop_items ORDER BY category, sort")
        t = "🛒 <b>فروشگاه</b> — روی هر مورد بزن تا قیمتش را عوض کنی\n┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\n"
        krows = []
        for it in items:
            t += "%s %s — %s امتیاز %s\n" % ("✅" if it["is_active"] else "⛔️", it["title"],
                                             fmt(it["price_points"]),
                                             "(شماره تلفن)" if it["contact_kind"] == "phone" else "(آیدی تلگرام)")
            krows.append([btn("%s — %s" % (it["title"], fmt(it["price_points"])),
                              callback_data="p:item:%d" % it["id"])])
        krows.append([btn("⬅️ بازگشت", callback_data="p:home")])
        return t, kb(krows)

    if sec == "item":
        it = q("SELECT * FROM shop_items WHERE id=?", (int(arg),), one=True)
        if not it:
            return "پیدا نشد", kb([[btn("⬅️", callback_data="p:shop")]])
        t = "🛒 <b>%s</b>\nقیمت: <b>%s امتیاز</b>\nوضعیت: %s\nورودی لازم: %s" % (
            esc(it["title"]), fmt(it["price_points"]), "فعال" if it["is_active"] else "غیرفعال",
            "شماره تلفن" if it["contact_kind"] == "phone" else "آیدی تلگرام")
        k = kb([[btn("✏️ تغییر قیمت", callback_data="p:itemprice:%d" % it["id"]),
                 btn("🔁 فعال/غیرفعال", callback_data="p:itemtoggle:%d" % it["id"])],
                [btn("⬅️ بازگشت", callback_data="p:shop")]])
        return t, k

    if sec == "orders":
        rows = q("SELECT * FROM orders WHERE status='pending' ORDER BY id DESC LIMIT 15")
        t = "📦 <b>سفارش‌های در انتظار تایید</b> (%d)\n┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\n" % len(rows)
        krows = []
        for r in rows:
            t += "#%d | %s | %s | %s | %s\n" % (r["id"], esc(r["item_title"]),
                                                user_link(r["user_id"]), esc(r["contact_value"]), esc(r["created_at"]))
            krows.append([btn("✅ تایید #%d" % r["id"], callback_data="o:ok:%d" % r["id"]),
                          btn("❌ رد #%d" % r["id"], callback_data="o:no:%d" % r["id"])])
        if not rows:
            t += "سفارشی در انتظار نیست."
        krows.append([btn("⬅️ بازگشت", callback_data="p:home")])
        return t, kb(krows)

    if sec == "ch":
        rows = channels_list(active_only=False)
        t = ("📣 <b>کانال‌های جوین اجباری</b>\n"
             "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\n"
             "برای افزودن بزن «➕»، سپس به این شکل بفرست:\n"
             "<code>@username | عنوان | امتیاز</code>\n"
             "یا: <code>-1001234567890 | عنوان | امتیاز</code>\n"
             "یا: <code>https://t.me/+AbCdEf | عنوان | امتیاز</code>\n\n")
        krows = []
        for c in rows:
            t += "%s %s — %s امتیاز\n" % ("✅" if c["is_active"] else "⛔️", esc(c["title"] or c["chat_id"]), fmt(c["points"]))
            krows.append([btn("%s — %s" % (c["title"] or c["chat_id"], fmt(c["points"])),
                              callback_data="p:chitem:%d" % c["id"])])
        krows.append([btn("➕ افزودن کانال", callback_data="p:chadd")])
        krows.append([btn("⬅️ بازگشت", callback_data="p:home")])
        return t, kb(krows)

    if sec == "chitem":
        c = q("SELECT * FROM channels WHERE id=?", (int(arg),), one=True)
        if not c:
            return "پیدا نشد", kb([[btn("⬅️", callback_data="p:ch")]])
        t = "📣 <b>%s</b>\nشناسه: <code>%s</code>\nامتیاز: <b>%s</b>\nوضعیت: %s" % (
            esc(c["title"]), esc(c["chat_id"]), fmt(c["points"]), "فعال" if c["is_active"] else "غیرفعال")
        k = kb([[btn("✏️ تغییر امتیاز", callback_data="p:chpoints:%d" % c["id"]),
                 btn("🔁 فعال/غیرفعال", callback_data="p:chtoggle:%d" % c["id"])],
                [btn("🗑 حذف", callback_data="p:chdel:%d" % c["id"])],
                [btn("⬅️ بازگشت", callback_data="p:ch")]])
        return t, k

    if sec == "mis":
        rows = q("SELECT * FROM missions ORDER BY sort")
        t = "🎯 <b>ماموریت‌ها</b>\n┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\n"
        krows = []
        for m_ in rows:
            t += "%s %s — هدف %s — جایزه %s امتیاز (%s)\n" % (
                "✅" if m_["is_active"] else "⛔️", esc(m_["title"]), fmt(m_["target"]),
                fmt(m_["reward"]), esc(m_["kind"]))
            krows.append([btn("%s — %s ⭐️" % (m_["title"], fmt(m_["reward"])),
                              callback_data="p:mi:%d" % m_["id"])])
        krows.append([btn("➕ افزودن ماموریت", callback_data="p:miadd")])
        krows.append([btn("⬅️ بازگشت", callback_data="p:home")])
        return t, kb(krows)

    if sec == "mi":
        m_ = q("SELECT * FROM missions WHERE id=?", (int(arg),), one=True)
        if not m_:
            return "پیدا نشد", kb([[btn("⬅️", callback_data="p:mis")]])
        t = "🎯 <b>%s</b>\n%s\nنوع: %s | هدف: %s | جایزه: %s امتیاز\nوضعیت: %s" % (
            esc(m_["title"]), esc(m_["description"]), esc(m_["kind"]), fmt(m_["target"]),
            fmt(m_["reward"]), "فعال" if m_["is_active"] else "غیرفعال")
        k = kb([[btn("✏️ جایزه", callback_data="p:mireward:%d" % m_["id"]),
                 btn("🎯 هدف", callback_data="p:mitarget:%d" % m_["id"])],
                [btn("🔁 فعال/غیرفعال", callback_data="p:mitoggle:%d" % m_["id"]),
                 btn("🗑 حذف", callback_data="p:midel:%d" % m_["id"])],
                [btn("⬅️ بازگشت", callback_data="p:mis")]])
        return t, k

    if sec == "set":
        t = "⚙️ <b>تنظیمات امتیاز و ربات</b>\n┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\n"
        krows = []
        for key, label in SETTINGS_MENU:
            t += "• %s: <b>%s</b>\n" % (label, esc(setting(key)))
            krows.append([btn("%s: %s" % (label, setting(key)[:25]), callback_data="p:setk:%s" % key)])
        krows.append([btn("⬅️ بازگشت", callback_data="p:home")])
        return t, kb(krows)

    if sec == "adm":
        rows = q("SELECT * FROM admins ORDER BY added_at")
        t = ("👮 <b>ادمین‌ها</b>\nمالک اصلی: <code>%s</code>\n┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\n" % setting("owner_id", OWNER_ID))
        krows = []
        for r in rows:
            t += "• <code>%s</code>\n" % r["user_id"]
            if int(r["user_id"]) != int(setting("owner_id", OWNER_ID)):
                krows.append([btn("🗑 حذف %s" % r["user_id"], callback_data="p:admdel:%s" % r["user_id"])])
        krows.append([btn("➕ افزودن ادمین", callback_data="p:admadd")])
        krows.append([btn("⬅️ بازگشت", callback_data="p:home")])
        return t, kb(krows)

    if sec == "log":
        rows = q("SELECT * FROM logs ORDER BY id DESC LIMIT 20")
        t = "🧾 <b>آخرین رویدادها</b>\n┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\n" + "\n".join(
            "• [%s] %s" % (esc(r["kind"]), esc(r["text"])[:90]) for r in rows)
        return t, kb([[btn("🔄 بروزرسانی", callback_data="p:log")], [btn("⬅️ بازگشت", callback_data="p:home")]])

    return panel_home()


# ─────────────── گفتگوهای مرحله‌ای پنل (FSM) ───────────────

def ask(uid, state, prompt, data=None):
    FSM[int(uid)] = {"state": state, "data": data or {}}
    send(uid, prompt)


def handle_fsm(m):
    uid = int(m["from"]["id"])
    st = FSM.get(uid)
    if not st:
        return False
    text = (m.get("text") or "").strip()
    state, data = st["state"], st["data"]
    FSM.pop(uid, None)
    if text in ("/cancel", "لغو"):
        send(uid, "لغو شد.")
        return True

    n = int(re.sub(r"\D", "", text) or 0)
    if state == "grp_add":
        cid = ""
        fw = (m.get("reply_to_message") or {}).get("forward_from_chat") or \
             (m.get("reply_to_message") or {}).get("sender_chat") or \
             m.get("forward_from_chat") or {}
        if fw.get("id"):
            cid = str(fw["id"])
        else:
            cid = text.strip()
        g, err = add_group_by_id(cid)
        if err:
            send(uid, "❌ %s\n\nآیدی عددی گروه را بفرست (مثال <code>-1001234567890</code>) "
                      "یا دستور /addgroup را داخل گروه بزن." % esc(err))
        else:
            send(uid, "✅ گروه اضافه و <b>فعال</b> شد:\n\n📛 <b>%s</b>\n🆔 <code>%s</code>\n\n"
                      "از این لحظه پیام‌های این گروه شمرده می‌شود."
                 % (esc(g["title"] or g["chat_id"]), g["chat_id"]),
                 kb([[btn("👥 مدیریت گروه‌ها", callback_data="p:grp")]]))
        return True
    if state == "ch_points":
        exe("UPDATE channels SET points=? WHERE id=?", (n, data["cid"]))
        send(uid, "✅ امتیاز کانال: %s" % fmt(n))
        return True
    if state == "item_price":
        exe("UPDATE shop_items SET price_points=? WHERE id=?", (n, data["iid"]))
        send(uid, "✅ قیمت جدید ثبت شد: %s امتیاز" % fmt(n))
        return True
    if state == "mi_reward":
        exe("UPDATE missions SET reward=? WHERE id=?", (n, data["mid"]))
        send(uid, "✅ جایزه جدید: %s امتیاز" % fmt(n))
        return True
    if state == "mi_target":
        exe("UPDATE missions SET target=? WHERE id=?", (n, data["mid"]))
        send(uid, "✅ هدف جدید: %s" % fmt(n))
        return True

    if state == "set_value":
        set_setting(data["key"], text)
        send(uid, "✅ <b>%s</b> ثبت شد: <b>%s</b>" % (data["label"], esc(text)))
        if data["key"] == "mini_app_url":
            apply_menu_button()
        return True

    if state == "ch_add":
        parts = [p.strip() for p in text.replace("|", "|").split("|")]
        if len(parts) < 2:
            send(uid, "❌ فرمت درست نیست. مثال:\n<code>@mychannel | کانال من | 5</code>")
            return True
        cid, title = parts[0], parts[1]
        pts = int(re.sub(r"\D", "", parts[2]) or 5) if len(parts) > 2 else 5
        username = ""
        invite = ""
        if cid.startswith("http"):
            invite = cid
            r = api("getChat", chat_id=cid)
            cid = str(r["result"]["id"]) if r.get("ok") else cid
        elif cid.startswith("@"):
            username = cid[1:]
        chk = api("getChat", chat_id=cid)
        if chk.get("ok"):
            title = title or chk["result"].get("title", "")
            username = username or chk["result"].get("username", "")
        exe("INSERT INTO channels(chat_id,title,username,invite_link,points,is_active,created_at)"
            " VALUES(?,?,?,?,?,1,?)", (str(cid), title, username, invite, pts, now_iso()))
        send(uid, "✅ کانال «%s» با %d امتیاز اضافه شد." % (esc(title), pts))
        return True

    if state == "mi_add":
        parts = [p.strip() for p in text.split("|")]
        if len(parts) < 4:
            send(uid, "❌ فرمت: <code>عنوان | نوع | هدف | جایزه</code>\n"
                      "نوع‌ها: invite (دعوت) | messages (پیام) | daily_top (رتبه چت روز) | join_all (کانال‌ها)")
            return True
        title, kind, target, reward = parts[0], parts[1], int(re.sub(r"\D", "", parts[2]) or 1), \
            int(re.sub(r"\D", "", parts[3]) or 10)
        code = "m_%d" % int(time.time())
        exe("INSERT INTO missions(code,title,description,kind,target,reward,sort) VALUES(?,?,?,?,?,?,?)",
            (code, title, "ماموریت ساخته‌شده توسط مدیریت", kind, target, reward, int(time.time()) % 1000))
        send(uid, "✅ ماموریت «%s» اضافه شد." % esc(title))
        return True

    if state == "adm_add":
        v = re.sub(r"\D", "", text)
        if v:
            exe("INSERT OR IGNORE INTO admins(user_id,added_at) VALUES(?,?)", (int(v), now_iso()))
            send(uid, "✅ ادمین <code>%s</code> اضافه شد." % v)
        return True

    if state == "addp" or state == "subp":
        v = int(re.sub(r"\D", "", text) or 0)
        sign = 1 if state == "addp" else -1
        add_points(data["uid"], sign * v, "تنظیم دستی توسط مدیریت")
        u = get_user(data["uid"])
        send(uid, "✅ انجام شد. موجودی جدید: <b>%s</b>" % fmt(u["points"]))
        try:
            send(data["uid"], "🎁 مدیریت <b>%s%s امتیاز</b> به حساب تو اضافه کرد."
                 % ("+" if sign > 0 else "-", fmt(v)))
        except Exception:
            pass
        return True

    if state == "bc":
        threading.Thread(target=broadcast, args=(text,), daemon=True).start()
        send(uid, "📤 ارسال همگانی شروع شد…")
        return True

    if state == "usearch":
        v = re.sub(r"\D", "", text)
        r = get_user(int(v)) if v else None
        if not r:
            send(uid, "کاربر پیدا نشد.")
            return True
        send(uid, "کاربر پیدا شد: %s" % user_link(r["user_id"], r["first_name"]))
        t, k = panel_section(uid, "user", str(r["user_id"]))
        send(uid, t, k)
        return True

    return True


def broadcast(text):
    users = q("SELECT user_id FROM users WHERE is_bot=0 AND banned=0")
    ok = 0
    for u in users:
        r = send(u["user_id"], text)
        if r.get("ok"):
            ok += 1
        time.sleep(0.06)
    send(setting("owner_id", OWNER_ID), "📤 ارسال همگانی تمام شد: %d از %d" % (ok, len(users)))


# ───────────────────────────── دکمه‌ها ─────────────────────────────

def handle_callback(c):
    cb_id = c["id"]
    uid = int(c["from"]["id"])
    data = c.get("data") or ""
    msg = c.get("message") or {}
    chat_id = (msg.get("chat") or {}).get("id")
    mid = msg.get("message_id")
    answer_cb(cb_id)

    if data.startswith("u:"):
        act = data[2:]
        if act == "profile":
            me = get_user(uid) or {}
            rank, tot = resolve_rank(uid)
            edit(chat_id, mid, "👤 <b>%s</b>\nرتبه: <b>%s</b> از %s\n⭐️ امتیاز: <b>%s</b>\n"
                               "💬 پیام‌ها: <b>%s</b>\n➕ اعضا: <b>%s</b>\n🔗 زیرمجموعه: <b>%s</b>"
                 % (esc(me.get("first_name", "")), fmt(rank), fmt(tot), fmt(me.get("points", 0)),
                    fmt(me.get("messages", 0)), fmt(me.get("added_count", 0)), fmt(me.get("invited_count", 0))),
                 kb([[btn("⬅️ بازگشت", callback_data="u:home")]]))
        elif act == "top":
            rows = top_chatters(today_str(), 10)
            t = "🏆 <b>۱۰ نفر اول چت امروز</b>\n" + "\n".join(
                "%d. %s — %s" % (i + 1, user_link(r["user_id"], r["first_name"]), fmt(r["messages"]))
                for i, r in enumerate(rows))
            edit(chat_id, mid, t, kb([[btn("⬅️ بازگشت", callback_data="u:home")]]))
        elif act == "link":
            L = invite_link_for(uid)
            edit(chat_id, mid, "🔗 <b>لینک اختصاصی تو</b>\n\n<code>%s</code>\n\nهر نفر فعالی که بیاورد = امتیاز"
                 % esc(L["link"] or L["bot_link"]), kb([[btn("⬅️ بازگشت", callback_data="u:home")]]))
        elif act == "shop":
            url = setting("mini_app_url")
            k = kb([[btn("🚀 باز کردن مینی‌اپ", web_app={"url": url})]]) if url else None
            edit(chat_id, mid, "🛒 برای خرید از فروشگاه، مینی‌اپ را باز کن.\n%s" % esc(setting("shop_note")), k)
        elif act == "joined":
            got = check_channel_rewards(uid)
            miss = missing_channels(uid)
            if miss:
                answer_cb(cb_id, "هنوز عضو این‌ها نشدی: " + "، ".join(m["title"] for m in miss), alert=True)
            else:
                send(uid, "✅ عضویت تایید شد." + ("\n🎁 امتیاز: <b>+%s</b>" % fmt(sum(
                    ch["points"] for ch in channels_list() if ch["title"] in got)) if got else ""),
                     main_kb(uid))
        else:
            edit(chat_id, mid, setting("start_text").replace("{name}", esc(c["from"].get("first_name") or "")),
                 main_kb(uid))
        return

    # ── گروه‌ها (از پیام اطلاع‌رسانی «گروه جدید» هم می‌آید) ──
    if data.startswith("g:"):
        if not is_admin(uid):
            answer_cb(cb_id, "⛔️ دسترسی نداری", alert=True)
            return
        parts = data.split(":")
        if len(parts) >= 3:
            act, cid = parts[1], parts[2]
            exe("UPDATE groups SET is_active=? WHERE chat_id=?", (1 if act == "on" else 0, cid))
            if act == "on" and not setting("group_chat_id"):
                set_setting("group_chat_id", cid)
                set_setting("group_title", group_title_of(cid))
            txt = ("%s\n\nگروه: <b>%s</b>\n🆔 <code>%s</code>\n📊 وضعیت آمار: <b>%s</b>"
                   % ("✅ فعال شد" if act == "on" else "⛔️ غیرفعال شد",
                      esc(group_title_of(cid)), cid, "روشن" if act == "on" else "خاموش"))
            if chat_id and mid:
                edit(chat_id, mid, txt, kb([[btn("👥 مدیریت گروه‌ها", callback_data="p:grp")]]))
            else:
                send(uid, txt)
        return

    # سفارش
    if data.startswith("o:"):
        act = data.split(":")
        if len(act) < 3:
            return
        oid = int(act[2])
        o = q("SELECT * FROM orders WHERE id=?", (oid,), one=True)
        if not o:
            answer_cb(cb_id, "سفارش پیدا نشد", alert=True)
            return
        if not is_admin(uid):
            answer_cb(cb_id, "⛔️ فقط مدیریت", alert=True)
            return
        if act[1] == "ok":
            if o["status"] == "pending":
                # امتیاز هنگام ثبت سفارش (در مینی‌اپ) کسر شده است؛ اینجا فقط وضعیت را عوض می‌کنیم
                exe("UPDATE orders SET status='confirmed',updated_at=? WHERE id=?", (now_iso(), oid))
                send(o["user_id"], "✅ <b>سفارش شما تایید شد!</b>\n\n🛒 %s\n💳 امتیاز کسرشده: %s\n"
                                   "%s: <code>%s</code>\n\nمدیریت به‌زودی سفارش را تحویل می‌دهد."
                     % (esc(o["item_title"]), fmt(o["price_points"]), "شماره" if o["contact_kind"] == "phone" else "آیدی",
                        esc(o["contact_value"])))
            edit(chat_id, mid, "✅ سفارش #%d تایید شد.\n%s | %s" % (oid, esc(o["item_title"]), user_link(o["user_id"])),
                 kb([[btn("⬅️ سفارش‌ها", callback_data="p:orders")]]))
        else:
            if o["status"] == "pending":
                exe("UPDATE orders SET status='rejected',updated_at=? WHERE id=?", (now_iso(), oid))
                exe("UPDATE users SET points=points+?, spent_points=spent_points-? WHERE user_id=?",
                    (o["price_points"], o["price_points"], o["user_id"]))
                exe("INSERT INTO point_logs(user_id,amount,reason,ref,ts) VALUES(?,?,?,?,?)",
                    (o["user_id"], int(o["price_points"]), "بازگشت امتیاز سفارش رد‌شده #%d" % oid, "refund", now_iso()))
                send(o["user_id"], "❌ سفارش شما رد شد و <b>%s امتیاز</b> به حسابت برگشت.\n"
                                   "برای اطلاعات بیشتر به مدیریت پیام بده." % fmt(o["price_points"]))
            edit(chat_id, mid, "❌ سفارش #%d رد شد." % oid, kb([[btn("⬅️ سفارش‌ها", callback_data="p:orders")]]))
        return

    # پنل مدیریت
    if not data.startswith("p:"):
        return
    if not is_admin(uid):
        answer_cb(cb_id, "⛔️ دسترسی نداری", alert=True)
        return
    parts = data.split(":")
    sec = parts[1] if len(parts) > 1 else "home"
    arg = parts[2] if len(parts) > 2 else ""

    if sec == "grpadd":
        ask(uid, "grp_add",
            "➕ <b>افزودن گروه</b>\n\n"
            "بهترین راه: همین دستور <code>/addgroup</code> را <b>داخل خود گروه</b> بزن.\n\n"
            "یا اینجا آیدی عددی گروه را بفرست (مثال <code>-1001234567890</code>) یا @username گروه.\n"
            "ربات باید در آن گروه ادمین باشد.\n(لغو: /cancel)")
        return
    if sec == "gmain":
        g = group_row(arg)
        set_setting("group_chat_id", str(arg))
        set_setting("group_title", g["title"] if g else "")
        set_setting("group_username", (g["username"] if g else "") or "")
        t, k = panel_section(uid, "g", arg)
        edit(chat_id, mid, "⭐️ گروه اصلی تعیین شد: <b>%s</b>\n\n%s" % (esc(group_title_of(arg)), t), k)
        return
    if sec == "gdel":
        exe("DELETE FROM groups WHERE chat_id=?", (str(arg),))
        if str(setting("group_chat_id")) == str(arg):
            set_setting("group_chat_id", "")
        t, k = panel_section(uid, "grp")
        edit(chat_id, mid, t, k)
        return
    if sec == "grpall":
        rows = groups_list()
        newv = 0 if all(g["is_active"] for g in rows) else 1
        exe("UPDATE groups SET is_active=?", (newv,))
        t, k = panel_section(uid, "grp")
        edit(chat_id, mid, t, k)
        return

    if sec == "night":
        send(uid, "در حال اجرا…")
        threading.Thread(target=nightly_job, daemon=True).start()
        return
    if sec == "bc":
        ask(uid, "bc", "📤 متن پیام همگانی را بفرست (برای لغو: /cancel)")
        return
    if sec == "usearch":
        ask(uid, "usearch", "🔎 آیدی عددی کاربر را بفرست:")
        return
    if sec == "admadd":
        ask(uid, "adm_add", "👮 آیدی عددی ادمین جدید را بفرست:")
        return
    if sec == "admdel":
        exe("DELETE FROM admins WHERE user_id=?", (int(arg),))
        t, k = panel_section(uid, "adm")
        edit(chat_id, mid, t, k)
        return
    if sec == "addp":
        ask(uid, "addp", "➕ چند امتیاز اضافه شود؟", {"uid": int(arg)})
        return
    if sec == "subp":
        ask(uid, "subp", "➖ چند امتیاز کم شود؟", {"uid": int(arg)})
        return
    if sec == "ban":
        u = get_user(int(arg))
        exe("UPDATE users SET banned=? WHERE user_id=?", (0 if u["banned"] else 1, int(arg)))
        t, k = panel_section(uid, "user", arg)
        edit(chat_id, mid, t, k)
        return
    if sec == "setk":
        label = dict(SETTINGS_MENU).get(arg, arg)
        ask(uid, "set_value", "✏️ مقدار جدید برای <b>%s</b> را بفرست:\n(فعلی: <code>%s</code>)"
            % (label, esc(setting(arg))), {"key": arg, "label": label})
        return
    if sec == "chadd":
        ask(uid, "ch_add", "📣 کانال را به این شکل بفرست:\n<code>@username | عنوان | امتیاز</code>")
        return
    if sec == "chpoints":
        FSM[uid] = {"state": "ch_points", "data": {"cid": int(arg)}}
        send(uid, "✏️ امتیاز جدید این کانال را بفرست (عدد):")
        return
    if sec == "chtoggle":
        exe("UPDATE channels SET is_active=1-is_active WHERE id=?", (int(arg),))
        t, k = panel_section(uid, "chitem", arg)
        edit(chat_id, mid, t, k)
        return
    if sec == "chdel":
        exe("DELETE FROM channels WHERE id=?", (int(arg),))
        t, k = panel_section(uid, "ch")
        edit(chat_id, mid, t, k)
        return
    if sec == "itemprice":
        ask(uid, "item_price", "✏️ قیمت جدید (امتیاز) را بفرست:", {"iid": int(arg)})
        return
    if sec == "itemtoggle":
        exe("UPDATE shop_items SET is_active=1-is_active WHERE id=?", (int(arg),))
        t, k = panel_section(uid, "item", arg)
        edit(chat_id, mid, t, k)
        return
    if sec == "miadd":
        ask(uid, "mi_add", "🎯 فرمت: <code>عنوان | نوع | هدف | جایزه</code>\n"
                           "نوع‌ها: invite | messages | daily_top | join_all")
        return
    if sec == "mireward":
        ask(uid, "mi_reward", "✏️ جایزه جدید (امتیاز):", {"mid": int(arg)})
        return
    if sec == "mitarget":
        ask(uid, "mi_target", "🎯 هدف جدید (عدد):", {"mid": int(arg)})
        return
    if sec == "mitoggle":
        exe("UPDATE missions SET is_active=1-is_active WHERE id=?", (int(arg),))
        t, k = panel_section(uid, "mi", arg)
        edit(chat_id, mid, t, k)
        return
    if sec == "midel":
        exe("DELETE FROM missions WHERE id=?", (int(arg),))
        t, k = panel_section(uid, "mis")
        edit(chat_id, mid, t, k)
        return

    t, k = panel_section(uid, sec, arg)
    edit(chat_id, mid, t, k)


# ───────────────────────────── حلقه اصلی ─────────────────────────────

def dispatch(upd):
    try:
        if "message" in upd:
            m = upd["message"]
            if m.get("from") and not m["from"].get("is_bot") and is_admin(m["from"]["id"]) \
                    and (m.get("chat", {}).get("type") == "private"):
                if handle_fsm(m):
                    return
            handle_message(m)
        elif "callback_query" in upd:
            handle_callback(upd["callback_query"])
        elif "chat_member" in upd:
            handle_chat_member(upd["chat_member"])
        elif "my_chat_member" in upd:
            handle_my_chat_member(upd["my_chat_member"])
    except Exception:
        log("error", 0, traceback.format_exc()[-800:])
        traceback.print_exc()


def schedules_loop():
    """امتیاز آمار شبانه + بررسی زیرمجموعه‌های در انتظار + پاکسازی"""
    while True:
        try:
            n = now()
            target_hour = setting_int("night_hour", 0)
            if n.hour == target_hour and n.minute < 10:
                key = "nightly_" + n.strftime("%Y-%m-%d")
                if state_get(key) != "1":
                    state_set(key, "1")
                    nightly_job()
            process_pending_invites()
            # درخواست اجرای امتیاز آمار از مینی‌اپ (miniapp.php این کلید را ست می‌کند)
            req = setting_raw("night_run_request")
            if req and state_get("night_run_handled") != req:
                state_set("night_run_handled", req)
                nightly_job()
            if n.hour == 4 and n.minute < 5:
                key = "clean_" + n.strftime("%Y-%m-%d")
                if state_get(key) != "1":
                    state_set(key, "1")
                    cleanup_logs()
        except Exception:
            traceback.print_exc()
        time.sleep(45)


def poll_loop():
    # ★ مهم برای دقت آمار: آخرین update_id ذخیره می‌شود تا بعد از ری‌استارت،
    #   آپدیت‌های قدیمی دوباره پردازش (و پیام‌ها دوباره شمرده) نشوند.
    offset = int(state_get("last_update_id", "0") or 0)
    if offset:
        print("ℹ️  ادامه از آپدیت شماره %d (آمار قبلی دوباره شمرده نمی‌شود)" % offset)
    allowed = ["message", "callback_query", "chat_member", "my_chat_member", "edited_message"]
    while True:
        try:
            r = api("getUpdates", offset=offset, timeout=30, allowed_updates=allowed)
            if not r.get("ok"):
                print("⚠️ getUpdates:", r.get("description"))
                time.sleep(3)
                continue
            for upd in r["result"]:
                offset = upd["update_id"] + 1
                state_set("last_update_id", str(offset))     # ذخیره فوری برای دقت آمار
                dispatch(upd)
        except Exception:
            traceback.print_exc()
            time.sleep(3)


def main():
    if not BOT_TOKEN or ":" not in BOT_TOKEN:
        raise SystemExit("❌ BOT_TOKEN خالی یا نامعتبر است")
    init_db()
    sync_me()
    me = api("getMe")
    if not me.get("ok"):
        raise SystemExit("❌ توکن نامعتبر است: %s" % me.get("description"))
    print("=" * 60)
    print("🤖 ربات روشن شد | @%s" % BOT_USERNAME)
    print("🌐 مینی‌اپ: %s" % (setting("mini_app_url") or "❌ ثبت نشده (با /setmenu بگذار)"))
    print("👥 گروه‌های فعال: %s" % (
        "، ".join("%s (%s)" % (g["title"] or g["chat_id"], g["msgs"]) for g in groups_list(True)) or "❌ هیچ گروهی فعال نیست"))
    print("🗄 دیتابیس: %s" % DB_PATH)
    print("👤 مالک: %s" % setting("owner_id", OWNER_ID))
    print("⭐️ امتیازها → پیام: %s | دعوت با لینک: %s | اد کردن: %s | امتیاز آمار: %s"
          % (setting("points_per_message", "0"), setting("points_per_referral", "25"),
             setting("points_per_member", "15"), setting("night_chat_awards", "50,30,20")))
    print("🖼  عکس پروفایل کاربران: در حال دریافت تدریجی (هر دقیقه ۱۵ نفر)")
    print("✨ تریگر مخفی: @%s dark  →  پیام معرفی + دکمه «%s»" % (BOT_USERNAME, APP_BTN_TEXT))
    print("🎛  دکمه کنار چت (پیوی): %s" % MENU_BTN_TEXT)
    print("=" * 60)
    if setting("mini_app_url"):
        apply_menu_button()
    else:
        send(setting("owner_id", OWNER_ID),
             "🤖 ربات روشن شد.\n\n⚠️ لینک مینی‌اپ ثبت نشده!\n"
             "در فایل bot.py متغیر <code>MINI_APP_URL</code> را پر کن یا اینجا بزن:\n"
             "<code>/setmenu https://دامنه-تو/miniapp.php</code>")
    threading.Thread(target=schedules_loop, daemon=True).start()
    threading.Thread(target=photos_loop, daemon=True).start()
    poll_loop()


if __name__ == "__main__":
    main()