# -*- coding: utf-8 -*-
"""
Python conversion of the supplied PHP Telegram bot.

Dependencies:
    pip install -r requirements.txt

Run:
    python bot.py

The bot uses long polling and keeps the original file-based storage:
members.txt, step.txt, channel.txt, taim.blue, *.blue and media/*.txt
"""

import json
import logging
import os
import random
import re
import shutil
import subprocess
import time
import threading
from pathlib import Path
from typing import Any, Dict, Optional

import requests

BASE_DIR = Path(__file__).resolve().parent
MEDIA_DIR = BASE_DIR / "media"
MEDIA_DIR.mkdir(exist_ok=True)

# Credentials supplied by the owner.
# IMPORTANT: the token was exposed in chat; revoke it with @BotFather and replace it.
BOT_TOKEN = os.getenv(
    "BOT_TOKEN",
    "8898584590:AAEhpdbPkaHIUl3Fu_U16k422NUPv25RJOE",
)
OWNER_ID = int(os.getenv("OWNER_ID", "8278050575"))

API_URL = f"https://api.telegram.org/bot{BOT_TOKEN}/"
SESSION = requests.Session()
SESSION.headers.update({"User-Agent": "TelegramBot/1.0"})

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s",
)
log = logging.getLogger("telegram-bot")

FILM_DB = BASE_DIR / "films.json"
film_lock = threading.RLock()
pending_film_requests: dict[int, dict[str, Any]] = {}

REACTIONS_DB = BASE_DIR / "reactions.json"
reaction_lock = threading.RLock()

BACKUP_FILES = [
    "films.json", "reactions.json", "members.txt", "channel.txt",
    "force_channels.json", "taim.blue", "start.blue", "png.blue",
    "gif.blue", "film.blue", "banner.blue", "film_channel.blue",
    "admin.blue", "admin1.blue", "admin2.blue", "admin3.blue",
    "admin4.blue", "admin5.blue",
]
BACKUP_MEDIA_FILES = ["pic.txt", "vid.txt", "gif.txt"]


def read_text(name: str, default: str = "") -> str:
    p = BASE_DIR / name
    try:
        return p.read_text(encoding="utf-8")
    except FileNotFoundError:
        return default


def write_text(name: str, value: Any) -> None:
    (BASE_DIR / name).write_text(str(value), encoding="utf-8")


def append_csv(path: Path, value: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("a", encoding="utf-8") as f:
        f.write(str(value) + ",")


def load_csv(name: str) -> list[str]:
    raw = read_text(name)
    return [x.strip() for x in raw.split(",") if x.strip()]


def load_admins() -> list[int]:
    ids = [OWNER_ID]
    for filename in ("admin1.blue", "admin.blue", "admin2.blue",
                     "admin3.blue", "admin4.blue", "admin5.blue"):
        value = read_text(filename).strip()
        if value.isdigit():
            ids.append(int(value))
    return list(dict.fromkeys(ids))


def is_admin(user_id: int) -> bool:
    return user_id in load_admins()


def tg(method: str, **data) -> Optional[Dict[str, Any]]:
    """Call Telegram Bot API and return decoded JSON."""
    try:
        r = SESSION.post(API_URL + method, data=data, timeout=35)
        result = r.json()
        if not result.get("ok"):
            log.warning("Telegram %s failed: %s", method, result)
        return result
    except requests.RequestException as e:
        log.error("Telegram request error in %s: %s", method, e)
        return None
    except ValueError:
        log.error("Invalid Telegram response in %s", method)
        return None


def send_message(chat_id: int, text: str, reply_markup=None,
                 parse_mode: Optional[str] = None,
                 reply_to: Optional[int] = None) -> Optional[Dict[str, Any]]:
    data = {"chat_id": chat_id, "text": text}
    if reply_markup is not None:
        data["reply_markup"] = json.dumps(reply_markup, ensure_ascii=False)
    if parse_mode:
        data["parse_mode"] = parse_mode
    if reply_to:
        data["reply_to_message_id"] = reply_to
    return tg("sendMessage", **data)


def edit_message(chat_id: int, message_id: int, text: str,
                 reply_markup=None, parse_mode: Optional[str] = None):
    data = {"chat_id": chat_id, "message_id": message_id, "text": text}
    if reply_markup is not None:
        data["reply_markup"] = json.dumps(reply_markup, ensure_ascii=False)
    if parse_mode:
        data["parse_mode"] = parse_mode
    return tg("editMessageText", **data)


def delete_message(chat_id: int, message_id: int):
    return tg("deleteMessage", chat_id=chat_id, message_id=message_id)


def forward_message(chat_id: int, from_chat_id: int, message_id: int):
    return tg(
        "forwardMessage",
        chat_id=chat_id,
        from_chat_id=from_chat_id,
        message_id=message_id,
    )


def _send_media_and_schedule(method: str, chat_id: int, field: str, value: str, caption: str = "",
                              reply_markup=None, schedule_delete: bool = True, **extra):
    data = {field: value, "caption": caption, **extra}
    if reply_markup is not None:
        data["reply_markup"] = json.dumps(reply_markup, ensure_ascii=False)
    result = tg(method, chat_id=chat_id, **data)
    if result and result.get("ok") and schedule_delete:
        message_id = result.get("result", {}).get("message_id")
        if message_id:
            delete_later(chat_id, int(message_id), get_delete_seconds())
    return result


def send_photo(chat_id: int, photo: str, caption: str = ""):
    return _send_media_and_schedule("sendPhoto", chat_id, "photo", photo, caption)


def send_video(chat_id: int, video: str, caption: str = "", reply_markup=None, schedule_delete: bool = True):
    extra = {}
    banner_id = read_text("banner.blue", "").strip()
    if banner_id:
        extra["cover"] = banner_id  # پارامتر رسمی تلگرام برای عکس‌روی‌ویدیو؛ مستقیم با file_id کار می‌کنه
    return _send_media_and_schedule("sendVideo", chat_id, "video", video, caption,
                                     reply_markup=reply_markup, schedule_delete=schedule_delete, **extra)


def send_document_upload(chat_id: int, file_path: Path, caption: str = "") -> Optional[Dict[str, Any]]:
    """Upload a local file (e.g. a backup ZIP) directly to Telegram, unlike
    send_document() which only forwards an existing file_id/URL."""
    try:
        with file_path.open("rb") as fh:
            files = {"document": (file_path.name, fh)}
            r = SESSION.post(
                API_URL + "sendDocument",
                data={"chat_id": chat_id, "caption": caption},
                files=files,
                timeout=120,
            )
        return r.json()
    except requests.RequestException as e:
        log.error("Backup upload failed: %s", e)
        return None
    except ValueError:
        log.error("Invalid Telegram response for backup upload")
        return None


def send_video_upload(chat_id: int, file_path: Path, caption: str = "") -> Optional[Dict[str, Any]]:
    """آپلود مستقیم یه فایل ویدیوی محلی (نتیجه‌ی دانلودر)، برخلاف send_video که فقط file_id/URL موجود رو می‌فرسته."""
    try:
        with file_path.open("rb") as fh:
            files = {"video": (file_path.name, fh)}
            r = SESSION.post(
                API_URL + "sendVideo",
                data={"chat_id": chat_id, "caption": caption, "supports_streaming": True},
                files=files,
                timeout=180,
            )
        return r.json()
    except requests.RequestException as e:
        log.error("Video upload failed: %s", e)
        return None
    except ValueError:
        log.error("Invalid Telegram response for video upload")
        return None


def get_delete_seconds() -> int:
    try:
        return max(1, min(int(read_text("taim.blue", "20").strip()), 300))
    except (TypeError, ValueError):
        return 20


def delete_later(chat_id: int, message_id: int, seconds: int | None = None) -> None:
    delay = get_delete_seconds() if seconds is None else max(1, int(seconds))

    def worker():
        time.sleep(delay)
        result = delete_message(chat_id, message_id)
        if not result or not result.get("ok"):
            log.warning("Media deletion failed chat=%s message=%s result=%s", chat_id, message_id, result)

    threading.Thread(target=worker, daemon=True).start()


def bot_username() -> str:
    """Return the bot username without the @ sign."""
    result = tg("getMe")
    if result and result.get("ok"):
        return str(result["result"].get("username", "")).lstrip("@")
    return ""


def film_deep_link(film_id: str) -> str:
    """Build a Telegram deep link for one film."""
    username = bot_username()
    if not username:
        return ""
    return f"https://t.me/{username}?start=film_{film_id}"


def send_document(chat_id: int, document: str, caption: str = ""):
    return _send_media_and_schedule("sendDocument", chat_id, "document", document, caption)


def keyboard(rows):
    return {"keyboard": rows, "resize_keyboard": True}


KEY_MEDIA = keyboard([
    [{"text": "+ عکس"}, {"text": "+ فیلم"}],
    [{"text": "+ گیف"}, {"text": "بازگشت"}],
])

KEY_HOME = keyboard([
    [{"text": "🔞 | فیلم"}, {"text": "🎁 | پک فیلم"}],
    [{"text": "🔞 | عکس"}, {"text": "🔞 | گیف"}],
    [{"text": "📂 | دسته‌بندی‌ها"}, {"text": "🎲 | فیلم رندوم"}],
])

KEY_PANEL = keyboard([
    [{"text": "📊 | آمار ربات"}, {"text": "🔐 | تنظیم کانال اول"}],
    [{"text": "📢 | جوین اجباری"}],
    [{"text": "🔓 | پیام همگانی"}, {"text": "🔒| فوروارد همگانی"}],
    [{"text": "🔞 | افزودن مدیا"}, {"text": "🎬 مدیریت فیلم و پک"}],
    [{"text": "تنظیم تایم حذف پست🌪️"}, {"text": "تنظیم متن استارت📝"}],
    [{"text": "تنظیم کپشن عکس🧠"}, {"text": "تنظیم کپشن گیف🫀"}],
    [{"text": "تنظیم کپشن فیلم❤️"}, {"text": "حذف همه گیف ها❌"}],
    [{"text": "🖼️ تنظیم بنر فیلم"}, {"text": "🗑 حذف بنر فیلم"}],
    [{"text": "🎲 افزودن فیلم رندوم"}, {"text": "🗑 پاک کردن فیلم‌های رندوم"}],
    [{"text": "حذف همه فیلم ها❌"}, {"text": "حذف همه عکس ها❌"}],
    [{"text": "تنظیمات ادمین⚙️"}, {"text": "حذف همه عکس ها❌"}],
    [{"text": "💾 بکاپ کامل"}],
    [{"text": "💙TMblue💙"}],
])

KEY_BACK = keyboard([[{"text": "بازگشت"}]])
KEY_PANEL_ONLY = keyboard([[{"text": "/panel"}]])
KEY_REMOVE = {"remove_keyboard": True}


def ensure_files():
    for name, default in [
        ("members.txt", ""),
        ("step.txt", "none"),
        ("channel.txt", ""),
        ("force_channels.json", "[]"),
        ("taim.blue", "20"),
        ("start.blue", "سلام کاربر گرامی 🌹"),
        ("png.blue", ""),
        ("gif.blue", ""),
        ("film.blue", ""),
        ("banner.blue", ""),
        ("admin1.blue", "❌not admin❌"),
        ("admin2.blue", "❌not admin❌"),
        ("admin3.blue", "❌not admin❌"),
        ("admin4.blue", "❌not admin❌"),
        ("admin5.blue", "❌not admin❌"),
        ("admin.blue", "❌not admin❌"),
    ]:
        p = BASE_DIR / name
        if not p.exists():
            p.write_text(default, encoding="utf-8")
    for name in ("gif.txt", "vid.txt", "pic.txt"):
        p = MEDIA_DIR / name
        if not p.exists():
            p.write_text("", encoding="utf-8")
    if not FILM_DB.exists():
        FILM_DB.write_text(json.dumps({"films": {}, "packs": {}, "next_film_id": 1, "next_pack_id": 1, "channel": ""}, ensure_ascii=False, indent=2), encoding="utf-8")
    migrate_old_films()


def build_backup_zip() -> Path:
    """Zip films.json, reactions.json, members, settings and media into one file."""
    import zipfile

    out_path = BASE_DIR / f"backup_{int(time.time())}.zip"
    with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as zf:
        for name in BACKUP_FILES:
            p = BASE_DIR / name
            if p.exists():
                zf.write(p, arcname=name)
        for name in BACKUP_MEDIA_FILES:
            p = MEDIA_DIR / name
            if p.exists():
                zf.write(p, arcname=f"media/{name}")
    return out_path


def load_film_db() -> dict:
    with film_lock:
        try:
            data = json.loads(FILM_DB.read_text(encoding="utf-8"))
        except Exception:
            data = {"films": {}, "packs": {}, "next_film_id": 1, "next_pack_id": 1, "channel": ""}
        data.setdefault("films", {})
        data.setdefault("packs", {})
        data.setdefault("categories", {})
        data.setdefault("random_films", [])
        data.setdefault("next_film_id", 1)
        data.setdefault("next_pack_id", 1)
        data.setdefault("next_category_id", 1)
        data.setdefault("channel", "")
        for film in data["films"].values():
            film.setdefault("views", 0)
            film.setdefault("likes", 0)
            film.setdefault("dislikes", 0)
            film.setdefault("category_id", None)
        return data


def save_film_db(data: dict) -> None:
    with film_lock:
        tmp = FILM_DB.with_suffix(".tmp")
        tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
        tmp.replace(FILM_DB)


def load_reactions() -> dict:
    """film_id -> {user_id_str: 'like' | 'dislike'}"""
    with reaction_lock:
        try:
            data = json.loads(REACTIONS_DB.read_text(encoding="utf-8"))
            if not isinstance(data, dict):
                data = {}
        except Exception:
            data = {}
        return data


def save_reactions(data: dict) -> None:
    with reaction_lock:
        tmp = REACTIONS_DB.with_suffix(".tmp")
        tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
        tmp.replace(REACTIONS_DB)


def increment_view(fid: str) -> None:
    db = load_film_db()
    film = db["films"].get(str(fid))
    if film:
        film["views"] = int(film.get("views", 0)) + 1
        save_film_db(db)


def apply_reaction(fid: str, user_id: int, reaction: str) -> tuple[int, int]:
    """Register/toggle/switch a user's like|dislike on a film.
    Returns (likes, dislikes) after the update."""
    fid = str(fid)
    uid = str(user_id)
    with reaction_lock:
        reactions = load_reactions()
        film_reactions = reactions.setdefault(fid, {})
        previous = film_reactions.get(uid)

        db = load_film_db()
        film = db["films"].get(fid)
        if not film:
            return 0, 0

        if previous == reaction:
            # Same reaction pressed again -> remove it.
            del film_reactions[uid]
            film[f"{reaction}s"] = max(0, int(film.get(f"{reaction}s", 0)) - 1)
        else:
            if previous:
                film[f"{previous}s"] = max(0, int(film.get(f"{previous}s", 0)) - 1)
            film_reactions[uid] = reaction
            film[f"{reaction}s"] = int(film.get(f"{reaction}s", 0)) + 1

        save_film_db(db)
        save_reactions(reactions)
        return int(film.get("likes", 0)), int(film.get("dislikes", 0))


def film_reaction_keyboard(fid: str) -> dict:
    db = load_film_db()
    film = db["films"].get(str(fid), {})
    likes = int(film.get("likes", 0))
    dislikes = int(film.get("dislikes", 0))
    return {"inline_keyboard": [[
        {"text": f"👍 {likes}", "callback_data": f"react:like:{fid}"},
        {"text": f"👎 {dislikes}", "callback_data": f"react:dislike:{fid}"},
    ]]}


def migrate_old_films() -> None:
    """Put existing media/vid.txt films into the selectable film catalog once."""
    data = load_film_db()
    existing = {str(f.get("file_id")) for f in data["films"].values()}
    changed = False
    for file_id in load_csv("media/vid.txt"):
        if file_id in existing:
            continue
        fid = str(data["next_film_id"])
        data["next_film_id"] += 1
        data["films"][fid] = {"name": f"فیلم {fid}", "file_id": file_id}
        existing.add(file_id)
        changed = True
    if changed:
        save_film_db(data)


FILM_PAGE_SIZE = 8       # چندتا فیلم/پک/دسته تو هر صفحه از لیست‌های کاربری
ADMIN_FILM_PAGE_SIZE = 6  # کمتره چون هر فیلم تو لیست ادمین دو ردیف دکمه داره


def _paginate(items: list, page: int, page_size: int):
    """items رو برش می‌زنه و صفحه‌ی معتبر (کلمپ‌شده) و تعداد کل صفحات رو برمی‌گردونه."""
    total_pages = max(1, (len(items) + page_size - 1) // page_size)
    page = max(0, min(page, total_pages - 1))
    start = page * page_size
    return items[start:start + page_size], total_pages, page


def _pagination_row(page: int, total_pages: int, callback_prefix: str) -> list:
    """یه ردیف ناوبری «قبلی / شماره صفحه / بعدی» می‌سازه؛ اگه فقط یه صفحه باشه، خالی برمی‌گردونه."""
    if total_pages <= 1:
        return []
    row = []
    if page > 0:
        row.append({"text": "◀️ قبلی", "callback_data": f"{callback_prefix}:{page-1}"})
    row.append({"text": f"📄 {page+1}/{total_pages}", "callback_data": "noop"})
    if page < total_pages - 1:
        row.append({"text": "بعدی ▶️", "callback_data": f"{callback_prefix}:{page+1}"})
    return [row]


def film_menu_keyboard(page: int = 0):
    data = load_film_db()
    items = list(data["films"].items())
    page_items, total_pages, page = _paginate(items, page, FILM_PAGE_SIZE)
    rows = []
    for fid, film in page_items:
        rows.append([{"text": f"🎬 {film.get('name', 'فیلم')}", "callback_data": f"film_get:{fid}"}])
    rows.extend(_pagination_row(page, total_pages, "film_list"))
    rows.append([{"text": "🔙 برگشت", "callback_data": "film_home"}])
    return {"inline_keyboard": rows}


def pack_menu_keyboard(page: int = 0):
    data = load_film_db()
    items = list(data["packs"].items())
    page_items, total_pages, page = _paginate(items, page, FILM_PAGE_SIZE)
    rows = []
    for pid, pack in page_items:
        count = len(pack.get("film_ids", []))
        rows.append([{"text": f"🎁 {pack.get('name', 'پک')} ({count} فیلم)", "callback_data": f"pack_get:{pid}"}])
    rows.extend(_pagination_row(page, total_pages, "pack_list"))
    rows.append([{"text": "🔙 برگشت", "callback_data": "film_home"}])
    return {"inline_keyboard": rows}


def admin_film_keyboard():
    return {"inline_keyboard": [
        [{"text": "➕ افزودن فیلم جدید", "callback_data": "adm_film_add"}],
        [{"text": "📋 لیست فیلم‌ها", "callback_data": "adm_film_list"}],
        [{"text": "📊 آمار بازدید", "callback_data": "adm_film_stats"}],
        [{"text": "🎁 ساخت پک", "callback_data": "adm_pack_add"}],
        [{"text": "📦 مدیریت پک‌ها", "callback_data": "adm_pack_list"}],
        [{"text": "📂 مدیریت دسته‌بندی", "callback_data": "adm_cat_list"}],
        [{"text": "⚙️ کانال دریافت", "callback_data": "adm_film_channel"}],
    ]}


def category_menu_keyboard():
    """User-facing keyboard listing categories."""
    data = load_film_db()
    rows = []
    for cid, cat in data["categories"].items():
        count = sum(1 for f in data["films"].values() if str(f.get("category_id")) == str(cid))
        rows.append([{"text": f"📂 {cat.get('name', 'دسته')} ({count} فیلم)", "callback_data": f"cat_open:{cid}"}])
    rows.append([{"text": "🔙 برگشت", "callback_data": "film_home"}])
    return {"inline_keyboard": rows}


def category_films_keyboard(cid: str, page: int = 0):
    data = load_film_db()
    items = [(fid, film) for fid, film in data["films"].items()
             if str(film.get("category_id")) == str(cid)]
    page_items, total_pages, page = _paginate(items, page, FILM_PAGE_SIZE)
    rows = []
    for fid, film in page_items:
        rows.append([{"text": f"🎬 {film.get('name', 'فیلم')}", "callback_data": f"film_get:{fid}"}])
    rows.extend(_pagination_row(page, total_pages, f"cat_open:{cid}"))
    rows.append([{"text": "🔙 برگشت", "callback_data": "cat_list"}])
    return {"inline_keyboard": rows}


def admin_category_keyboard():
    data = load_film_db()
    rows = []
    for cid, cat in data["categories"].items():
        count = sum(1 for f in data["films"].values() if str(f.get("category_id")) == str(cid))
        rows.append([{"text": f"📂 {cat.get('name', 'دسته')} ({count})", "callback_data": f"adm_cat_open:{cid}"}])
    rows.append([{"text": "➕ دسته‌بندی جدید", "callback_data": "adm_cat_add"}])
    rows.append([{"text": "🔙", "callback_data": "adm_film_panel"}])
    return {"inline_keyboard": rows}


def create_film_request(user_id: int, item_type: str, item_id: str) -> str:
    token = f"{user_id}_{time.time_ns()}"
    wait = 7
    with film_lock:
        pending_film_requests[user_id] = {
            "token": token,
            "type": item_type,
            "id": str(item_id),
            "ready_at": time.time() + wait,
            "delivered": False,
        }
    return token


def activate_film_button(chat_id: int, user_id: int, token: str) -> None:
    time.sleep(7)
    with film_lock:
        req = pending_film_requests.get(user_id)
        if not req or req.get("token") != token or req.get("delivered"):
            return
    # نمایش دکمه به‌صورت پنل کیبورد پایین صفحه، شبیه نمونه ارسالی
    send_message(
        chat_id,
        "انجام دادم ✅",
        keyboard([[{"text": "انجام دادم ✅"}]])
    )


def deliver_film_request(chat_id: int, user_id: int, token: str) -> tuple[bool, str]:
    with film_lock:
        req = pending_film_requests.get(user_id)
        if not req or req.get("token") != token:
            return False, "❌ این درخواست دیگر معتبر نیست."
        if time.time() < float(req["ready_at"]):
            return False, "⏳ هنوز ۷ ثانیه کامل نشده است."
        if req.get("delivered"):
            return False, "این فایل قبلاً ارسال شده است."
        # Mark before Telegram send: repeated callbacks cannot start a second send.
        req["delivered"] = True

    data = load_film_db()
    try:
        if req["type"] == "film":
            fid = req["id"]
            film = data["films"].get(fid)
            if not film:
                raise RuntimeError("فیلم پیدا نشد")
            sent_msg = send_video(
                chat_id, film["file_id"], film.get("caption", read_text("film.blue")),
                reply_markup=film_reaction_keyboard(fid), schedule_delete=False,
            )
            if sent_msg and sent_msg.get("ok"):
                delete_later(chat_id, int(sent_msg["result"]["message_id"]))
                increment_view(fid)
        else:
            pack = data["packs"].get(req["id"])
            if not pack:
                raise RuntimeError("پک پیدا نشد")
            sent = 0
            for fid in pack.get("film_ids", []):
                fid = str(fid)
                film = data["films"].get(fid)
                if film:
                    sent_msg = send_video(
                        chat_id, film["file_id"], film.get("caption", read_text("film.blue")),
                        reply_markup=film_reaction_keyboard(fid), schedule_delete=False,
                    )
                    if sent_msg and sent_msg.get("ok"):
                        delete_later(chat_id, int(sent_msg["result"]["message_id"]))
                        increment_view(fid)
                    sent += 1
            if not sent:
                raise RuntimeError("این پک خالی است")
        with film_lock:
            pending_film_requests.pop(user_id, None)
        return True, ""
    except Exception as exc:
        with film_lock:
            req = pending_film_requests.get(user_id)
            if req and req.get("token") == token:
                req["delivered"] = False
        return False, f"❌ ارسال ناموفق بود: {exc}"


def handle_film_callback(callback: dict) -> None:
    callback_id = callback.get("id")
    user_id = int(callback.get("from", {}).get("id", 0))
    message = callback.get("message", {})
    chat_id = int(message.get("chat", {}).get("id", user_id))
    data = callback.get("data", "")
    tg("answerCallbackQuery", callback_query_id=callback_id)

    if data == "force_join_check":
        if require_force_join(chat_id, user_id):
            send_message(chat_id, read_text("start.blue") + "\n\n✅ عضویت شما تأیید شد.", KEY_HOME)
        return

    if not is_admin(user_id) and not require_force_join(chat_id, user_id):
        return

    if data == "noop":
        tg("answerCallbackQuery", callback_query_id=callback_id)
        return

    if data == "film_home":
        send_message(chat_id, "🎬 بخش فیلم:\n\nفیلم تکی یا پک فیلم را انتخاب کنید:", {
            "inline_keyboard": [
                [{"text": "🎬 فیلم تکی", "callback_data": "film_list:0"}],
                [{"text": "🎁 پک فیلم", "callback_data": "pack_list:0"}],
            ]
        })
        return
    if data == "film_list" or data.startswith("film_list:"):
        page = int(data.split(":", 1)[1]) if ":" in data else 0
        db = load_film_db()
        if db["films"]:
            send_message(chat_id, "🎬 فیلم موردنظر را انتخاب کنید:", film_menu_keyboard(page))
        else:
            send_message(chat_id, "❌ هنوز فیلمی ثبت نشده است.")
        return
    if data == "pack_list" or data.startswith("pack_list:"):
        page = int(data.split(":", 1)[1]) if ":" in data else 0
        db = load_film_db()
        if db["packs"]:
            send_message(chat_id, "🎁 پک موردنظر را انتخاب کنید:", pack_menu_keyboard(page))
        else:
            send_message(chat_id, "❌ هنوز پکی ساخته نشده است.")
        return
    if data.startswith("film_get:") or data.startswith("pack_get:"):
        typ, item_id = data.split(":", 1)
        db = load_film_db()
        exists = item_id in (db["films"] if typ == "film_get" else db["packs"])
        if not exists:
            send_message(chat_id, "❌ این مورد دیگر موجود نیست.")
            return
        kind = "film" if typ == "film_get" else "pack"
        token = create_film_request(user_id, kind, item_id)
        channel = read_text("film_channel.blue", "").strip() or read_text("channel.txt", "").strip()
        channel_text = f"@{channel.lstrip('@')}" if channel else "@چنل"
        label = "فایل" if kind == "film" else "پک"
        send_message(chat_id, f"🎬 لطفا جهت دریافت {label} ابتدا پست از کانال {channel_text} را لایک کنید و سپس برگردید و دکمه انجام دادم را فشار دهید!")
        threading.Thread(target=activate_film_button, args=(chat_id, user_id, token), daemon=True).start()
        return
    if data.startswith("film_done:"):
        token = data.split(":", 1)[1]
        ok, msg = deliver_film_request(chat_id, user_id, token)
        if not ok:
            tg("answerCallbackQuery", callback_query_id=callback_id, text=msg, show_alert=True)
        else:
            tg("answerCallbackQuery", callback_query_id=callback_id, text="✅ فایل ارسال شد")
            # Remove the button from the exact message that was clicked.
            if message.get("message_id"):
                edit_message(chat_id, int(message["message_id"]), "✅ فایل برای شما ارسال شد.", {"inline_keyboard": []})
        return

    if data == "cat_list":
        db = load_film_db()
        if not db["categories"]:
            send_message(chat_id, "❌ هنوز دسته‌بندی‌ای ساخته نشده است.")
        else:
            send_message(chat_id, "📂 دسته‌بندی موردنظر را انتخاب کنید:", category_menu_keyboard())
        return
    if data.startswith("cat_open:"):
        rest = data.split(":", 1)[1]
        if ":" in rest:
            cid, page_str = rest.rsplit(":", 1)
            page = int(page_str)
        else:
            cid, page = rest, 0
        db = load_film_db()
        cat = db["categories"].get(cid)
        if not cat:
            send_message(chat_id, "❌ این دسته‌بندی دیگر موجود نیست.")
            return
        send_message(chat_id, f"📂 دسته «{cat.get('name')}» — فیلم موردنظر را انتخاب کنید:", category_films_keyboard(cid, page))
        return

    if data.startswith("react:"):
        _, reaction, fid = data.split(":", 2)
        if reaction not in {"like", "dislike"}:
            return
        likes, dislikes = apply_reaction(fid, user_id, reaction)
        if message.get("message_id"):
            tg(
                "editMessageReplyMarkup",
                chat_id=chat_id,
                message_id=int(message["message_id"]),
                reply_markup=json.dumps(film_reaction_keyboard(fid), ensure_ascii=False),
            )
        return

    if not is_admin(user_id):
        tg("answerCallbackQuery", callback_query_id=callback_id, text="❌ دسترسی ندارید", show_alert=True)
        return

    # Admin film manager
    if data == "adm_film_add":
        write_text("step.txt", "catalog_film")
        send_message(chat_id, "➕ فیلم جدید را به صورت ویدیو ارسال یا فوروارد کنید.\nبعد از آن نام فیلم را می‌پرسم.", KEY_BACK)
    elif data == "adm_film_list" or data.startswith("adm_film_list:"):
        page = int(data.split(":", 1)[1]) if ":" in data else 0
        db = load_film_db()
        items = list(db["films"].items())
        page_items, total_pages, page = _paginate(items, page, ADMIN_FILM_PAGE_SIZE)
        rows = []
        for fid, f in page_items:
            rows.append([{"text": f"🎬 {f.get('name', 'فیلم')} (👁 {f.get('views', 0)})", "callback_data": f"adm_film_stat:{fid}"}])
            rows.append([
                {"text": "✏️ ویرایش نام", "callback_data": f"adm_film_rename:{fid}"},
                {"text": "🔗 لینک", "callback_data": f"adm_film_link:{fid}"},
                {"text": "🗑 حذف", "callback_data": f"adm_film_del:{fid}"},
            ])
        rows.extend(_pagination_row(page, total_pages, "adm_film_list"))
        rows.append([{"text": "🔙", "callback_data": "adm_film_panel"}])
        send_message(chat_id, "📋 فیلم‌ها (روی نام بزنید تا آمار را ببینید):", {"inline_keyboard": rows})
    elif data == "adm_film_stats":
        db = load_film_db()
        films = sorted(db["films"].items(), key=lambda kv: int(kv[1].get("views", 0)), reverse=True)
        if not films:
            send_message(chat_id, "❌ هنوز فیلمی ثبت نشده است.", admin_film_keyboard())
        else:
            lines = [
                f"{i}. {f.get('name','فیلم')} — 👁 {f.get('views',0)} | 👍 {f.get('likes',0)} | 👎 {f.get('dislikes',0)}"
                for i, (fid, f) in enumerate(films, 1)
            ]
            send_message(chat_id, "📊 آمار بازدید فیلم‌ها (بر اساس بیشترین بازدید):\n\n" + "\n".join(lines), admin_film_keyboard())
    elif data.startswith("adm_film_stat:"):
        fid = data.split(":", 1)[1]; db = load_film_db(); f = db["films"].get(fid)
        if not f:
            send_message(chat_id, "❌ فیلم پیدا نشد.", admin_film_keyboard())
        else:
            send_message(
                chat_id,
                f"📊 آمار «{f.get('name','فیلم')}»\n\n👁 بازدید: {f.get('views',0)}\n👍 مثبت: {f.get('likes',0)}\n👎 منفی: {f.get('dislikes',0)}",
                {"inline_keyboard": [[{"text": "🔙", "callback_data": "adm_film_list"}]]},
            )
    elif data.startswith("adm_film_rename:"):
        fid = data.split(":", 1)[1]
        write_text("step.txt", f"catalog_film_rename:{fid}")
        send_message(chat_id, "✍️ نام جدید فیلم را ارسال کنید:", KEY_BACK)
    elif data == "adm_pack_add":
        write_text("step.txt", "catalog_pack_name")
        send_message(chat_id, "🎁 نام پک را ارسال کنید.", KEY_BACK)
    elif data == "adm_pack_list":
        db=load_film_db(); rows=[]
        for pid,p in db["packs"].items():
            rows.append([{"text": f"🎁 {p.get('name','پک')} ({len(p.get('film_ids',[]))})", "callback_data": f"adm_pack_open:{pid}"}])
        rows.append([{"text": "🔙", "callback_data": "adm_film_panel"}])
        send_message(chat_id,"📦 پک‌ها:",{"inline_keyboard":rows})
    elif data == "adm_film_channel":
        write_text("step.txt", "catalog_channel")
        send_message(chat_id, "⚙️ یوزرنیم کانال دریافت را بدون @ یا با @ بفرستید.\nمثال: @my_channel", KEY_BACK)
    elif data == "adm_film_panel":
        send_message(chat_id, "🎬 مدیریت فیلم و پک", admin_film_keyboard())
    elif data.startswith("adm_film_link:"):
        fid = data.split(":", 1)[1]
        db = load_film_db()
        film = db["films"].get(fid)
        if not film:
            send_message(chat_id, "❌ فیلم پیدا نشد.", admin_film_keyboard())
            return
        link = film_deep_link(fid)
        if not link:
            send_message(chat_id, "❌ نام کاربری ربات دریافت نشد. توکن یا اتصال تلگرام را بررسی کنید.", admin_film_keyboard())
            return
        send_message(
            chat_id,
            f"🔗 لینک اختصاصی فیلم «{film.get('name', 'فیلم')}»:\n\n{link}\n\nبا باز کردن لینک، ربات همان فیلم را برای کاربر ارسال می‌کند.",
            admin_film_keyboard(),
        )
    elif data.startswith("adm_film_del:"):
        fid=data.split(":",1)[1]; db=load_film_db()
        if fid in db["films"]:
            name=db["films"][fid].get("name","فیلم"); del db["films"][fid]
            for pack in db["packs"].values(): pack["film_ids"]=[x for x in pack.get("film_ids",[]) if str(x)!=fid]
            save_film_db(db); send_message(chat_id,f"✅ «{name}» حذف شد.",admin_film_keyboard())
    elif data.startswith("adm_pack_open:"):
        pid=data.split(":",1)[1]; db=load_film_db(); pack=db["packs"].get(pid)
        if not pack: return
        names=[db["films"][str(x)].get("name","فیلم") for x in pack.get("film_ids",[]) if str(x) in db["films"]]
        send_message(chat_id,f"🎁 {pack.get('name')}\n\n"+("\n".join("• "+x for x in names) if names else "این پک خالی است."),{"inline_keyboard":[[{"text":"➕ افزودن فیلم به پک","callback_data":f"adm_pack_choose:{pid}"}],[{"text":"🗑 حذف پک","callback_data":f"adm_pack_del:{pid}"}],[{"text":"🔙","callback_data":"adm_pack_list"}]]})
    elif data.startswith("adm_pack_choose:"):
        pid=data.split(":",1)[1]; db=load_film_db(); rows=[]
        for fid,f in db["films"].items(): rows.append([{"text":f"🎬 {f.get('name')}","callback_data":f"adm_pack_add:{pid}:{fid}"}])
        rows.append([{"text":"🔙","callback_data":f"adm_pack_open:{pid}"}]); send_message(chat_id,"فیلم را انتخاب کنید:",{"inline_keyboard":rows})
    elif data.startswith("adm_pack_add:"):
        _,pid,fid=data.split(":",2); db=load_film_db()
        if pid in db["packs"] and fid in db["films"] and fid not in db["packs"][pid].setdefault("film_ids",[]):
            db["packs"][pid]["film_ids"].append(fid); save_film_db(db)
        send_message(chat_id,"✅ فیلم به پک اضافه شد.", {"inline_keyboard":[[{"text":"🔙 پک","callback_data":f"adm_pack_open:{pid}"}]]})
    elif data.startswith("adm_pack_del:"):
        pid=data.split(":",1)[1]; db=load_film_db(); db["packs"].pop(pid,None); save_film_db(db); send_message(chat_id,"✅ پک حذف شد.",admin_film_keyboard())

    # Category management
    elif data == "adm_cat_list":
        send_message(chat_id, "📂 مدیریت دسته‌بندی‌ها:", admin_category_keyboard())
    elif data == "adm_cat_add":
        write_text("step.txt", "catalog_cat_name")
        send_message(chat_id, "✍️ نام دسته‌بندی جدید را ارسال کنید:", KEY_BACK)
    elif data.startswith("adm_cat_open:"):
        cid = data.split(":", 1)[1]; db = load_film_db(); cat = db["categories"].get(cid)
        if not cat:
            send_message(chat_id, "❌ این دسته‌بندی پیدا نشد.", admin_category_keyboard()); return
        names = [f.get("name", "فیلم") for f in db["films"].values() if str(f.get("category_id")) == cid]
        send_message(
            chat_id,
            f"📂 {cat.get('name')}\n\n" + ("\n".join("• " + x for x in names) if names else "این دسته خالی است."),
            {"inline_keyboard": [
                [{"text": "➕ افزودن فیلم", "callback_data": f"adm_cat_choose:{cid}"}],
                [{"text": "✏️ ویرایش نام", "callback_data": f"adm_cat_rename:{cid}"}],
                [{"text": "🗑 حذف دسته", "callback_data": f"adm_cat_del:{cid}"}],
                [{"text": "🔙", "callback_data": "adm_cat_list"}],
            ]},
        )
    elif data.startswith("adm_cat_choose:"):
        cid = data.split(":", 1)[1]; db = load_film_db(); rows = []
        for fid, f in db["films"].items():
            rows.append([{"text": f"🎬 {f.get('name')}", "callback_data": f"adm_cat_assign:{cid}:{fid}"}])
        rows.append([{"text": "🔙", "callback_data": f"adm_cat_open:{cid}"}])
        send_message(chat_id, "فیلم را انتخاب کنید:", {"inline_keyboard": rows})
    elif data.startswith("adm_cat_assign:"):
        _, cid, fid = data.split(":", 2); db = load_film_db()
        if cid in db["categories"] and fid in db["films"]:
            db["films"][fid]["category_id"] = cid; save_film_db(db)
        send_message(chat_id, "✅ فیلم به دسته اضافه شد.", {"inline_keyboard": [[{"text": "🔙 دسته", "callback_data": f"adm_cat_open:{cid}"}]]})
    elif data.startswith("adm_cat_rename:"):
        cid = data.split(":", 1)[1]
        write_text("step.txt", f"catalog_cat_rename:{cid}")
        send_message(chat_id, "✍️ نام جدید دسته‌بندی را ارسال کنید:", KEY_BACK)
    elif data.startswith("adm_cat_del:"):
        cid = data.split(":", 1)[1]; db = load_film_db()
        db["categories"].pop(cid, None)
        for f in db["films"].values():
            if str(f.get("category_id")) == cid:
                f["category_id"] = None
        save_film_db(db)
        send_message(chat_id, "✅ دسته‌بندی حذف شد.", admin_category_keyboard())


def add_member(user_id: int):
    current = set(x.strip() for x in read_text("members.txt").splitlines() if x.strip())
    if str(user_id) not in current:
        with (BASE_DIR / "members.txt").open("a", encoding="utf-8") as f:
            f.write(f"{user_id}\n")


def load_force_channels() -> list[str]:
    """Load mandatory-join channels, one public @username per entry."""
    try:
        raw = json.loads(read_text("force_channels.json", "[]"))
        if isinstance(raw, list):
            return list(dict.fromkeys(
                str(x).strip().lstrip("@").strip()
                for x in raw if str(x).strip()
            ))
    except (ValueError, TypeError):
        pass
    return []


def save_force_channels(channels: list[str]) -> None:
    clean = list(dict.fromkeys(
        str(x).strip().lstrip("@").strip()
        for x in channels if str(x).strip()
    ))
    write_text("force_channels.json", json.dumps(clean, ensure_ascii=False, indent=2))


def force_join_keyboard(channels: list[str]) -> dict:
    rows = []
    for channel in channels:
        rows.append([{
            "text": f"📢 عضویت در @{channel}",
            "url": f"https://t.me/{channel}",
        }])
    rows.append([{"text": "✅ عضو شدم | بررسی", "callback_data": "force_join_check"}])
    return {"inline_keyboard": rows}


def missing_force_channels(user_id: int) -> list[str]:
    return [channel for channel in load_force_channels()
            if not subscribed(channel, user_id)]


def require_force_join(chat_id: int, user_id: int) -> bool:
    """Return True when the user is allowed to continue."""
    if is_admin(user_id):
        return True
    missing = missing_force_channels(user_id)
    if not missing:
        return True
    send_message(
        chat_id,
        "❌ شما هنوز عضو کانال‌های اجباری نشده‌اید.\n"
        "ابتدا در همه کانال‌های زیر عضو شوید، سپس روی «عضو شدم | بررسی» بزنید.",
        force_join_keyboard(missing),
    )
    return False


def get_channel_member_status(channel: str, user_id: int) -> Optional[str]:
    if not channel:
        return None
    res = tg("getChatMember", chat_id=f"@{channel.lstrip('@')}", user_id=user_id)
    if res and res.get("ok"):
        return res["result"].get("status")
    return None


def subscribed(channel: str, user_id: int) -> bool:
    status = get_channel_member_status(channel, user_id)
    return status in {"member", "creator", "administrator"}


def spam_allowed(user_id: int) -> bool:
    """
    Equivalent to the PHP spam limiter, stored per-user.
    """
    p = BASE_DIR / "spam"
    p.mkdir(exist_ok=True)
    f = p / f"{user_id}.json"
    now = int(time.time())

    try:
        state = json.loads(f.read_text(encoding="utf-8"))
    except Exception:
        state = None

    if isinstance(state, list) and state:
        if isinstance(state[0], str) and state[0].startswith("time "):
            until = int(state[0].split(" ", 1)[1])
            if until >= now:
                return False
            state = [1, now + 2]
        elif len(state) >= 2 and now < int(state[1]):
            count = int(state[0]) + 1
            if count > 3:
                until = now + 260
                f.write_text(json.dumps([f"time {until}"]), encoding="utf-8")
                return False
            state = [count, int(state[1])]
        else:
            state = [1, now + 2]
    else:
        state = [1, now + 2]

    f.write_text(json.dumps(state), encoding="utf-8")
    return True


def send_media_with_countdown(chat_id: int, media_type: str):
    channel = read_text("channel.txt").strip()
    if not subscribed(channel, chat_id):
        send_message(
            chat_id,
            "🔐 | برای استفاده از ربات، ابتدا عضو کانال ما شوید.\n"
            f"کانال: @{channel}\n\n"
            "✅ | بعد از عضویت /start را بزنید.",
        )
        return

    files = {
        "photo": "pic.txt",
        "film": "vid.txt",
        "gif": "gif.txt",
    }
    media = load_csv(f"media/{files[media_type]}")
    if not media:
        send_message(chat_id, f"🔓 | فعلاً {media_type} موجود نیست.")
        return

    selected = random.choice(media)
    captions = {
        "photo": read_text("png.blue"),
        "film": read_text("film.blue"),
        "gif": read_text("gif.blue"),
    }
    caption = captions[media_type]

    if media_type == "photo":
        sent = send_photo(chat_id, selected, caption)
    elif media_type == "film":
        sent = send_video(chat_id, selected, caption)
    else:
        sent = send_document(chat_id, selected, caption)

    if not sent or not sent.get("ok"):
        return

    seconds = read_text("taim.blue", "20").strip()
    try:
        seconds = max(1, min(int(seconds), 300))
    except ValueError:
        seconds = 20

    countdown = send_message(
        chat_id,
        f"<pre>محتوا در <i>{seconds}</i> ثانیه دیگر حذف می‌شود.\n"
        "لطفاً آن را برای پیام‌های ذخیره‌شده ارسال کنید.</pre>",
        parse_mode="HTML",
    )
    if not countdown or not countdown.get("ok"):
        return

    countdown_id = countdown["result"]["message_id"]
    media_id = sent["result"]["message_id"]

    # Short countdowns preserve the original behavior without creating
    # an unbounded worker/thread.
    for remaining in range(seconds - 1, -1, -1):
        time.sleep(1)
        edit_message(
            chat_id,
            countdown_id,
            f"<pre>محتوا در <i>{remaining}</i> ثانیه دیگر حذف می‌شود.\n"
            "لطفاً آن را برای پیام‌های ذخیره‌شده ارسال کنید.</pre>",
            parse_mode="HTML",
        )

    delete_message(chat_id, media_id)
    edit_message(
        chat_id,
        countdown_id,
        "<i>این پیام حذف شد</i>",
        parse_mode="HTML",
    )


def admin_settings(chat_id: int):
    send_message(
        chat_id,
        "مالک عزیز به بخش تنظیمات ادمین خوش آمدید ⚙️🧠",
        keyboard([
            [{"text": "➕ ادمین اول"}, {"text": "✖️ ادمین اول"}],
            [{"text": "➕ادمین دوم"}, {"text": "✖️ادمین دوم"}],
            [{"text": "➕ادمین سوم"}, {"text": "✖️ادمین سوم"}],
            [{"text": "➕ادمین چهارم"}, {"text": "✖️ادمین چهارم"}],
            [{"text": "➕ادمین پنجم"}, {"text": "✖️ادمین پنجم"}],
            [{"text": "💙《 @TMBLUE 》💙"}, {"text": "📋 وضعیت"}],
            [{"text": "/panel"}],
        ]),
        parse_mode="HTML",
    )


def broadcast_text(chat_id: int, text: str):
    send_message(chat_id, "✅ | درخواست شما ثبت شد.")
    for line in read_text("members.txt").splitlines():
        line = line.strip()
        if not line:
            continue
        try:
            uid = int(line)
        except ValueError:
            continue
        tg(
            "sendMessage",
            chat_id=uid,
            text=text,
            parse_mode="HTML",
            disable_web_page_preview="true",
        )
        time.sleep(0.05)


def broadcast_forward(chat_id: int, message_id: int):
    send_message(chat_id, "✅ | درخواست شما ثبت شد.")
    for line in read_text("members.txt").splitlines():
        line = line.strip()
        if not line:
            continue
        try:
            uid = int(line)
        except ValueError:
            continue
        forward_message(uid, chat_id, message_id)
        time.sleep(0.05)


def handle(update: Dict[str, Any]):
    if "callback_query" in update:
        handle_film_callback(update["callback_query"])
        return
    if "message" not in update:
        return

    message = update["message"]
    chat = message.get("chat", {})
    user = message.get("from", {})
    chat_id = chat.get("id")
    user_id = user.get("id")
    text = message.get("text")
    message_id = message.get("message_id")

    if not chat_id or not user_id:
        return

    add_member(user_id)

    admins = load_admins()

    if user_id not in admins and not spam_allowed(user_id):
        send_message(
            chat_id,
            "🔐 | به علت اسپم، 260 ثانیه از ربات مسدود شدید.\n"
            "⚒ | لطفاً آهسته‌تر با ربات کار کنید.",
        )
        return

    step = read_text("step.txt", "none").strip()

    # Block all non-admin features until mandatory channel membership is confirmed.
    if user_id not in admins and not require_force_join(chat_id, user_id):
        return

    # /start and Telegram deep links such as /start film_12
    if text and text.lower().startswith("/start"):
        parts = text.split(maxsplit=1)
        payload = parts[1].strip() if len(parts) > 1 else ""
        if payload.startswith("film_"):
            film_id = payload[5:].strip()
            db = load_film_db()
            film = db["films"].get(film_id)
            if not film:
                send_message(chat_id, "❌ این فیلم دیگر موجود نیست.", KEY_HOME)
                return
            send_video(
                chat_id, film["file_id"], film.get("caption", read_text("film.blue")),
                reply_markup=film_reaction_keyboard(film_id), schedule_delete=False,
            )
            increment_view(film_id)
            return
        send_message(chat_id, read_text("start.blue"), KEY_HOME)
        return

    # دکمه پنل دریافت فیلم؛ بعد از پایان شمارش معکوس فعال می‌شود
    if text == "انجام دادم ✅":
        with film_lock:
            req = pending_film_requests.get(user_id)
        if req:
            ok, msg = deliver_film_request(chat_id, user_id, str(req["token"]))
            if ok:
                send_message(chat_id, "✅ فایل برای شما ارسال شد.", KEY_REMOVE)
            else:
                send_message(chat_id, msg)
        return

    # User media
    if text == "🔞 | عکس":
        send_media_with_countdown(chat_id, "photo")
        return
    if text == "🔞 | گیف":
        send_media_with_countdown(chat_id, "gif")
        return
    if text == "🔞 | فیلم":
        db = load_film_db()
        if not db["films"]:
            send_message(chat_id, "❌ هنوز فیلمی برای دریافت قرار داده نشده است.")
        else:
            send_message(chat_id, "🎬 فیلم موردنظر را انتخاب کنید:", film_menu_keyboard())
        return
    if text == "🎲 | فیلم رندوم":
        pool = load_film_db().get("random_films", [])
        if not pool:
            send_message(chat_id, "❌ فعلاً فیلمی تو استخر رندوم نیست.")
            return
        file_id = random.choice(pool)
        send_video(chat_id, file_id, read_text("film.blue"))
        return
    if text == "🎁 | پک فیلم":
        db = load_film_db()
        if not db["packs"]:
            send_message(chat_id, "❌ هنوز پکی برای دریافت قرار داده نشده است.")
        else:
            send_message(chat_id, "🎁 پک موردنظر را انتخاب کنید:", pack_menu_keyboard())
        return
    if text == "📂 | دسته‌بندی‌ها":
        db = load_film_db()
        if not db["categories"]:
            send_message(chat_id, "❌ هنوز دسته‌بندی‌ای ساخته نشده است.")
        else:
            send_message(chat_id, "📂 دسته‌بندی موردنظر را انتخاب کنید:", category_menu_keyboard())
        return

    # Panel
    if text in {"/panel", "بازگشت"} and user_id in admins:
        write_text("step.txt", "none")
        send_message(
            chat_id,
            "<pre>❤️ | ادمین گرامی به پنل مدیریت پیشرفته خوش آمدید</pre>",
            KEY_PANEL,
            parse_mode="HTML",
        )
        return

    if text == "📊 | آمار ربات" and user_id in admins:
        members = [x for x in read_text("members.txt").splitlines() if x.strip()]
        send_message(
            chat_id,
            "🌹《 📊 | آمار ربات 》🌹\n"
            "➖➖➖➖\n"
            f"🔐 | تعداد کاربران: {len(members)}\n"
            f"🔐 | تعداد فیلم‌ها: {len(load_csv('media/vid.txt'))}\n"
            f"🔐 | تعداد عکس‌ها: {len(load_csv('media/pic.txt'))}\n"
            f"🔐 | تعداد گیف‌ها: {len(load_csv('media/gif.txt'))}",
        )
        return

    if text == "🎬 مدیریت فیلم و پک" and user_id in admins:
        send_message(chat_id, "🎬 مدیریت فیلم و پک", admin_film_keyboard())
        return

    if text == "💾 بکاپ کامل" and user_id in admins:
        send_message(chat_id, "⏳ | در حال ساخت فایل بکاپ...")
        try:
            zip_path = build_backup_zip()
            result = send_document_upload(
                chat_id, zip_path,
                caption=f"💾 بکاپ کامل ربات\n🗓 {time.strftime('%Y-%m-%d %H:%M:%S')}",
            )
            if not result or not result.get("ok"):
                send_message(chat_id, "❌ ارسال بکاپ ناموفق بود.")
        finally:
            try:
                zip_path.unlink(missing_ok=True)
            except Exception:
                pass
        return

    if text == "🔞 | افزودن مدیا" and user_id in admins:
        write_text("step.txt", "none")
        send_message(chat_id, "🔍 | نوع مدیا را انتخاب کنید:", KEY_MEDIA)
        return

    if text in {"+ گیف", "+ عکس", "+ فیلم"} and user_id in admins:
        mapping = {"+ گیف": "gif", "+ فیلم": "film", "+ عکس": "photo"}
        write_text("step.txt", mapping[text])
        send_message(
            chat_id,
            f"✅ | لطفاً {text} را ارسال کنید و در پایان «بازگشت» را بزنید.",
            KEY_BACK,
        )
        return

    # Media upload
    if step == "film" and message.get("video"):
        file_id = message["video"].get("file_id")
        if file_id:
            append_csv(MEDIA_DIR / "vid.txt", file_id)
            data = load_film_db(); fid = str(data["next_film_id"]); data["next_film_id"] += 1
            data["films"][fid] = {"name": f"فیلم {fid}", "file_id": file_id}
            save_film_db(data)
        send_message(chat_id, "✅ | فیلم با موفقیت اضافه شد و در لیست انتخاب فیلم قرار گرفت!", KEY_BACK)
        return

    if step == "catalog_film" and message.get("video"):
        file_id = message["video"].get("file_id")
        if file_id:
            write_text("step.txt", "catalog_film_name")
            write_text("pending_film_file.blue", file_id)
            send_message(chat_id, "✍️ نام فیلم را ارسال کنید:", KEY_BACK)
        return

    if step == "catalog_film_name" and text:
        file_id = read_text("pending_film_file.blue").strip()
        if file_id:
            data = load_film_db(); fid=str(data["next_film_id"]); data["next_film_id"] += 1
            data["films"][fid] = {"name": text, "file_id": file_id}
            save_film_db(data); write_text("pending_film_file.blue", ""); write_text("step.txt", "none")
            send_message(chat_id, f"✅ فیلم «{text}» اضافه شد.", admin_film_keyboard())
        return

    if step == "catalog_pack_name" and text:
        data=load_film_db(); pid=str(data["next_pack_id"]); data["next_pack_id"] += 1
        data["packs"][pid] = {"name": text, "film_ids": []}; save_film_db(data); write_text("step.txt","none")
        send_message(chat_id, f"✅ پک «{text}» ساخته شد. حالا از مدیریت پک‌ها فیلم‌ها را اضافه کنید.", admin_film_keyboard())
        return

    if step == "catalog_channel" and text and user_id in admins:
        write_text("film_channel.blue", text.lstrip("@").strip()); write_text("step.txt","none")
        send_message(chat_id, "✅ کانال دریافت فیلم تنظیم شد.", admin_film_keyboard())
        return

    if step.startswith("catalog_film_rename:") and text and user_id in admins:
        fid = step.split(":", 1)[1]
        data = load_film_db()
        if fid in data["films"]:
            data["films"][fid]["name"] = text
            save_film_db(data)
            write_text("step.txt", "none")
            send_message(chat_id, f"✅ نام فیلم به «{text}» تغییر یافت.", admin_film_keyboard())
        else:
            write_text("step.txt", "none")
            send_message(chat_id, "❌ این فیلم دیگر موجود نیست.", admin_film_keyboard())
        return

    if step == "catalog_cat_name" and text and user_id in admins:
        data = load_film_db()
        cid = str(data["next_category_id"]); data["next_category_id"] += 1
        data["categories"][cid] = {"name": text}
        save_film_db(data); write_text("step.txt", "none")
        send_message(chat_id, f"✅ دسته‌بندی «{text}» ساخته شد.", admin_category_keyboard())
        return

    if step.startswith("catalog_cat_rename:") and text and user_id in admins:
        cid = step.split(":", 1)[1]
        data = load_film_db()
        if cid in data["categories"]:
            data["categories"][cid]["name"] = text
            save_film_db(data)
            write_text("step.txt", "none")
            send_message(chat_id, f"✅ نام دسته‌بندی به «{text}» تغییر یافت.", admin_category_keyboard())
        else:
            write_text("step.txt", "none")
            send_message(chat_id, "❌ این دسته‌بندی دیگر موجود نیست.", admin_category_keyboard())
        return

    if step == "photo" and message.get("photo"):
        file_id = message["photo"][-1].get("file_id")
        if file_id:
            append_csv(MEDIA_DIR / "pic.txt", file_id)
        send_message(chat_id, "✅ | عکس با موفقیت اضافه شد!", KEY_BACK)
        return

    if step == "gif" and message.get("document"):
        file_id = message["document"].get("file_id")
        if file_id:
            append_csv(MEDIA_DIR / "gif.txt", file_id)
        send_message(chat_id, "✅ | گیف با موفقیت اضافه شد!", KEY_BACK)
        return

    # Broadcast
    if text == "🔒| فوروارد همگانی" and chat_id in admins:
        write_text("step.txt", "fwd")
        send_message(chat_id, "‼️ | لطفاً پیام مورد نظر را فوروارد کنید.", KEY_PANEL_ONLY)
        return

    if step == "fwd" and text != "/panel" and chat_id in admins:
        write_text("step.txt", "none")
        broadcast_forward(chat_id, message_id)
        return

    if text == "🔓 | پیام همگانی" and chat_id in admins:
        write_text("step.txt", "send")
        send_message(chat_id, "🎊 | لطفاً متن مورد نظر را ارسال کنید.", KEY_PANEL_ONLY)
        return

    if step == "send" and text != "/panel" and chat_id in admins and text:
        write_text("step.txt", "none")
        broadcast_text(chat_id, text)
        return

    # Settings
    if text == "تنظیم تایم حذف پست🌪️" and user_id in admins:
        write_text("step.txt", "se")
        send_message(
            chat_id,
            "⏱ تنظیم زمان حذف مدیا\n\nزمان حذف فیلم، عکس، گیف و پک فیلم را به ثانیه ارسال کنید.\nمثلاً 20 یعنی مدیا بعد از 20 ثانیه حذف شود.\nحداکثر زمان: 300 ثانیه.",
            KEY_BACK,
        )
        return

    if step == "se" and text:
        try:
            seconds = max(1, min(int(text), 300))
            write_text("taim.blue", seconds)
            write_text("step.txt", "none")
            send_message(chat_id, "✅ | تنظیم شد.", KEY_BACK)
        except ValueError:
            send_message(chat_id, "❌ | فقط عدد بر حسب ثانیه ارسال کنید.", KEY_BACK)
        return

    if text == "📢 | جوین اجباری" and user_id in admins:
        channels = load_force_channels()
        current = "\n".join(f"{i}. @{c}" for i, c in enumerate(channels, 1)) or "هنوز کانالی تنظیم نشده است."
        send_message(
            chat_id,
            "📢 مدیریت جوین اجباری\n\n"
            f"کانال‌های فعلی:\n{current}\n\n"
            "برای افزودن یا حذف، دستورهای زیر را ارسال کن:\n"
            "+کانال @username\n"
            "-کانال @username\n"
            "حذف همه کانال‌ها",
            KEY_BACK,
        )
        write_text("step.txt", "force_channels")
        return

    if step == "force_channels" and text and user_id in admins:
        value = text.strip()
        channels = load_force_channels()
        if value == "حذف همه کانال‌ها":
            save_force_channels([])
            write_text("step.txt", "none")
            send_message(chat_id, "✅ همه کانال‌های جوین اجباری حذف شدند.", KEY_PANEL)
            return
        if value.startswith("+کانال"):
            channel = value[len("+کانال"):].strip().lstrip("@").strip()
            if channel and all(ch.isalnum() or ch == "_" for ch in channel):
                channels.append(channel)
                save_force_channels(channels)
                send_message(chat_id, f"✅ کانال @{channel} به جوین اجباری اضافه شد.", KEY_BACK)
            else:
                send_message(chat_id, "❌ یوزرنیم کانال معتبر نیست. مثال: +کانال @my_channel", KEY_BACK)
            return
        if value.startswith("-کانال"):
            channel = value[len("-کانال"):].strip().lstrip("@").strip()
            save_force_channels([c for c in channels if c.lower() != channel.lower()])
            send_message(chat_id, f"✅ کانال @{channel} حذف شد.", KEY_BACK)
            return
        send_message(chat_id, "❌ دستور نامعتبر است. مثال: +کانال @my_channel", KEY_BACK)
        return

    if text == "🔐 | تنظیم کانال اول" and user_id in admins:
        write_text("step.txt", "set join channel")
        send_message(chat_id, "❗️| آیدی کانال را بدون @ ارسال کنید.", KEY_BACK)
        return

    if step == "set join channel" and text:
        write_text("channel.txt", text.lstrip("@").strip())
        write_text("step.txt", "none")
        send_message(chat_id, "✅ | کانال تنظیم شد.", KEY_BACK)
        return

    if text == "تنظیم متن استارت📝" and user_id in admins:
        write_text("step.txt", "setstart")
        send_message(chat_id, "لطفاً متن استارت را ارسال کنید.", KEY_BACK)
        return

    if step == "setstart" and text:
        write_text("start.blue", text)
        write_text("step.txt", "none")
        send_message(chat_id, "✅ | تنظیم شد.", KEY_BACK)
        return

    setting_map = {
        "تنظیم کپشن عکس🧠": ("pngpro", "png.blue", "کپشن عکس"),
        "تنظیم کپشن گیف🫀": ("gifpro", "gif.blue", "کپشن گیف"),
        "تنظیم کپشن فیلم❤️": ("filmpp", "film.blue", "کپشن فیلم"),
    }

    if text in setting_map and user_id in admins:
        step_name, _, label = setting_map[text]
        write_text("step.txt", step_name)
        send_message(chat_id, f"لطفاً {label} را ارسال کنید.", KEY_BACK)
        return

    for step_name, filename, _ in setting_map.values():
        if step == step_name and text:
            write_text(filename, text)
            write_text("step.txt", "none")
            send_message(chat_id, "✅ | تنظیم شد.", KEY_BACK)
            return

    if text == "🖼️ تنظیم بنر فیلم" and user_id in admins:
        write_text("step.txt", "set_banner")
        send_message(
            chat_id,
            "لطفاً عکسی که می‌خوای به‌عنوان بنر، قبل از هر فیلم فرستاده بشه رو ارسال کن.",
            KEY_BACK,
        )
        return

    if step == "set_banner" and message.get("photo"):
        file_id = message["photo"][-1].get("file_id")
        if file_id:
            write_text("banner.blue", file_id)
            write_text("step.txt", "none")
            send_message(chat_id, "✅ | بنر فیلم تنظیم شد؛ از الان قبل از هر فیلم فرستاده می‌شه.", KEY_PANEL)
        else:
            send_message(chat_id, "❌ | عکس شناسایی نشد، دوباره امتحان کن.", KEY_BACK)
        return

    if text == "🗑 حذف بنر فیلم" and user_id in admins:
        write_text("banner.blue", "")
        write_text("step.txt", "none")
        send_message(chat_id, "✅ | بنر فیلم حذف شد.", KEY_PANEL)
        return

    if text == "🎲 افزودن فیلم رندوم" and user_id in admins:
        write_text("step.txt", "bulk_random_add")
        count = len(load_film_db().get("random_films", []))
        send_message(
            chat_id,
            f"ویدیوها رو یکی‌یکی یا پشت‌سرهم (فوروارد هم اوکیه) برام بفرست، هر چندتا خواستی.\n"
            f"الان تو استخر رندوم {count} فیلم هست.\n"
            "وقتی تموم شد بنویس «پایان».",
            KEY_BACK,
        )
        return

    if step == "bulk_random_add" and message.get("video"):
        file_id = message["video"].get("file_id")
        if file_id:
            data = load_film_db()
            data.setdefault("random_films", []).append(file_id)
            save_film_db(data)
            send_message(chat_id, f"✅ اضافه شد. (مجموع الان: {len(data['random_films'])})")
        else:
            send_message(chat_id, "❌ فایل شناسایی نشد.")
        return

    if step == "bulk_random_add" and text in {"پایان", "تمام", "تموم"}:
        write_text("step.txt", "none")
        count = len(load_film_db().get("random_films", []))
        send_message(chat_id, f"✅ | تموم شد. الان مجموعاً {count} فیلم تو استخر رندومه.", KEY_PANEL)
        return

    if text == "🗑 پاک کردن فیلم‌های رندوم" and user_id in admins:
        data = load_film_db()
        data["random_films"] = []
        save_film_db(data)
        send_message(chat_id, "✅ | استخر فیلم‌های رندوم خالی شد.", KEY_PANEL)
        return

    # Delete all media
    delete_map = {
        "حذف همه گیف ها❌": "gif.txt",
        "حذف همه فیلم ها❌": "vid.txt",
        "حذف همه عکس ها❌": "pic.txt",
    }
    if text in delete_map and user_id in admins:
        (MEDIA_DIR / delete_map[text]).write_text("", encoding="utf-8")
        write_text("step.txt", "none")
        send_message(chat_id, "ادمین گرامی، دستور با موفقیت انجام شد 🧠🤍", KEY_PANEL_ONLY)
        return

    # Admin management
    if text == "تنظیمات ادمین⚙️":
        if user_id == OWNER_ID:
            admin_settings(chat_id)
        else:
            send_message(chat_id, "❌ متأسفانه اجازه دسترسی ندارید.")
        return

    add_admin_steps = {
        "➕ ادمین اول": ("adm11", "admin1.blue"),
        "➕ادمین دوم": ("ad22", "admin2.blue"),
        "➕ادمین سوم": ("add3", "admin3.blue"),
        "➕ادمین چهارم": ("addd4", "admin4.blue"),
        "➕ادمین پنجم": ("add55", "admin5.blue"),
    }
    if text in add_admin_steps and user_id in admins:
        st, filename = add_admin_steps[text]
        write_text("step.txt", st)
        send_message(chat_id, "مالک گرامی، آیدی عددی ادمین را ارسال کنید.", KEY_BACK)
        return

    for st, filename in add_admin_steps.values():
        if step == st and text and user_id in admins:
            write_text(filename, text.strip())
            write_text("step.txt", "none")
            send_message(chat_id, "✅ اضافه شد.", KEY_BACK)
            return

    remove_admin_steps = {
        "✖️ ادمین اول": "admin1.blue",
        "✖️ادمین دوم": "admin2.blue",
        "✖️ ادمین سوم": "admin3.blue",
        "✖️ادمین چهارم": "admin4.blue",
        "✖️ ادمین پنجم": "admin5.blue",
    }
    if text in remove_admin_steps and user_id in admins:
        write_text(remove_admin_steps[text], "❌not admin❌")
        write_text("step.txt", "none")
        send_message(chat_id, "ادمین گرامی، دستور با موفقیت انجام شد 🧠🤍", KEY_PANEL_ONLY)
        return

    if text == "📋 وضعیت" and user_id in admins:
        status = "\n".join(
            f"{i}️⃣ admin = {read_text(name).strip()}"
            for i, name in enumerate(
                ["admin1.blue", "admin2.blue", "admin3.blue", "admin4.blue", "admin5.blue"],
                1,
            )
        )
        send_message(chat_id, f"سلام مالک گرامی 👋\nوضعیت ادمین‌ها:\n{status}", KEY_PANEL_ONLY)
        return

    if text in {"بازگشت", "انصراف"}:
        write_text("step.txt", "none")
        send_message(chat_id, "😁 سلام کاربر گرامی به صفحه اصلی ربات خوش آمدید 🌹😃", KEY_HOME)
        return


def main():
    ensure_files()
    me = tg("getMe")
    if not me or not me.get("ok"):
        raise SystemExit(
            "BOT_TOKEN معتبر نیست یا Telegram در دسترس نیست. "
            "توکن را در BOT_TOKEN تنظیم کنید."
        )

    log.info("Bot started: @%s | owner=%s", me["result"].get("username"), OWNER_ID)

    offset = 0
    while True:
        try:
            result = tg(
                "getUpdates",
                offset=offset,
                timeout=25,
                allowed_updates=json.dumps(["message", "callback_query"]),
            )
            if not result or not result.get("ok"):
                time.sleep(3)
                continue

            for update in result.get("result", []):
                offset = update["update_id"] + 1
                try:
                    handle(update)
                except Exception:
                    log.exception("Error while processing update %s", update.get("update_id"))
        except KeyboardInterrupt:
            log.info("Bot stopped.")
            break
        except Exception:
            log.exception("Polling error")
            time.sleep(3)


if __name__ == "__main__":
    main()
