# main.py
# Python 3.10+
# pip install python-telegram-bot==20.7 qrcode[pil]

import os
import re
import json
import zipfile
import shutil
import random
import asyncio
import urllib.request
import urllib.parse
from io import BytesIO
from datetime import datetime, timedelta
from pathlib import Path
from typing import List, Optional, Dict, Any

from telegram import (
    Update,
    InlineKeyboardMarkup,
    InlineKeyboardButton,
    ReplyKeyboardMarkup,
    InputFile,
)
from telegram.constants import ParseMode
from telegram.ext import (
    ApplicationBuilder,
    CommandHandler,
    CallbackQueryHandler,
    MessageHandler,
    ContextTypes,
    filters,
)

# =========================
# CONFIG 
# =========================
BOT_TOKEN = "8341937631:AAFjEeMznGE59HAOfzOHEM8f14UFsAXuYgc"

ADMIN_IDS = {540916392,7731331455}  #admin ids inja

# Payment (USDT TRC20)
RECEIVE_TRON_ADDRESS = "TCdEyX5DcSezM7A8NJCfoEKFRAxadYZP3k"  # tron adres
TRONGRID_API_KEY = ""  # optional
TRONGRID_BASE = "https://api.trongrid.io"
USDT_TRC20_CONTRACT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
LOG_CHANNEL_ID = "@loglogloghypersesszszsdszd"   # <-- اینو عوض کن
INVOICE_EXPIRE_MIN = 30
AMOUNT_BUMP_MIN = 0.01
AMOUNT_BUMP_MAX = 0.70

TOPUP_AMOUNTS = [5, 10, 20, 50, 100, 300, 500, 1000]
SUPPORT_CONTACT = "@Hypersess"

BASE_DIR = Path("data")
DB_FILE = BASE_DIR / "db.json"
PRODUCTS_DIR = BASE_DIR / "products"
TMP_DIR = BASE_DIR / "tmp"

# ✅ Global lock to prevent db.json overwrite / race conditions
DB_LOCK = asyncio.Lock()

# ======================
# I18N
# ========================
TEXT = {
    "EN": {
        "main_menu": "Main menu ✅",
        "welcome": """💎Our Business💎

Telegram accounts, protocol accounts, direct login (tdata) wholesale / retail!
_____________________________________

❗️ For those who have never used our products, please make small purchases for testing first to avoid unnecessary disputes! Thank you for your cooperation!

❗️ Disclaimer: All our products are for entertainment and testing purposes only and must not be used for illegal activities! Please comply with local laws and regulations!

_____________________________________

☎️ Customer Service: @Hypersess
🏦 Channel: @HyperSession

⚙️ /start   ⬅️ Click command to open bottom menu!
""",

        "products_list": "🛒 Products List",
        "topup": "💰 Top-up",
        "profile": "👤 Profile",
        "lang": "🌍 Change Language",
        "help": "📖 Help and Rules",
        "support": "👮‍♀️ Contact Support",
        "admin_panel": "🛠 Admin Panel",

        "products_title": "🛒 Products List — Select an option from below👇(S+J+Tdata)",
        "buy": "🛍 Buy",
        "back": "⬅️ Back",

        "note": (
            "⚠️ Note: If you're a new buyer, please start with a small test purchase to ensure everything works perfectly.\n"
            "Your satisfaction is our priority — thank you for choosing us! 💖"
        ),

        "not_found": "❌ Product not found",
        "enter_qty": "Enter quantity (number):",
        "qty_number": "❌ Please send a number (e.g. 10)",
        "qty_positive": "❌ Quantity must be > 0",
        "not_enough_stock": "❌ Not enough stock",
        "not_enough_balance": "❌ Not enough balance",

        "order_done_caption": (
            "✅ Your order has been successfully completed!\n\n"
            "• Product: {name}\n"
            "• Quantity: {qty}\n"
            "• Total Price: {total} USDT\n"
            "{pw_line}\n"
            "⏱ {time}"
        ),

        "topup_title": "💰 Top-up",
        "select_amount": "📥 Please select the recharge order amount below(USDT):",
        "custom_amount": "Custom Amount",
        "send_amount": "Send amount (number):",
        "amount_invalid": "❌ Please send a valid amount (e.g. 100)",

        "invoice_caption": (
            "📩 *Your payment invoice has been successfully created.*\n\n"
            "• Cryptocurrency: USDT (TRC20)\n"
            "• Amount: `{amount}`\n"
            "• Invoice ID: `{invoice_id}`\n\n"
            "❗ *Please send the exact amount to the wallet address below*\n"
            "`{address}`\n\n"
            "⚠️ If you don't send the exact amount, your invoice will not be confirmed.\n\n"
            "⏱ Invoice expires in {minutes} minutes."
        ),
        "paid_btn": "📩 Paid",
        "ask_tx": "Please send your Transaction Hash (TxID):",
        "tx_invalid": "❌ Tx hash format looks wrong. Please send a valid TxID (64 hex).",
        "tx_not_found": (
            "❌ Payment not found / not confirmed / details mismatch.\n"
            "Make sure:\n"
            "- USDT TRC20\n"
            "- To our address\n"
            "- Exact amount\n"
            "- Confirmed on-chain\n\n"
            "You can send another TxID."
        ),
        "tx_ok": "📥 Your payment has been confirmed and {amount} USDT added.\n\n⏱ {time}",
        "tx_used": "❌ This TxID has already been used.",

        "profile_text": (
            "👤 Your account details:\n\n"
            "• Your ID: {id}\n"
            "• Your Username: {username}\n"
            "• Joined date-time: {joined}\n\n"
            "• Balance: ${bal}\n"
            "• Total purchase quantity: {pur}\n\n"
            "⏱ {now}"
        ),

        "help_text": (
            "📖 Help and Rules\n"
            "- Always send the exact invoice amount.\n"
            "- Goods are delivered as ZIP.\n"
            "- New buyers: test with small quantity first.\n"
        ),
        "support_text": """👮‍♀️ ☎️ Customer service: @hypersess

🔔 Official restocking notice: https://t.me/HyperSession

🤖 Self-service pickup robot : https://t.me/hypersession_bot""",

        "lang_choose": "Select language:",
        "lang_set": "✅ Language updated.",
        "lang_en": "🇺🇸 English",
        "lang_zh": "🇨🇳 中文",

        "admin_title": "🛠 Admin Panel",
        "admin_add_country": "➕ Add Country",
        "admin_remove_country": "🗑 Remove Country",
        "admin_change_price": "💰 Change Price",
        "admin_upload_stock": "📦 Upload Stock (ZIP folders)",
        "admin_reduce_stock": "🧹 Reduce Stock",
        "admin_charge_user": "💳 Charge User",
        "admin_members": "🧑‍🤝‍🧑 Members Count",
        "admin_stock": "📦 Stock Status",
        "admin_back": "⬅️ Back to Main Menu",

        "admin_members_text": "🧑‍🤝‍🧑 Members: {n}",
        "admin_add_prompt": (
            "➕ Add Country\nSend like this:\n"
            "PK|🇵🇰 Pakistan +92|0.38\n\n"
            "(code|name_with_flag|price)\n"
        ),
        "admin_add_exists": "❌ This country code already exists.",
        "admin_add_ok": "✅ Added: {code} {name} price={price}",

        "admin_del_select": "Select a country to remove:",
        "admin_del_confirm": "⚠️ Remove {code} ({name})?\nThis will remove product and its files.",
        "yes_delete": "✅ Yes, remove",
        "no_cancel": "❌ Cancel",
        "admin_del_done": "✅ Removed {code}.",
        "admin_del_cancel": "Cancelled ✅",

        "admin_price_select": "Select country to change price:",
        "admin_price_send": "Send new price for {code} (number):",
        "admin_price_invalid": "❌ Send a valid price (e.g. 0.38)",
        "admin_price_ok": "✅ Price updated for {code}: {price}",

        "admin_upload_select": "Select a country to upload ZIP stock (ZIP must contain folders).",
        "invoice_expired": "❌ This invoice has expired. Please create a new invoice and pay again.",
        "admin_upload_sendzip": (
            "📦 Now send ZIP for {code}\n"
            "✅ Each TOP-LEVEL folder inside ZIP = 1 item.\n"
            "⚠️ Don't put files loose at root; must be folders."
        ),
        "admin_zip_need": "❌ Please send a ZIP file as document.",
        "admin_zip_ext": "❌ File must be .zip",
        "admin_zip_empty": "❌ No top-level folders found in ZIP.",

        # ✅ NEW: password ask after zip...
        "admin_pw_ask": (
            "🔐 Send *2FA/Second Password* for this uploaded batch.\n"
            "Send `-` if you want it empty."
        ),
        "admin_pw_saved": "✅ Password saved. Uploading stock now...",

        "admin_zip_ok": "✅ Uploaded.\n• Added items: {added}\n• Stock now: {stock}",

        "admin_reduce_select": "Select a country to reduce stock:",
        "admin_reduce_send": "Send amount to remove from stock for {code} (number):",
        "admin_reduce_invalid": "❌ Send a valid number.",
        "admin_reduce_done": "✅ Removed {removed} items. Stock now: {stock}",

        "admin_charge_prompt": (
            "💳 Charge User\nSend like this:\n"
            "user_id|amount\nExample:\n"
            "123456789|50"
        ),
        "admin_charge_fmt": "❌ Format wrong. Example: 123456789|50",
        "admin_charge_nouser": "❌ User not found in db (must /start first).",
        "admin_charge_ok": "✅ Charged {uid} by {amt}. New balance={bal}",

        "admin_stock_text": "📦 Stock Status:\n{lines}",
        "unknown": "I didn't understand. Use menu buttons ✅",
    },

    "ZH": {
        "main_menu": "主菜单 ✅",
        "welcome": """💎我们的业务💎

Telegram 账号、协议账号、直登（tdata）批发/零售！
_____________________________________

❗️ 对于从未使用过我们产品的用户，请先进行小额测试购买，以避免不必要的纠纷！感谢您的配合！

❗️ 免责声明：我们所有产品仅供娱乐和测试用途，严禁用于任何非法活动！请遵守当地法律法规！

_____________________________________

☎️ 客服：@hypersess
🏦 频道：@HyperSession

⚙️ /start   ⬅️ 点击指令打开底部菜单！
"""
,

        "products_list": "🛒 产品列表",
        "topup": "💰 充值",
        "profile": "👤 个人资料",
        "lang": "🌍 切换语言",
        "help": "📖 帮助与规则",
        "support": "👮‍♀️ 联系客服",
        "admin_panel": "🛠 管理面板",

        "products_title": "🛒 商品列表 — 请从下方选择 👇 (S+J+Tdata)",
        "buy": "🛍 购买",
        "back": "⬅️ 返回",

        "note": (
            "⚠️ 温馨提示：如果你是新买家，建议先小额测试购买，确保一切正常。\n"
            "你的满意是我们的优先 — 感谢选择我们！💖"
        ),

        "not_found": "❌ 未找到产品",
        "enter_qty": "请输入数量（数字）：",
        "qty_number": "❌ 请输入数字（例如 10）",
        "qty_positive": "❌ 数量必须大于 0",
        "not_enough_stock": "❌ 库存不足",
        "not_enough_balance": "❌ 余额不足",

        "order_done_caption": (
            "✅ 订单已完成！\n\n"
            "• 产品: {name}\n"
            "• 数量: {qty}\n"
            "• 总价: {total} USDT\n"
            "{pw_line}\n"
            "⏱ {time}"
        ),

        "topup_title": "💰 充值",
        "select_amount": "📥 请选择充值金额：",
        "custom_amount": "自定义金额",
        "send_amount": "请输入金额（数字）：",
        "amount_invalid": "❌ 请输入有效金额（例如 100）",

        "invoice_caption": (
            "📩 *已创建充值订单*\n\n"
            "• 币种: USDT (TRC20)\n"
            "• 金额: `{amount}`\n"
            "• 订单号: `{invoice_id}`\n\n"
            "❗ *请向以下地址发送“精确金额”*\n"
            "`{address}`\n\n"
            "⚠️ 如果金额不完全一致，订单将无法确认。\n\n"
            "⏱ 订单有效期 {minutes} 分钟。"
        ),
        "paid_btn": "📩 已支付",
        "ask_tx": "请发送交易哈希（TxID）：",
        "tx_invalid": "❌ TxID 格式不正确，请发送 64 位十六进制 TxID。",
        "tx_not_found": (
            "❌ 未找到/未确认/信息不匹配。\n"
            "请确认：\n"
            "- USDT TRC20\n"
            "- 转入我们的地址\n"
            "- 金额完全一致\n"
            "- 链上已确认\n\n"
            "你可以继续发送另一个 TxID。"
        ),
        "tx_ok": "📥 已确认支付并添加 {amount} USDT。\n\n⏱ {time}",
        "tx_used": "❌ 此 TxID 已被使用。",

        "profile_text": (
            "👤 个人资料:\n\n"
            "• 你的ID: {id}\n"
            "• 用户名: {username}\n"
            "• 注册时间: {joined}\n\n"
            "• 余额: ${bal}\n"
            "• 购买总数量: {pur}\n\n"
            "⏱ {now}"
        ),

        "help_text": (
            "📖 帮助与规则\n"
            "- 请发送精确金额。\n"
            "- 商品以 ZIP 形式交付。\n"
            "- 新买家建议先小额测试。\n"
        ),
        "support_text": """👮‍♀️ ☎️ 客服：@hypersess

🔔 官方补货通知：https://t.me/HyperSession

🤖 自助取货机器人：https://t.me/hypersession_bot
""",

        "lang_choose": "请选择语言：",
        "lang_set": "✅ 语言已更新。",
        "lang_en": "🇺🇸 English",
        "lang_zh": "🇨🇳 中文",

        "admin_title": "🛠 管理面板",
        "admin_add_country": "➕ 添加国家",
        "admin_remove_country": "🗑 删除国家",
        "admin_change_price": "💰 修改价格",
        "admin_upload_stock": "📦 上传库存（ZIP 文件夹）",
        "admin_reduce_stock": "🧹 减少库存",
        "admin_charge_user": "💳 给用户充值",
        "admin_members": "🧑‍🤝‍🧑 用户数",
        "admin_stock": "📦 库存状态",
        "admin_back": "⬅️ 返回主菜单",

        "admin_members_text": "🧑‍🤝‍🧑 用户数: {n}",
        "admin_add_prompt": (
            "➕ 添加国家\n按此格式发送：\n"
            "PK|🇵🇰 Pakistan +92|0.38\n\n"
            "(代码|名称(含旗帜)|价格)\n"
        ),
        "admin_add_exists": "❌ 国家代码已存在。",
        "admin_add_ok": "✅ 已添加: {code} {name} 价格={price}",

        "admin_del_select": "选择要删除的国家：",
        "admin_del_confirm": "⚠️ 确认删除 {code} ({name})？\n将删除产品及文件。",
        "yes_delete": "✅ 确认删除",
        "no_cancel": "❌ 取消",
        "admin_del_done": "✅ 已删除 {code}。",
        "admin_del_cancel": "已取消 ✅",

        "admin_price_select": "选择要修改价格的国家：",
        "admin_price_send": "请输入 {code} 新价格（数字）：",
        "admin_price_invalid": "❌ 请输入有效价格（例如 0.38）",
        "admin_price_ok": "✅ {code} 价格已更新: {price}",

        "admin_upload_select": "选择国家并上传库存 ZIP（ZIP 内必须包含文件夹）。",
        "invoice_expired": "❌ 该订单已过期，请重新创建充值订单后再支付。",

        "admin_upload_sendzip": (
            "📦 现在发送 {code} 的 ZIP\n"
            "✅ ZIP 顶层每个文件夹 = 1 个商品。\n"
            "⚠️ 不要把文件散放在根目录；必须是文件夹。"
        ),
        "admin_zip_need": "❌ 请以 document 形式发送 ZIP。",
        "admin_zip_ext": "❌ 文件必须是 .zip",
        "admin_zip_empty": "❌ ZIP 内未找到顶层文件夹。",

        # ✅NEW
        "admin_pw_ask": (
            "🔐 请发送此批次的 *二次密码/2FA*。\n"
            "如果不需要请输入 `-`"
        ),
        "admin_pw_saved": "✅ 密码已保存。正在入库...",

        "admin_zip_ok": "✅ 上传成功。\n• 新增: {added}\n• 当前库存: {stock}",

        "admin_reduce_select": "选择要减少库存的国家：",
        "admin_reduce_send": "请输入要从 {code} 库存移除的数量：",
        "admin_reduce_invalid": "❌ 请输入有效数字。",
        "admin_reduce_done": "✅ 已移除 {removed} 个。当前库存: {stock}",

        "admin_charge_prompt": (
            "💳 给用户充值\n按此格式发送：\n"
            "user_id|amount\n例如：\n"
            "123456789|50"
        ),
        "admin_charge_fmt": "❌ 格式错误。例如：123456789|50",
        "admin_charge_nouser": "❌ 数据库找不到该用户（需要先 /start）。",
        "admin_charge_ok": "✅ 已给 {uid} 增加 {amt}。新余额={bal}",

        "admin_stock_text": "📦 库存状态:\n{lines}",
        "unknown": "未理解，请使用菜单按钮 ✅",
    }
}


def t(lang: str, key: str, **kwargs) -> str:
    if lang not in ("EN", "ZH"):
        lang = "EN"
    s = TEXT[lang].get(key, TEXT["EN"].get(key, key))
    return s.format(**kwargs) if kwargs else s


def now_str() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

async def send_log(context: ContextTypes.DEFAULT_TYPE, text: str):
    try:
        await context.bot.send_message(
            chat_id=LOG_CHANNEL_ID,
            text=text,
            parse_mode=ParseMode.HTML,
            disable_web_page_preview=True,
        )
    except Exception as e:
        print("LOG SEND ERROR:", repr(e))

def ensure_dirs() -> None:
    BASE_DIR.mkdir(parents=True, exist_ok=True)
    PRODUCTS_DIR.mkdir(parents=True, exist_ok=True)
    TMP_DIR.mkdir(parents=True, exist_ok=True)


def load_db() -> dict:
    ensure_dirs()
    if not DB_FILE.exists():
        db = {"users": {}, "products": {}, "invoices": {}, "used_txids": {}}
        save_db(db)
    db = json.loads(DB_FILE.read_text("utf-8"))
    db.setdefault("users", {})
    db.setdefault("products", {})
    db.setdefault("invoices", {})
    # txid -> {"status": "PENDING"/"USED", "invoice": "...", "ts": "..."}
    db.setdefault("used_txids", {})
    return db


def save_db(db: dict) -> None:
    ensure_dirs()
    DB_FILE.write_text(json.dumps(db, ensure_ascii=False, indent=2), "utf-8")


def is_admin(uid: int) -> bool:
    return uid in ADMIN_IDS


def get_user(db: dict, user_id: int, username: str) -> dict:
    uid = str(user_id)
    if uid not in db["users"]:
        db["users"][uid] = {
            "balance": 0.0,
            "joined": now_str(),
            "lang": "EN",
            "purchases": 0,
            "username": username or "",
        }
    else:
        if username and not db["users"][uid].get("username"):
            db["users"][uid]["username"] = username
    return db["users"][uid]


def get_lang(db: dict, user_id: int) -> str:
    u = db["users"].get(str(user_id))
    if not u:
        return "EN"
    l = u.get("lang", "EN")
    return l if l in ("EN", "ZH") else "EN"


# =========================
# Product storage (folders)
# =========================
def product_root(code: str) -> Path:
    return PRODUCTS_DIR / code


def stock_dir(code: str) -> Path:
    return product_root(code) / "stock"


def sold_dir(code: str) -> Path:
    return product_root(code) / "sold"


def reserved_dir(code: str) -> Path:
    return product_root(code) / "reserved"


def ensure_product_dirs(code: str) -> None:
    stock_dir(code).mkdir(parents=True, exist_ok=True)
    sold_dir(code).mkdir(parents=True, exist_ok=True)
    reserved_dir(code).mkdir(parents=True, exist_ok=True)


def extract_zip_to_temp(zip_path: Path) -> Path:
    out = TMP_DIR / f"extract_{random.randint(1000,9999)}"
    out.mkdir(parents=True, exist_ok=True)
    with zipfile.ZipFile(zip_path, "r") as z:
        z.extractall(out)
    return out


def top_level_folders(extract_dir: Path) -> List[Path]:
    folders = [p for p in extract_dir.iterdir() if p.is_dir()]
    folders.sort(key=lambda p: p.name)
    return folders


def stock_items(code: str) -> List[Path]:
    sdir = stock_dir(code)
    if not sdir.exists():
        return []
    items = [p for p in sdir.iterdir() if p.is_dir()]
    items.sort(key=lambda p: p.name)
    return items


def stock_count(code: str) -> int:
    return len(stock_items(code))


def reserve_items(code: str, qty: int) -> Optional[Path]:
    items = stock_items(code)
    if len(items) < qty:
        return None

    batch_id = f"batch_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{random.randint(1000,9999)}"
    batch_dir = reserved_dir(code) / batch_id
    batch_dir.mkdir(parents=True, exist_ok=True)

    for p in items[:qty]:
        dst = batch_dir / p.name
        if dst.exists():
            dst = batch_dir / f"{p.name}_{random.randint(1000,9999)}"
        shutil.move(str(p), str(dst))

    return batch_dir


def rollback_reserved_to_stock(code: str, batch_dir: Path) -> None:
    if not batch_dir.exists():
        return
    for p in batch_dir.iterdir():
        if p.is_dir():
            dst = stock_dir(code) / p.name
            if dst.exists():
                dst = stock_dir(code) / f"{p.name}_{random.randint(1000,9999)}"
            shutil.move(str(p), str(dst))
    shutil.rmtree(batch_dir, ignore_errors=True)


def commit_reserved_to_sold(code: str, batch_dir: Path) -> None:
    if not batch_dir.exists():
        return
    dst_batch = sold_dir(code) / batch_dir.name
    dst_batch.mkdir(parents=True, exist_ok=True)
    for p in batch_dir.iterdir():
        if p.is_dir():
            dst = dst_batch / p.name
            if dst.exists():
                dst = dst_batch / f"{p.name}_{random.randint(1000,9999)}"
            shutil.move(str(p), str(dst))
    shutil.rmtree(batch_dir, ignore_errors=True)


def write_password2_to_folder(folder: Path, password2: str) -> None:
    # Put password in each item folder
    try:
        (folder / "password2.txt").write_text(password2, "utf-8")
    except Exception:
        pass


def read_password2_from_folder(folder: Path) -> str:
    p = folder / "password2.txt"
    if p.exists():
        try:
            return p.read_text("utf-8").strip()
        except Exception:
            return ""
    return ""


def zip_batch(batch_dir: Path, out_zip: Path) -> None:
    # ✅ Keep each item folder at ZIP root
    with zipfile.ZipFile(out_zip, "w", compression=zipfile.ZIP_DEFLATED) as z:
        for item_folder in sorted([p for p in batch_dir.iterdir() if p.is_dir()], key=lambda x: x.name):
            base = item_folder.name
            for root, _, files in os.walk(item_folder):
                root_path = Path(root)
                for fn in files:
                    full = root_path / fn
                    rel = full.relative_to(item_folder)
                    arc = str(Path(base) / rel).replace("\\", "/")
                    z.write(full, arcname=arc)


def purge_stock(code: str, count: int) -> int:
    removed = 0
    items = stock_items(code)
    for p in items[:count]:
        shutil.rmtree(p, ignore_errors=True)
        removed += 1
    return removed


# =========================
# Payment verification..
# =========================
def http_get_json(url: str, headers: Optional[dict] = None) -> dict:
    req = urllib.request.Request(url, headers=headers or {})
    with urllib.request.urlopen(req, timeout=25) as resp:
        raw = resp.read().decode("utf-8")
    return json.loads(raw)


def verify_usdt_trc20_payment(to_address: str, expected_amount: float, txid: str) -> bool:
    params = {
        "limit": "200",
        "only_confirmed": "true",
        "contract_address": USDT_TRC20_CONTRACT,
    }
    url = f"{TRONGRID_BASE}/v1/accounts/{urllib.parse.quote(to_address)}/transactions/trc20?{urllib.parse.urlencode(params)}"

    headers = {}
    if TRONGRID_API_KEY:
        headers["TRON-PRO-API-KEY"] = TRONGRID_API_KEY

    js = http_get_json(url, headers=headers)
    items = js.get("data", []) or []

    exp = float(expected_amount)
    for it in items:
        tx_key = it.get("transaction_id") or it.get("txID") or it.get("txid")
        if not tx_key:
            continue
        if str(tx_key).lower() != str(txid).lower():
            continue

        to = it.get("to") or it.get("to_address") or it.get("recipient")
        if not to or str(to) != str(to_address):
            return False

        val = it.get("value")
        got = float(val) / 1_000_000  # 🔥 VERY IMPORTANT دقت کو
        return abs(got - exp) <= 0.0005


    return False


from io import BytesIO
from telegram import InputFile

def make_qr_inputfile(data: str):
    try:
        import qrcode  # pip install qrcode[pil]
        img = qrcode.make(data)

        bio = BytesIO()
        bio.name = "qrcode.png"   # خیلی مهم
        img.save(bio, format="PNG")
        bio.seek(0)

        return InputFile(bio)
    except Exception as e:
        print("QR ERROR:", repr(e))  # ← تا بفهمیم چرا None میشه
        return None



# =========================
# Keyboards
# =========================
def main_menu_kb(lang: str, admin: bool) -> ReplyKeyboardMarkup:
    rows = [
        [t(lang, "products_list"), t(lang, "topup")],
        [t(lang, "profile")],
        [t(lang, "lang")],
        [t(lang, "help"), t(lang, "support")],
    ]
    if admin:
        rows.append([t(lang, "admin_panel")])
    return ReplyKeyboardMarkup(rows, resize_keyboard=True)


def admin_menu_kb(lang: str) -> ReplyKeyboardMarkup:
    rows = [
        [t(lang, "admin_add_country"), t(lang, "admin_remove_country")],
        [t(lang, "admin_change_price"), t(lang, "admin_upload_stock")],
        [t(lang, "admin_reduce_stock"), t(lang, "admin_charge_user")],
        [t(lang, "admin_members"), t(lang, "admin_stock")],
        [t(lang, "admin_back")],
    ]
    return ReplyKeyboardMarkup(rows, resize_keyboard=True)


def lang_kb(lang: str) -> InlineKeyboardMarkup:
    return InlineKeyboardMarkup([
        [InlineKeyboardButton(t(lang, "lang_en"), callback_data="lang|EN")],
        [InlineKeyboardButton(t(lang, "lang_zh"), callback_data="lang|ZH")],
    ])


def products_inline(db: dict) -> InlineKeyboardMarkup:
    btns = []
    for code, p in db["products"].items():
        name = p.get("name", code)
        price = p.get("price", 0)
        st = stock_count(code)
        btns.append([InlineKeyboardButton(
            f"{name} | {price} USDT | Stock {st}",
            callback_data=f"pdetail|{code}",
        )])
    if not btns:
        btns = [[InlineKeyboardButton("—", callback_data="noop|x")]]
    return InlineKeyboardMarkup(btns)


def product_detail_kb(lang: str, code: str) -> InlineKeyboardMarkup:
    return InlineKeyboardMarkup([
        [InlineKeyboardButton(t(lang, "buy"), callback_data=f"buy|{code}")],
        [InlineKeyboardButton(t(lang, "back"), callback_data="plist|x")],
    ])


def topup_kb(lang: str) -> InlineKeyboardMarkup:
    rows = []
    row = []
    for amt in TOPUP_AMOUNTS:
        row.append(InlineKeyboardButton(str(amt), callback_data=f"topup|{amt}"))
        if len(row) == 3:
            rows.append(row)
            row = []
    if row:
        rows.append(row)
    rows.append([InlineKeyboardButton(t(lang, "custom_amount"), callback_data="topup|custom")])
    return InlineKeyboardMarkup(rows)


def invoice_kb(lang: str, inv_id: str) -> InlineKeyboardMarkup:
    return InlineKeyboardMarkup([
        [InlineKeyboardButton(t(lang, "paid_btn"), callback_data=f"paid|{inv_id}")]
    ])


def admin_select_product_kb(db: dict, prefix: str) -> InlineKeyboardMarkup:
    rows = []
    for code, p in db["products"].items():
        rows.append([InlineKeyboardButton(f"{code} - {p.get('name', code)}", callback_data=f"{prefix}|{code}")])
    if not rows:
        rows = [[InlineKeyboardButton("—", callback_data="noop|x")]]
    return InlineKeyboardMarkup(rows)


# =========================
# States...
# =========================
MODE_BUY_QTY = "BUY_QTY"
MODE_TOPUP_CUSTOM = "TOPUP_CUSTOM"
MODE_WAIT_TX = "WAIT_TX"

MODE_ADMIN_ADD = "ADMIN_ADD"
MODE_ADMIN_SET_PRICE = "ADMIN_SET_PRICE"
MODE_ADMIN_CHARGE = "ADMIN_CHARGE"

MODE_ADMIN_UPLOAD_WAIT_ZIP = "ADMIN_UPLOAD_WAIT_ZIP"
MODE_ADMIN_UPLOAD_WAIT_PASS = "ADMIN_UPLOAD_WAIT_PASS"  # ✅ new
MODE_ADMIN_REDUCE_WAIT = "ADMIN_REDUCE_WAIT"


# =========================
# Core handlers تست شد اوک بود
# =========================
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    async with DB_LOCK:
        db = load_db()
        get_user(db, update.effective_user.id, update.effective_user.username or "")
        save_db(db)
        lang = get_lang(db, update.effective_user.id)

    context.user_data.clear()
    await update.message.reply_text(
        t(lang, "welcome"),
        reply_markup=main_menu_kb(lang, is_admin(update.effective_user.id)),
    )


async def show_products(update: Update, context: ContextTypes.DEFAULT_TYPE):
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)

    context.user_data.clear()
    await update.message.reply_text(t(lang, "products_title"), reply_markup=products_inline(db))


async def on_product_detail(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.callback_query
    await q.answer()

    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, q.from_user.id)
        _, code = q.data.split("|", 1)
        p = db["products"].get(code)

    if not p:
        await q.message.reply_text(t(lang, "not_found"))
        return

    ensure_product_dirs(code)
    name = p.get("name", code)
    price = float(p.get("price", 0))
    st = stock_count(code)

    text = (
        f"🛒 {name}\n"
        f"🌍 Country: {code}\n\n"
        f"💵 Price: {price} USDT\n"
        f"📦 Available Stock: {st}\n\n"
        f"{t(lang, 'note')}"
    )
    await q.message.reply_text(text, reply_markup=product_detail_kb(lang, code))


async def on_back_to_products(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.callback_query
    await q.answer()

    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, q.from_user.id)

    await q.message.reply_text(t(lang, "products_title"), reply_markup=products_inline(db))


async def on_buy_clicked(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.callback_query
    await q.answer()

    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, q.from_user.id)
        _, code = q.data.split("|", 1)
        ok = code in db["products"]

    if not ok:
        await q.message.reply_text(t(lang, "not_found"))
        return

    context.user_data.clear()
    context.user_data["mode"] = MODE_BUY_QTY
    context.user_data["buy_code"] = code
    await q.message.reply_text(t(lang, "enter_qty"))


async def handle_buy_qty(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
    if context.user_data.get("mode") != MODE_BUY_QTY:
        return False

    txt = (update.message.text or "").strip()
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)

    if not txt.isdigit():
        await update.message.reply_text(t(lang, "qty_number"))
        return True
    qty = int(txt)
    if qty <= 0:
        await update.message.reply_text(t(lang, "qty_positive"))
        return True

    code = context.user_data.get("buy_code", "")

    # ✅ Critical section under lock (stock + balance + reserve)
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)
        p = db["products"].get(code)
        if not p:
            context.user_data.clear()
            await update.message.reply_text(t(lang, "not_found"))
            return True

        ensure_product_dirs(code)
        st = stock_count(code)
        if st < qty:
            context.user_data.clear()
            await update.message.reply_text(t(lang, "not_enough_stock"))
            return True

        user = get_user(db, update.effective_user.id, update.effective_user.username or "")
        price = float(p.get("price", 0))
        total = round(qty * price, 6)
        if float(user.get("balance", 0)) < total:
            context.user_data.clear()
            await update.message.reply_text(t(lang, "not_enough_balance"))
            return True

        batch_dir = reserve_items(code, qty)
        if not batch_dir:
            context.user_data.clear()
            await update.message.reply_text(t(lang, "not_enough_stock"))
            return True

        # Deduct now
        user["balance"] = round(float(user["balance"]) - total, 6)
        user["purchases"] = int(user.get("purchases", 0)) + qty
        save_db(db)

    # Zip outside lock
    zip_path = TMP_DIR / f"order_{code}_{update.effective_user.id}_{random.randint(1000,9999)}.zip"
    try:
        zip_batch(batch_dir, zip_path)
    except Exception:
        async with DB_LOCK:
            rollback_reserved_to_stock(code, batch_dir)
        context.user_data.clear()
        await update.message.reply_text("❌ Failed to package goods. Try again.")
        return True

    # Read passwords from items to show below
    pw_set = set()
    try:
        for item_folder in [p for p in batch_dir.iterdir() if p.is_dir()]:
            pw = read_password2_from_folder(item_folder)
            if pw:
                pw_set.add(pw)
    except Exception:
        pass

    if len(pw_set) == 1:
        pw_line = f"• 2FA Password: {list(pw_set)[0]}\n"
    elif len(pw_set) > 1:
        pw_line = "• 2FA Password: (included inside folders)\n"
    else:
        pw_line = ""

    async with DB_LOCK:
        commit_reserved_to_sold(code, batch_dir)

    name = p.get("name", code)
    await update.message.reply_document(
        document=open(zip_path, "rb"),
        caption=t(lang, "order_done_caption", name=name, qty=qty, total=total, pw_line=pw_line, time=now_str()),
    )
    # ✅ Send purchase log to channel
    u = update.effective_user
    username = f"@{u.username}" if u.username else "—"
    log_text = (
        "🛒 <b>NEW ORDER</b>\n"
        f"• User ID: <code>{u.id}</code>\n"
        f"• Username: {username}\n"
        f"• Product: <b>{name}</b> ({code})\n"
        f"• Qty: <b>{qty}</b>\n"
        f"• Total: <b>{total}</b> USDT\n"
        f"• Time: <code>{now_str()}</code>"
    )
    await send_log(context, log_text)

    try:
        zip_path.unlink(missing_ok=True)
    except Exception:
        pass

    context.user_data.clear()
    return True


# =========================
# Top-up okkkk
# =========================
def new_invoice_id() -> str:
    return str(random.randint(100000000, 999999999))


async def show_topup(update: Update, context: ContextTypes.DEFAULT_TYPE):
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)
    context.user_data.clear()
    await update.message.reply_text(t(lang, "select_amount"), reply_markup=topup_kb(lang))


async def create_invoice_message(message, user_id: int, base_amount: float):
    bump = random.uniform(AMOUNT_BUMP_MIN, AMOUNT_BUMP_MAX)
    amount = round(float(base_amount) + bump, 3)
    inv_id = new_invoice_id()
    expires = datetime.now() + timedelta(minutes=INVOICE_EXPIRE_MIN)

    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, user_id)
        db["invoices"][inv_id] = {
            "user_id": user_id,
            "amount": amount,
            "address": RECEIVE_TRON_ADDRESS,
            "expires": expires.strftime("%Y-%m-%d %H:%M:%S"),
            "paid": False,
            "txid": "",
        }
        save_db(db)

    caption = t(
        lang, "invoice_caption",
        amount=amount,
        invoice_id=inv_id,
        address=RECEIVE_TRON_ADDRESS,
        minutes=INVOICE_EXPIRE_MIN,
    )

# ✅ FIXED: send QR reliably
    qr_file = make_qr_inputfile(RECEIVE_TRON_ADDRESS)

    if qr_file:
        await message.reply_photo(
        photo=qr_file,
        caption=caption,
        parse_mode=ParseMode.MARKDOWN,
        reply_markup=invoice_kb(lang, inv_id),
    )
    else:
    # fallback if qrcode fails
        await message.reply_text(
        caption,
        parse_mode=ParseMode.MARKDOWN,
        reply_markup=invoice_kb(lang, inv_id),
    )

async def on_topup_choice(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.callback_query
    await q.answer()

    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, q.from_user.id)

    _, val = q.data.split("|", 1)
    if val == "custom":
        context.user_data.clear()
        context.user_data["mode"] = MODE_TOPUP_CUSTOM
        await q.message.reply_text(t(lang, "send_amount"))
        return

    await create_invoice_message(q.message, q.from_user.id, float(val))


async def handle_topup_custom_amount(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
    if context.user_data.get("mode") != MODE_TOPUP_CUSTOM:
        return False

    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)

    try:
        amount = float((update.message.text or "").strip())
        if amount <= 0:
            raise ValueError()
    except Exception:
        await update.message.reply_text(t(lang, "amount_invalid"))
        return True

    await create_invoice_message(update.message, update.effective_user.id, amount)
    context.user_data.clear()
    return True


async def on_paid_clicked(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.callback_query
    await q.answer()

    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, q.from_user.id)
        inv_id = q.data.split("|", 1)[1]
        inv = db["invoices"].get(inv_id)

    if not inv:
        return

    context.user_data.clear()
    context.user_data["mode"] = MODE_WAIT_TX
    context.user_data["invoice_id"] = inv_id
    await q.message.reply_text(t(lang, "ask_tx"))


async def handle_txid(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
    if context.user_data.get("mode") != MODE_WAIT_TX:
        return False

    txid = (update.message.text or "").strip()
    if not re.fullmatch(r"[A-Fa-f0-9]{64}", txid):
        async with DB_LOCK:
            db = load_db()
            lang = get_lang(db, update.effective_user.id)
        await update.message.reply_text(t(lang, "tx_invalid"))
        return True

    inv_id = context.user_data.get("invoice_id", "")
    txkey = txid.lower()

    should_log_topup = False
    expected_amount = 0.0

    # ✅ Lock + PENDING reservation to prevent simultaneous reuse
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)
        inv = db["invoices"].get(inv_id)

        if not inv or int(inv.get("user_id", 0)) != update.effective_user.id:
            context.user_data.clear()
            return True

        # ✅ Check invoice expiration (before reserving txid)
        try:
            exp_dt = datetime.strptime(inv.get("expires", ""), "%Y-%m-%d %H:%M:%S")
        except Exception:
            exp_dt = None

        if exp_dt and datetime.now() > exp_dt:
            context.user_data.clear()
            await update.message.reply_text(t(lang, "invoice_expired"))
            return True

        used = db["used_txids"].get(txkey)
        if used and used.get("status") in ("PENDING", "USED"):
            await update.message.reply_text(t(lang, "tx_used"))
            return True

        db["used_txids"][txkey] = {"status": "PENDING", "invoice": inv_id, "ts": now_str()}
        save_db(db)

        expected_amount = float(inv["amount"])

    # verify OUTSIDE lock
    try:
        ok = verify_usdt_trc20_payment(RECEIVE_TRON_ADDRESS, expected_amount, txid)
    except Exception:
        ok = False

    if not ok:
        async with DB_LOCK:
            db = load_db()
            cur = db["used_txids"].get(txkey)
            if cur and cur.get("status") == "PENDING" and cur.get("invoice") == inv_id:
                db["used_txids"].pop(txkey, None)
                save_db(db)
            lang = get_lang(db, update.effective_user.id)

        await update.message.reply_text(t(lang, "tx_not_found"))
        return True

    # ✅ Finalize payment (inside lock)
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)
        inv = db["invoices"].get(inv_id)

        if not inv or int(inv.get("user_id", 0)) != update.effective_user.id:
            context.user_data.clear()
            return True

        # ✅ Re-check invoice expiration (edge case)
        try:
            exp_dt = datetime.strptime(inv.get("expires", ""), "%Y-%m-%d %H:%M:%S")
        except Exception:
            exp_dt = None

        if exp_dt and datetime.now() > exp_dt:
            cur = db["used_txids"].get(txkey)
            if cur and cur.get("status") == "PENDING" and cur.get("invoice") == inv_id:
                db["used_txids"].pop(txkey, None)
            save_db(db)

            context.user_data.clear()
            await update.message.reply_text(t(lang, "invoice_expired"))
            return True

        if inv.get("paid") is True:
            db["used_txids"][txkey] = {"status": "USED", "invoice": inv_id, "ts": now_str()}
            save_db(db)
            context.user_data.clear()
            await update.message.reply_text(t(lang, "tx_used"))
            return True

        db["used_txids"][txkey] = {"status": "USED", "invoice": inv_id, "ts": now_str()}
        inv["paid"] = True
        inv["txid"] = txkey

        user = get_user(db, update.effective_user.id, update.effective_user.username or "")
        user["balance"] = round(float(user["balance"]) + expected_amount, 6)
        save_db(db)

        should_log_topup = True

    # ✅ Send topup log to channel (outside lock)
    if should_log_topup:
        u = update.effective_user
        username = f"@{u.username}" if u.username else "—"
        log_text = (
            "✅ <b>TOPUP CONFIRMED</b>\n"
            f"• User ID: <code>{u.id}</code>\n"
            f"• Username: {username}\n"
            f"• Amount: <b>{expected_amount}</b> USDT\n"
            f"• Invoice ID: <code>{inv_id}</code>\n"
            f"• TxID: <code>{txkey}</code>\n"
            f"• Time: <code>{now_str()}</code>"
        )
        await send_log(context, log_text)

    await update.message.reply_text(t(lang, "tx_ok", amount=expected_amount, time=now_str()))
    context.user_data.clear()
    return True

# =========================
# Profile / Help / Support / Language okk
# =========================
async def show_profile(update: Update, context: ContextTypes.DEFAULT_TYPE):
    async with DB_LOCK:
        db = load_db()
        u = get_user(db, update.effective_user.id, update.effective_user.username or "")
        lang = get_lang(db, update.effective_user.id)

        username = f"@{u.get('username')}" if u.get("username") else "—"
        msg = t(
            lang, "profile_text",
            id=update.effective_user.id,
            username=username,
            joined=u.get("joined", "—"),
            bal=u.get("balance", 0),
            pur=u.get("purchases", 0),
            now=now_str(),
        )

    await update.message.reply_text(msg)


async def show_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)
    await update.message.reply_text(t(lang, "help_text"))


async def show_support(update: Update, context: ContextTypes.DEFAULT_TYPE):
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)
    await update.message.reply_text(t(lang, "support_text", contact=SUPPORT_CONTACT))


async def change_language(update: Update, context: ContextTypes.DEFAULT_TYPE):
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)
    await update.message.reply_text(t(lang, "lang_choose"), reply_markup=lang_kb(lang))


async def on_language_select(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.callback_query
    await q.answer()
    _, l = q.data.split("|", 1)
    if l not in ("EN", "ZH"):
        return

    async with DB_LOCK:
        db = load_db()
        u = get_user(db, q.from_user.id, q.from_user.username or "")
        u["lang"] = l
        save_db(db)

    await q.message.reply_text(
        t(l, "lang_set"),
        reply_markup=main_menu_kb(l, is_admin(q.from_user.id)),
    )


# =========================
# Admin panel inam ok
# =========================
async def open_admin_panel(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not is_admin(update.effective_user.id):
        return
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)
    await update.message.reply_text(t(lang, "admin_title"), reply_markup=admin_menu_kb(lang))


async def admin_members(update: Update, context: ContextTypes.DEFAULT_TYPE):
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)
        n = len(db["users"])
    await update.message.reply_text(t(lang, "admin_members_text", n=n))


async def admin_stock(update: Update, context: ContextTypes.DEFAULT_TYPE):
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)

    lines = []
    for code, p in db["products"].items():
        ensure_product_dirs(code)
        st = stock_count(code)
        lines.append(f"• {code} | {p.get('name', code)} | Stock: {st} | Price: {p.get('price', 0)}")
    await update.message.reply_text(t(lang, "admin_stock_text", lines="\n".join(lines) if lines else "—"))


async def admin_add_country(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not is_admin(update.effective_user.id):
        return
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)
    context.user_data.clear()
    context.user_data["mode"] = MODE_ADMIN_ADD
    await update.message.reply_text(t(lang, "admin_add_prompt"))


async def handle_admin_add_country(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
    if context.user_data.get("mode") != MODE_ADMIN_ADD:
        return False
    if not is_admin(update.effective_user.id):
        return True

    txt = (update.message.text or "").strip()
    parts = [p.strip() for p in txt.split("|")]

    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)

        if len(parts) != 3:
            await update.message.reply_text(t(lang, "admin_add_prompt"))
            return True

        code, name, price_s = parts
        code = code.upper()
        if code in db["products"]:
            await update.message.reply_text(t(lang, "admin_add_exists"))
            return True

        try:
            price = float(price_s)
            if price < 0:
                raise ValueError()
        except Exception:
            await update.message.reply_text(t(lang, "admin_add_prompt"))
            return True

        db["products"][code] = {"name": name, "price": price}
        save_db(db)

    ensure_product_dirs(code)
    await update.message.reply_text(t(lang, "admin_add_ok", code=code, name=name, price=price))
    context.user_data.clear()
    return True


async def admin_remove_country(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not is_admin(update.effective_user.id):
        return
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)
    await update.message.reply_text(t(lang, "admin_del_select"), reply_markup=admin_select_product_kb(db, "adm_del"))


async def on_admin_del_select(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.callback_query
    await q.answer()
    if not is_admin(q.from_user.id):
        return

    _, code = q.data.split("|", 1)
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, q.from_user.id)
        p = db["products"].get(code)

    if not p:
        await q.message.reply_text(t(lang, "not_found"))
        return

    kb = InlineKeyboardMarkup([
        [InlineKeyboardButton(t(lang, "yes_delete"), callback_data=f"adm_del_yes|{code}")],
        [InlineKeyboardButton(t(lang, "no_cancel"), callback_data="adm_del_no|x")],
    ])
    await q.message.reply_text(t(lang, "admin_del_confirm", code=code, name=p.get("name", code)), reply_markup=kb)


async def on_admin_del_yes(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.callback_query
    await q.answer()
    if not is_admin(q.from_user.id):
        return

    _, code = q.data.split("|", 1)
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, q.from_user.id)
        db["products"].pop(code, None)
        save_db(db)

    shutil.rmtree(product_root(code), ignore_errors=True)
    await q.message.reply_text(t(lang, "admin_del_done", code=code))


async def on_admin_del_no(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.callback_query
    await q.answer()
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, q.from_user.id)
    await q.message.reply_text(t(lang, "admin_del_cancel"))


async def admin_change_price(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not is_admin(update.effective_user.id):
        return
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)
    await update.message.reply_text(t(lang, "admin_price_select"), reply_markup=admin_select_product_kb(db, "adm_price"))


async def on_admin_price_select(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.callback_query
    await q.answer()
    if not is_admin(q.from_user.id):
        return

    _, code = q.data.split("|", 1)
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, q.from_user.id)
        ok = code in db["products"]

    if not ok:
        await q.message.reply_text(t(lang, "not_found"))
        return

    context.user_data.clear()
    context.user_data["mode"] = MODE_ADMIN_SET_PRICE
    context.user_data["price_code"] = code
    await q.message.reply_text(t(lang, "admin_price_send", code=code))


async def handle_admin_set_price(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
    if context.user_data.get("mode") != MODE_ADMIN_SET_PRICE:
        return False
    if not is_admin(update.effective_user.id):
        return True

    code = context.user_data.get("price_code", "")
    try:
        price = float((update.message.text or "").strip())
        if price < 0:
            raise ValueError()
    except Exception:
        async with DB_LOCK:
            db = load_db()
            lang = get_lang(db, update.effective_user.id)
        await update.message.reply_text(t(lang, "admin_price_invalid"))
        return True

    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)
        if code in db["products"]:
            db["products"][code]["price"] = price
            save_db(db)

    await update.message.reply_text(t(lang, "admin_price_ok", code=code, price=price))
    context.user_data.clear()
    return True


# -Admin upload stock (ZIP -> ask password -> commit to stock) moshkel fix shod
async def admin_upload_stock(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not is_admin(update.effective_user.id):
        return
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)
    await update.message.reply_text(t(lang, "admin_upload_select"), reply_markup=admin_select_product_kb(db, "adm_upload"))


async def on_admin_upload_select(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.callback_query
    await q.answer()
    if not is_admin(q.from_user.id):
        return

    _, code = q.data.split("|", 1)
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, q.from_user.id)
        ok = code in db["products"]

    if not ok:
        await q.message.reply_text(t(lang, "not_found"))
        return

    ensure_product_dirs(code)
    context.user_data.clear()
    context.user_data["mode"] = MODE_ADMIN_UPLOAD_WAIT_ZIP
    context.user_data["upload_code"] = code
    await q.message.reply_text(t(lang, "admin_upload_sendzip", code=code))


async def handle_admin_upload_zip(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
    if context.user_data.get("mode") != MODE_ADMIN_UPLOAD_WAIT_ZIP:
        return False
    if not is_admin(update.effective_user.id):
        return True

    code = context.user_data.get("upload_code", "")
    doc = update.message.document

    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)

    if not code:
        context.user_data.clear()
        return True
    if not doc:
        await update.message.reply_text(t(lang, "admin_zip_need"))
        return True
    if not (doc.file_name or "").lower().endswith(".zip"):
        await update.message.reply_text(t(lang, "admin_zip_ext"))
        return True

    ensure_product_dirs(code)

    # download zip
    tg_file = await doc.get_file()
    local_zip = TMP_DIR / f"upload_{code}_{random.randint(1000,9999)}.zip"
    await tg_file.download_to_drive(str(local_zip))

    # extract
    extract_dir = extract_zip_to_temp(local_zip)
    folders = top_level_folders(extract_dir)
    if not folders:
        # cleanup
        try:
            shutil.rmtree(extract_dir, ignore_errors=True)
            local_zip.unlink(missing_ok=True)
        except Exception:
            pass
        await update.message.reply_text(t(lang, "admin_zip_empty"))
        return True

    # ✅ store temp paths in state, then ask password
    context.user_data["mode"] = MODE_ADMIN_UPLOAD_WAIT_PASS
    context.user_data["upload_zip_path"] = str(local_zip)
    context.user_data["upload_extract_dir"] = str(extract_dir)
    await update.message.reply_text(t(lang, "admin_pw_ask"), parse_mode=ParseMode.MARKDOWN)
    return True


async def handle_admin_upload_password(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
    if context.user_data.get("mode") != MODE_ADMIN_UPLOAD_WAIT_PASS:
        return False
    if not is_admin(update.effective_user.id):
        return True

    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)

    code = context.user_data.get("upload_code", "")
    zip_path_s = context.user_data.get("upload_zip_path", "")
    extract_dir_s = context.user_data.get("upload_extract_dir", "")
    if not code or not zip_path_s or not extract_dir_s:
        context.user_data.clear()
        return True

    password2 = (update.message.text or "").strip()
    if password2 == "-":
        password2 = ""

    await update.message.reply_text(t(lang, "admin_pw_saved"))

    local_zip = Path(zip_path_s)
    extract_dir = Path(extract_dir_s)

    added = 0
    try:
        folders = top_level_folders(extract_dir)
        if not folders:
            await update.message.reply_text(t(lang, "admin_zip_empty"))
            return True

        for folder in folders:
            # attach password file in each item folder
            if password2:
                write_password2_to_folder(folder, password2)

            dst = stock_dir(code) / folder.name
            if dst.exists():
                dst = stock_dir(code) / f"{folder.name}_{random.randint(1000,9999)}"
            shutil.move(str(folder), str(dst))
            added += 1

        await update.message.reply_text(t(lang, "admin_zip_ok", added=added, stock=stock_count(code)))
        return True
    finally:
        try:
            shutil.rmtree(extract_dir, ignore_errors=True)
            local_zip.unlink(missing_ok=True)
        except Exception:
            pass
        context.user_data.clear()


async def admin_reduce_stock(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not is_admin(update.effective_user.id):
        return
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)
    await update.message.reply_text(t(lang, "admin_reduce_select"), reply_markup=admin_select_product_kb(db, "adm_reduce"))


async def on_admin_reduce_select(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.callback_query
    await q.answer()
    if not is_admin(q.from_user.id):
        return
    _, code = q.data.split("|", 1)

    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, q.from_user.id)
        ok = code in db["products"]

    if not ok:
        await q.message.reply_text(t(lang, "not_found"))
        return

    context.user_data.clear()
    context.user_data["mode"] = MODE_ADMIN_REDUCE_WAIT
    context.user_data["reduce_code"] = code
    await q.message.reply_text(t(lang, "admin_reduce_send", code=code))


async def handle_admin_reduce_qty(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
    if context.user_data.get("mode") != MODE_ADMIN_REDUCE_WAIT:
        return False
    if not is_admin(update.effective_user.id):
        return True

    code = context.user_data.get("reduce_code", "")
    txt = (update.message.text or "").strip()

    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)

    if not txt.isdigit():
        await update.message.reply_text(t(lang, "admin_reduce_invalid"))
        return True

    n = int(txt)
    if n <= 0:
        await update.message.reply_text(t(lang, "admin_reduce_invalid"))
        return True

    removed = purge_stock(code, n)
    await update.message.reply_text(t(lang, "admin_reduce_done", removed=removed, stock=stock_count(code)))
    context.user_data.clear()
    return True


async def admin_charge_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not is_admin(update.effective_user.id):
        return
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)
    context.user_data.clear()
    context.user_data["mode"] = MODE_ADMIN_CHARGE
    await update.message.reply_text(t(lang, "admin_charge_prompt"))


async def handle_admin_charge(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
    if context.user_data.get("mode") != MODE_ADMIN_CHARGE:
        return False
    if not is_admin(update.effective_user.id):
        return True

    txt = (update.message.text or "").strip()
    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)

        if "|" not in txt:
            await update.message.reply_text(t(lang, "admin_charge_fmt"))
            return True

        uid_s, amt_s = [x.strip() for x in txt.split("|", 1)]
        if not uid_s.isdigit():
            await update.message.reply_text(t(lang, "admin_charge_fmt"))
            return True

        try:
            amt = float(amt_s)
        except Exception:
            await update.message.reply_text(t(lang, "admin_charge_fmt"))
            return True

        if uid_s not in db["users"]:
            await update.message.reply_text(t(lang, "admin_charge_nouser"))
            return True

        db["users"][uid_s]["balance"] = round(float(db["users"][uid_s]["balance"]) + amt, 6)
        save_db(db)
        bal = db["users"][uid_s]["balance"]

    await update.message.reply_text(t(lang, "admin_charge_ok", uid=uid_s, amt=amt, bal=bal))
    context.user_data.clear()
    return True


# =========================
# Routers goooood
# =========================
async def text_router(update: Update, context: ContextTypes.DEFAULT_TYPE):
    # If message has a document (admin zip upload), do NOT rely on update.message.text
    if update.message and update.message.document:
        # Only admin upload zip uses this path
        if await handle_admin_upload_zip(update, context):
            return
        # if not handled, ignore
        return

    async with DB_LOCK:
        db = load_db()
        lang = get_lang(db, update.effective_user.id)

    txt = update.message.text if update.message else ""

    if txt == t(lang, "products_list"):
        await show_products(update, context); return
    if txt == t(lang, "topup"):
        await show_topup(update, context); return
    if txt == t(lang, "profile"):
        await show_profile(update, context); return
    if txt == t(lang, "lang"):
        await change_language(update, context); return
    if txt == t(lang, "help"):
        await show_help(update, context); return
    if txt == t(lang, "support"):
        await show_support(update, context); return
    if txt == t(lang, "admin_panel"):
        await open_admin_panel(update, context); return

    # admin actions
    if txt == t(lang, "admin_add_country"):
        await admin_add_country(update, context); return
    if txt == t(lang, "admin_remove_country"):
        await admin_remove_country(update, context); return
    if txt == t(lang, "admin_change_price"):
        await admin_change_price(update, context); return
    if txt == t(lang, "admin_upload_stock"):
        await admin_upload_stock(update, context); return
    if txt == t(lang, "admin_reduce_stock"):
        await admin_reduce_stock(update, context); return
    if txt == t(lang, "admin_charge_user"):
        await admin_charge_user(update, context); return
    if txt == t(lang, "admin_members"):
        await admin_members(update, context); return
    if txt == t(lang, "admin_stock"):
        await admin_stock(update, context); return
    if txt == t(lang, "admin_back"):
        await update.message.reply_text(t(lang, "main_menu"), reply_markup=main_menu_kb(lang, is_admin(update.effective_user.id)))
        return

    # stateful handlers is gooood toooo
    if await handle_buy_qty(update, context): return
    if await handle_topup_custom_amount(update, context): return
    if await handle_txid(update, context): return

    if await handle_admin_add_country(update, context): return
    if await handle_admin_set_price(update, context): return
    if await handle_admin_upload_password(update, context): return  # ✅ new
    if await handle_admin_reduce_qty(update, context): return
    if await handle_admin_charge(update, context): return

    await update.message.reply_text(t(lang, "unknown"))


async def callback_router(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.callback_query
    data = q.data

    if data.startswith("lang|"):
        await on_language_select(update, context); return

    if data.startswith("pdetail|"):
        await on_product_detail(update, context); return
    if data.startswith("plist|"):
        await on_back_to_products(update, context); return
    if data.startswith("buy|"):
        await on_buy_clicked(update, context); return

    if data.startswith("topup|"):
        await on_topup_choice(update, context); return
    if data.startswith("paid|"):
        await on_paid_clicked(update, context); return

    if data.startswith("adm_del|"):
        await on_admin_del_select(update, context); return
    if data.startswith("adm_del_yes|"):
        await on_admin_del_yes(update, context); return
    if data.startswith("adm_del_no|"):
        await on_admin_del_no(update, context); return

    if data.startswith("adm_price|"):
        await on_admin_price_select(update, context); return

    if data.startswith("adm_upload|"):
        await on_admin_upload_select(update, context); return

    if data.startswith("adm_reduce|"):
        await on_admin_reduce_select(update, context); return

    if data.startswith("noop|"):
        await q.answer(); return


# =========================
# ruuuuuunesh kon
# =========================
def main():
    ensure_dirs()
    _ = load_db()

    app = ApplicationBuilder().token(BOT_TOKEN).build()
    app.add_handler(CommandHandler("start", cmd_start))
    app.add_handler(CallbackQueryHandler(callback_router))
    app.add_handler(MessageHandler(filters.ALL & ~filters.COMMAND, text_router))

    print("🤖 Bot is running...")
    app.run_polling()


if __name__ == "__main__":
    main()
