import html
import logging
import os
import re
import json
import urllib.request
import urllib.parse
import urllib.error
from telegram import Bot, Update, InlineKeyboardButton, InlineKeyboardMarkup, BotCommand, MessageEntity
from telegram.error import TelegramError
from telegram.ext import (
    Updater, CommandHandler, MessageHandler, CallbackQueryHandler, Filters,
)

# TODO: توکن ربات جدید (اکانت پرمیوم مشتری) رو اینجا بذار
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "8963632052:AAGIyfH6O5UvsfovSk2c9Vlve4xosoBuyb4").strip()
CHANNEL = "@NetMovis"  # اگه اسم کانال فرق داره عوضش کن

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
user_data = {}

# ---------------------------------------------------------------------------
# پچ برای python-telegram-bot 12.x: این نسخه از کتابخونه نوع entity به اسم
# «custom_emoji» رو می‌شناسه (چون مستقیماً از JSON تلگرام میاد) ولی موقع ساختن
# آبجکت MessageEntity فیلد custom_emoji_id رو دور می‌ریزه، چون این ویژگی از
# اون کتابخونه جدیدتره. نتیجه‌ش این بود که /emojiid با AttributeError کرش
# می‌کرد. این پچ همون فیلد رو نگه می‌داره تا /emojiid درست کار کنه.
# ---------------------------------------------------------------------------
try:
    from telegram import MessageEntity as _MessageEntity

    if not getattr(_MessageEntity, "_netmovis_custom_emoji_patch", False):
        _orig_entity_init = _MessageEntity.__init__

        def _patched_entity_init(self, *args, **kwargs):
            custom_emoji_id = kwargs.pop("custom_emoji_id", None)
            _orig_entity_init(self, *args, **kwargs)
            if custom_emoji_id is not None:
                self.custom_emoji_id = custom_emoji_id

        _MessageEntity.__init__ = _patched_entity_init

        # در python-telegram-bot 12.x مشکل اصلی فقط __init__ نیست؛ de_json هم
        # فیلد custom_emoji_id را از آپدیت تلگرام به آبجکت MessageEntity پاس نمی‌دهد.
        # بدون این پچ، /emojiid آیدی ایموجی‌های پرمیوم را خالی/نامشخص نشان می‌دهد.
        _orig_entity_de_json = _MessageEntity.de_json

        def _patched_entity_de_json(cls, data, bot):
            obj = _orig_entity_de_json(data, bot)
            if obj is not None and isinstance(data, dict) and data.get("custom_emoji_id"):
                obj.custom_emoji_id = data.get("custom_emoji_id")
            return obj

        _MessageEntity.de_json = classmethod(_patched_entity_de_json)
        _MessageEntity._netmovis_custom_emoji_patch = True
except Exception:
    logger.exception("پچ MessageEntity اعمال نشد؛ ممکنه /emojiid درست کار نکنه")

SKIP_TAGS = [
    "زیرنویس_چسبیده", "زیرنویس_چسبیده_فارسی", "بدون_سانسور",
    "پیشنهادی", "دوبله_فارسی", "فیلم_خارجی"
]

NOT_FILM_NAME = [
    "زیرنویس_چسبیده", "زیرنویس_چسبیده_فارسی", "بدون_سانسور",
    "پیشنهادی", "دوبله_فارسی", "فیلم_خارجی", "درام", "عاشقانه",
    "کمدی", "اکشن", "ترسناک", "هیجان_انگیز", "رازآلود", "فانتزی",
    "علمی_تخیلی", "انیمیشن", "مستند", "تاریخی", "ورزشی", "جنگی",
    "موزیکال", "موسیقی", "بیوگرافی", "خانوادگی", "ماجراجویی"
]

YEAR_RE = r"(19\d{2}|20\d{2})"
YEAR_RANGE_RE = r"(19\d{2}|20\d{2})(?:\s*[-–—]\s*(19\d{2}|20\d{2}))?"

PERSIAN_ORDINALS = {
    "اول": 1, "دوم": 2, "سوم": 3, "چهارم": 4, "پنجم": 5,
    "ششم": 6, "هفتم": 7, "هشتم": 8, "نهم": 9, "دهم": 10,
}
ORD_PATTERN = "|".join(PERSIAN_ORDINALS.keys())

# ترتیب نمایش کیفیت‌ها در پوستر: صعودی (480 -> 1080)، مطابق نمونه‌های کانال مشتری
DISPLAY_QUALITY_ORDER = ["480p", "540p", "720p", "1080p"]
QUALITY_ORDER = ["1080p", "720p", "540p", "480p"]  # برای مرتب‌سازی داخلی/ارسال ویدیوها


# ---------------------------------------------------------------------------
# استخراج اطلاعات از کپشن پوستر فورواردشده — همون منطق رباتِ روشن (bot.py)
# ---------------------------------------------------------------------------

def extract_name_fa(caption):
    summary_kw = re.search(r"خلاصه داستان|خلاصه", caption)
    header = caption[:summary_kw.start()] if summary_kw else caption

    tag_words = "|".join(re.escape(w.replace("_", " ")) for w in NOT_FILM_NAME)
    header = re.sub(r"\(\s*(?:" + tag_words + r")\s*\)", "", header)
    header = re.sub(r"#(?:" + "|".join(re.escape(w) for w in NOT_FILM_NAME) + r")\b", "", header)

    fa = re.search(r"«([^«»\n]+)»", header)
    if fa:
        return fa.group(1).strip()

    fa2 = re.search(r"\(([^\(\)\n]*[\u0600-\u06FF][^\(\)\n]+)\)", header)
    if fa2:
        return fa2.group(1).strip()

    m = re.search(
        r"(?:🎥|🎬|📹|📽️|🎞️?)?\s*(?:فیلم سینمایی|فیلم کوتاه|فیلم|سریال)\s*:?\s*([^\n]+)",
        header
    )
    if m:
        name = m.group(1).strip()
        name = re.split(r"[A-Za-z\d]|\||📝|🎬|📤|🎭|💯|📆|⏰|👤|👥|📖|🎙|🏅|🌎|🗯|⚡", name)[0]
        name = name.strip()
        name = name.strip("،,:»«\"'()（）")
        name = name.strip()
        if name and len(name) > 2:
            return name

    fa3 = re.findall(r"#([\u0600-\u06FF\u200c_]+)", header)
    fa3 = [f for f in fa3 if f not in NOT_FILM_NAME]
    if fa3:
        return fa3[0].replace("_", " ")

    return ""


def build_year(start, end):
    return f"{start}-{end}" if end else (start or "")


def extract_name_en_year(caption):
    m = re.search(
        r"([A-Za-z][A-Za-z0-9 :\-,.']{1,}?)[ \t]*\(\s*" + YEAR_RANGE_RE + r"\s*\)",
        caption
    )
    if m:
        return m.group(1).strip(), build_year(m.group(2), m.group(3))

    m = re.search(r"#([A-Za-z][A-Za-z0-9_]+)\s+" + YEAR_RE, caption)
    if m:
        return m.group(1).replace("_", " ").strip(), m.group(2)

    m = re.search(r"([A-Za-z][A-Za-z0-9\s:\-,.']{3,}?)\s+" + YEAR_RANGE_RE, caption)
    if m:
        return m.group(1).strip(), build_year(m.group(2), m.group(3))

    for line in caption.split("\n"):
        if "@" in line or "http" in line:
            continue
        if re.search(r"[\u0600-\u06FF]", line):
            continue
        lm = re.search(r"([A-Za-z][A-Za-z0-9\s:\-,.']{2,})", line)
        if lm and len(lm.group(1).strip()) >= 4:
            return lm.group(1).strip(), ""

    return "", ""


def extract_genres(caption):
    m = re.search(r"ژانر[^\n]*:\s*([^\n]+)", caption)
    if m:
        raw = m.group(1)
        tagged = re.findall(r"#([\u0600-\u06FF\u200c_]+)", raw)
        if tagged:
            return [t for t in tagged if t not in SKIP_TAGS]
        tokens = re.split(r"[|,،]+", raw)
        genres = []
        for t in tokens:
            t = t.strip().replace(" ", "_")
            if t and t not in SKIP_TAGS:
                genres.append(t)
        if genres:
            return genres
    all_tags = re.findall(r"#([\u0600-\u06FF\u200c_]+)", caption)
    return [g for g in all_tags if g not in SKIP_TAGS]


def extract_country(caption):
    m = re.search(r"(?:محصول کشور|کشور سازنده|محصول)\s*:?\s*#?([^\n]+)", caption)
    if m:
        c = m.group(1).strip()
        c = re.sub(r"[#@]", "", c)
        tokens = [t.strip() for t in re.split(r"[|,،]", c) if t.strip()]
        return " | ".join(tokens)
    return ""


def extract_imdb(caption):
    m = re.search(r"(?:IMDB|IMDb)[^\d]{0,10}(\d+\.?\d*)", caption)
    if m:
        return m.group(1)
    m = re.search(r"(\d+\.?\d*)\s*از\s*10\b", caption)
    if m:
        return m.group(1)
    m = re.search(r"(\d+\.?\d*)\s*/\s*10\b", caption)
    if m:
        return m.group(1)
    m = re.search(r"امتیاز[^\d]{0,10}(\d+\.?\d*)", caption)
    if m:
        return m.group(1)
    return ""


def extract_summary(caption):
    """خلاصه رو با یه regex ساده و اثبات‌شده (از رباتِ روشن/bot.py که هنوز باهاش
    کار می‌کنی) استخراج می‌کنه. نسخه‌ی قبلیِ این تابع (خط‌به‌خط) اضافه پیچیده
    شده بود و باگ داشت (نمی‌ایستاد سر خط امضای پایانی کانال). این نسخه از
    «خلاصه»/«خلاصه فیلم»/«خلاصه سریال» شروع می‌کنه و در اولین موردِ خط خالی،
    خط جداکننده، @/🆔، یا کلمات Season/Download/دانلود/... متوقف می‌شه.
    """
    if not caption:
        return ""
    WS = r"[\s\u200c]*"
    m = re.search(
        r'(?:خلاصه داستان|خلاصه فیلم|خلاصه سریال|خلاصه)[^\u0600-\u06FFA-Za-z0-9"«»]*([\s\S]+?)'
        r"(?:\n" + WS + r"\n"
        r"|\n" + WS + r"[➖\-_~〰]{2,}"
        r"|\n" + WS + r"[@🆔⌞]"
        r"|\n" + WS + r"(?:Season|Download|دانلود|⬇️|👈|🔻|💎|⚠️|✔️|✅|✂️|🚫|🔞)"
        r"|\n" + WS + r"E\d{1,2}[_\-]E\d{1,2}"
        r"|\Z)",
        caption
    )
    if m:
        # سقف ایمنی: حتی اگه هیچ‌کدوم از نشونه‌های بالا زودتر پیدا نشه (پستی با
        # ساختار کاملاً غیرمنتظره)، خلاصه بی‌نهایت ادامه پیدا نکنه.
        return m.group(1).strip()[:700]
    return ""

def extract_info(caption):
    info = {
        "name_fa": "", "name_en": "", "year": "",
        "imdb": "", "genres": [], "country": "",
        "summary": "", "seasons": "", "episodes": "",
        "is_dub": False
    }
    if not caption:
        return info

    info["name_fa"] = extract_name_fa(caption)

    fa_year = re.search(r"\s*" + YEAR_RE + r"\s*$", info["name_fa"])
    if fa_year:
        info["year"] = fa_year.group(1)
        info["name_fa"] = info["name_fa"][:fa_year.start()].strip()

    name_en, year_en = extract_name_en_year(caption)
    info["name_en"] = name_en
    if not info["year"]:
        info["year"] = year_en
    if not info["year"]:
        summary_kw = re.search(r"خلاصه داستان|خلاصه", caption)
        header = caption[:summary_kw.start()] if summary_kw else caption
        yr = re.search(r"\b" + YEAR_RE + r"\b", header)
        if yr:
            info["year"] = yr.group(1)

    info["imdb"] = extract_imdb(caption)
    info["genres"] = extract_genres(caption)
    info["country"] = extract_country(caption)
    info["summary"] = extract_summary(caption)
    info["is_dub"] = bool(re.search(r"دوبله", caption))

    status_line = re.search(
        r"(?:وضعیت|تعداد)\s*:?\s*(\d+)\s*فصل(?:\s*[|،,\-–—]?\s*(\d+)\s*قسمت)?",
        caption
    )
    if status_line:
        info["seasons"] = status_line.group(1) or ""
        info["episodes"] = status_line.group(2) or ""
    else:
        # کانال مشتری برعکس چیدمان بالاست: «تعداد فصل : 2» / «تعداد قسمت : 10»
        # (لیبل قبل از عدد میاد، نه بعدش) — این دو تا رو جدا چک می‌کنیم چون
        # قبلاً این حالت اصلاً match نمی‌شد و seasons/episodes همیشه خالی می‌موند.
        seasons_lbl = re.search(r"تعداد\s*فصل\s*:?\s*(\d+)", caption)
        episodes_lbl = re.search(r"تعداد\s*(?:قسمت|اپیزود)\s*:?\s*(\d+)", caption)
        if seasons_lbl:
            info["seasons"] = seasons_lbl.group(1)
        if episodes_lbl:
            info["episodes"] = episodes_lbl.group(1)

        if not info["seasons"]:
            seasons = re.search(r"(\d+)\s*فصل", caption)
            if seasons:
                info["seasons"] = seasons.group(1)
        if not info["episodes"]:
            episodes = re.search(r"(\d+)\s*قسمت", caption)
            if episodes:
                info["episodes"] = episodes.group(1)

    return info


def extract_season(caption):
    m = re.search(r"\b[Ss](\d{1,2})[Ee]\d{1,3}\b", caption)
    if m:
        return str(int(m.group(1)))
    m = re.search(r"\b[Ss](\d{1,2})\b", caption)
    if m:
        return str(int(m.group(1)))
    m = re.search(r"فصل\s*:?\s*(\d{1,2})\b", caption)
    if m:
        return str(int(m.group(1)))
    m = re.search(r"فصل\s*(" + ORD_PATTERN + r")(?=\s|$|[^\u0600-\u06FF])", caption)
    if m:
        return str(PERSIAN_ORDINALS[m.group(1)])
    return "1"


def extract_episode(caption):
    m = re.search(r"\b[Ss]\d{1,2}[Ee](\d{1,3})\b", caption)
    if m:
        return str(int(m.group(1)))
    m = re.search(r"قسمت\s*:?\s*(\d{1,3})\b", caption)
    if m:
        return str(int(m.group(1)))
    m = re.search(r"قسمت\s*(" + ORD_PATTERN + r")(?=\s|$|[^\u0600-\u06FF])", caption)
    if m:
        return str(PERSIAN_ORDINALS[m.group(1)])
    m = re.search(r"\b[Ee][Pp]?\.?\s*(\d{1,3})\b", caption)
    if m:
        return str(int(m.group(1)))
    return ""


def get_quality(caption):
    if not caption:
        return ""
    if "1080" in caption:
        return "1080p"
    elif "720" in caption:
        return "720p"
    elif "540" in caption:
        return "540p"
    elif "480" in caption:
        return "480p"
    return ""


def present_qualities(videos):
    present = {v["quality"] for v in videos if v.get("quality")}
    return [q for q in DISPLAY_QUALITY_ORDER if q in present]


def quality_display(videos):
    """کیفیت‌ها به ترتیب صعودی، جدا شده با ' - '. اگه هیچی نبود، رشته‌ی خالی."""
    quals = present_qualities(videos)
    return " - ".join(quals)


def name_en_with_year(info):
    # قالب مشتری سال رو داخل پرانتز می‌ذاره: «Number One (2026)»، «A shop for Killers (2024-2026)»
    if info["name_en"] and info["year"]:
        return f'{info["name_en"]} ({info["year"]})'
    return info["name_en"] or (f'({info["year"]})' if info["year"] else "")


def esc(text):
    """کپشن‌های پوستر با parse_mode='HTML' فرستاده می‌شن (به خاطر tg-emoji)، پس
    هر متن آزادی که از کپشن اصلی میاد (اسم فیلم و ...) باید escape بشه."""
    return html.escape(text or "")


# ---------------------------------------------------------------------------
# ایموجی‌های پرمیوم — فقط توی کپشن «پوستر» استفاده می‌شن (تیزر/ویدیو/قسمت‌ها
# پرمیوم ندارن، طبق تأیید مشتری). دو دیکشنری جدا برای فیلم و سریال چون خیلی
# از آیدی‌ها بین این دو فرق دارن.
#
# نکته‌ی مهم که مطمئن نیستم و باید تست بشه:
# دو تا آیدی «انتهای کانال» (5296467787261175784) و «ایموجی بعد از @NetMovis»
# (5471978009449731768) با هم فرق دارن ولی مشخص نبود دقیقاً هرکدوم مال کدوم
# گلیف/موقعیتن. فعلاً فرض کردم 5296467787261175784 همون 🪽 انتهای فوتر فیلمه؛
# آیدی دوم (5471978009449731768) رو استفاده نکردم. اگه اشتباه بود، با
# /emojiid روی یه پست واقعی مشتری چک کن و PREMIUM_EMOJI_FILM["پایان_کانال"]
# رو اصلاح کن.
# ---------------------------------------------------------------------------

PREMIUM_EMOJI_FILM = {
    "فیلم": ("📹", "6007932130615628766"),
    "عنوان_انگلیسی": ("🍿", "5408850391154569842"),
    "محصول": ("📍", "5391032818111363540"),
    "امتیاز": ("📱", "5346242859039209592"),
    "ژانر": ("🎭", "5359441070201513074"),
    "کیفیت": ("👁", "5032776298733240935"),
    "زیرنویس": ("✏️", "5395444784611480792"),
    "خلاصه": ("📝", "5956237238521040714"),
    "نسخه_زیرنویس": ("💬", "5443038326535759644"),
    # فلش پایین بعد از «نسخه زیرنویس فارسی» — قبلاً اصلاً نداشتیمش
    "نسخه_زیرنویس_فلش": ("👇", "5470177992950946662"),
    "دانلود": ("📥", "5433811242135331842"),
    "فلش": ("👉", "5471978009449731768"),
    "پایان_کانال": ("🪽", "5296467787261175784"),
}

PREMIUM_EMOJI_SERIAL = {
    "سریال": ("📹", "5936178658916964051"),
    "عنوان_انگلیسی": ("🍿", "5408850391154569842"),
    "محصول": ("📍", "5391032818111363540"),
    "امتیاز": ("📱", "5346242859039209592"),
    "ژانر": ("🎭", "5359441070201513074"),
    "کیفیت": ("👁", "5032776298733240935"),
    "زیرنویس": ("📝", "5003700551255328333"),
    "تعداد_فصل": ("⭐️", "5438496463044752972"),
    "خلاصه": ("📝", "5956237238521040714"),
    "دانلود": ("⬇️", "4965313018326942268"),
    "پایان_کانال": ("🪽", "5296467787261175784"),
}



def tg(mapping, key):
    char, emoji_id = mapping.get(key, (None, None))
    if char and emoji_id:
        return f'<tg-emoji emoji-id="{emoji_id}">{char}</tg-emoji>'
    return char or ""


def strip_tg_emoji_tags(text):
    """تگ‌های <tg-emoji> را به ایموجی معمولی تبدیل می‌کند.

    اگر یکی از custom_emoji_idها اشتباه/منقضی باشد، تلگرام کل پیام را با
    Bad Request رد می‌کند. این تابع اجازه می‌دهد همان پست حداقل با ایموجی
    معمولی ارسال شود و ربات وسط کار نخوابد.
    """
    pattern = re.compile(
        r'<tg-emoji\s+emoji-id="([0-9]+)">(.*?)</tg-emoji>',
        re.DOTALL,
    )
    without_tags = pattern.sub(lambda m: html.unescape(m.group(2)), text or "")
    return html.unescape(without_tags)


def html_custom_emoji_to_entities(text):
    """تبدیل HTML تگ‌های tg-emoji به متن ساده + MessageEntity.

    این مسیر مخصوص کپشن سریال است تا Telegram مجبور نباشد HTML کپشن سریال
    را parse کند. خود Custom Emoji به‌صورت entity استاندارد Bot API فرستاده
    می‌شود؛ این کار با متن‌های طولانی/ترکیبی و چند ایموجی سفارشی مطمئن‌تر است.
    """
    pattern = re.compile(
        r'<tg-emoji\s+emoji-id="([0-9]+)">(.*?)</tg-emoji>',
        re.DOTALL,
    )
    parts = []
    entities = []
    plain_offset = 0  # UTF-16 code units, as required by Telegram
    cursor = 0

    for match in pattern.finditer(text):
        before = html.unescape(text[cursor:match.start()])
        parts.append(before)
        plain_offset += len(before.encode("utf-16-le")) // 2

        emoji = html.unescape(match.group(2))
        start = plain_offset
        parts.append(emoji)
        emoji_len = len(emoji.encode("utf-16-le")) // 2
        entities.append(
            MessageEntity(
                type="custom_emoji",
                offset=start,
                length=emoji_len,
                custom_emoji_id=match.group(1),
            )
        )
        plain_offset += emoji_len
        cursor = match.end()

    tail = html.unescape(text[cursor:])
    parts.append(tail)
    plain_text = "".join(parts)
    return plain_text, entities


def poster_caption_payload(mode, info, videos=None):
    """کپشن پوستر را به متن ساده + custom emoji entities تبدیل می‌کند.

    نکته مهم: این تابع خلاصه را کوتاه نمی‌کند. اگر طول کپشن از سقف ۱۰۲۴ کپشن
    تلگرام رد شود، موقع ارسال، عکس بدون کپشن می‌رود و متن کامل با ایموجی‌های
    پرمیوم در پیام جداگانه زیر عکس ارسال می‌شود.
    """
    html_caption = make_serial_caption(info, videos) if mode == "serial" else make_film_caption(info, videos)
    return html_custom_emoji_to_entities(html_caption)


def serial_caption_payload(info, videos=None):
    # برای سازگاری با نسخه‌های قبلی کد؛ از این به بعد کوتاه‌سازی انجام نمی‌شود.
    return poster_caption_payload("serial", info, videos)


PLAYER_NOTE = "✍🏼 از برنامه Mx player - Km player استفاده کنین"


# ---------------------------------------------------------------------------
# کپشن پوستر فیلم / سریال (پرمیوم)
# ---------------------------------------------------------------------------

def make_film_caption(info, videos=None):
    videos = videos or []
    E = PREMIUM_EMOJI_FILM
    genres_str = " ".join("#" + g for g in info["genres"]) if info["genres"] else ""
    sub_tag = "دوبله_فارسی" if info.get("is_dub") else "زیرنویس_فارسی"
    version_line = (
        f'🗯 نسخه دوبله فارسی{tg(E, "نسخه_زیرنویس_فلش")}' if info.get("is_dub")
        else f'{tg(E, "نسخه_زیرنویس")} نسخه زیرنویس فارسی{tg(E, "نسخه_زیرنویس_فلش")}'
    )
    return (
        f'{tg(E, "فیلم")} فیلم : « {esc(info["name_fa"])} »\n'
        f'{tg(E, "عنوان_انگلیسی")} {esc(name_en_with_year(info))}\n\n'
        f'{tg(E, "محصول")}┇محصول : {esc(info["country"])}\n'
        f'{tg(E, "امتیاز")}┇امتیاز : {info["imdb"]}\n'
        f'{tg(E, "ژانر")}┇ژانر : {genres_str}\n'
        f'{tg(E, "کیفیت")}┇کیفیت : {quality_display(videos)}\n'
        f'{tg(E, "زیرنویس")}┇#{sub_tag}\n'
        f'{tg(E, "خلاصه")}┇خلاصه فیلم : {esc(info["summary"])}\n\n'
        f'{version_line}\n'
        f'{tg(E, "دانلود")} [ جهت دانلود اینجا کلیک کنید ] {tg(E, "فلش")}\n\n'
        f'⌞ {CHANNEL} {tg(E, "پایان_کانال")}⌝'
    )


def build_download_lines(videos, seasons_count=""):
    """برای سریال فقط یک خط برای هر فصل بساز؛ شماره قسمت‌ها عمداً نمایش داده نمی‌شوند."""
    seasons = set()
    for v in videos or []:
        try:
            seasons.add(int(v.get("season") or 1))
        except (TypeError, ValueError):
            seasons.add(1)

    # اگر تعداد فصل در کپشن ورودی مشخص است، همه فصل‌های اعلام‌شده را نشان بده.
    try:
        count = int(seasons_count or 0)
    except (TypeError, ValueError):
        count = 0
    if count > 0:
        seasons.update(range(1, count + 1))
    if not seasons:
        seasons.add(1)

    E = PREMIUM_EMOJI_SERIAL
    dl = tg(E, "دانلود")
    sep = tg(E, "جداکننده_فصل")
    season_text = '\n'.join(f'{dl}Download: season{s}' for s in sorted(seasons))
    return season_text

def make_serial_caption(info, videos=None):
    videos = videos or []
    E = PREMIUM_EMOJI_SERIAL
    genres_str = " ".join("#" + g for g in info["genres"]) if info["genres"] else ""
    sub_tag = "دوبله_فارسی" if info.get("is_dub") else "زیرنویس_فارسی"
    seasons_count = info["seasons"] if info["seasons"] else ""
    dl_lines = build_download_lines(videos, info.get("seasons", ""))

    return (
        f'{tg(E, "سریال")} سریال : « {esc(info["name_fa"])} »\n'
        f'{tg(E, "عنوان_انگلیسی")} {esc(name_en_with_year(info))}\n\n'
        f'{tg(E, "محصول")}┇محصول : {esc(info["country"])}\n'
        f'{tg(E, "امتیاز")}┇امتیاز : {info["imdb"]}\n'
        f'{tg(E, "ژانر")}┇ژانر : {genres_str}\n'
        f'{tg(E, "کیفیت")}┇کیفیت : {quality_display(videos)}\n'
        f'{tg(E, "زیرنویس")}┇#{sub_tag}\n'
        f'{tg(E, "تعداد_فصل")}┇تعداد فصل : {seasons_count}\n'
        f'{tg(E, "خلاصه")}┇خلاصه سریال : {esc(info["summary"])}\n\n'
        f'{dl_lines}\n\n'
        f'⌞ {CHANNEL} {tg(E, "پایان_کانال")}⌝'
    )


# تلگرام کپشن عکس/ویدیو را حداکثر ۱۰۲۴ کاراکتر/واحد UTF-16 قبول می‌کند.
# برای اینکه خلاصه کامل بماند، دیگر خلاصه را کوتاه نمی‌کنیم؛ اگر متن کامل از
# این سقف رد شود، پوستر بدون کپشن ارسال می‌شود و کپشن کامل با ایموجی‌های
# پرمیوم در پیام جداگانه زیر پوستر می‌آید.
CAPTION_LIMIT = 1024
CAPTION_SAFETY_MARGIN = 40  # فقط برای تابع قدیمیِ fit_caption_to_limit نگه داشته شده؛ دیگر استفاده نمی‌شود.


def _utf16_len(text):
    return len(text.encode("utf-16-le")) // 2


def fit_caption_to_limit(build_fn, info, videos, max_len=CAPTION_LIMIT - CAPTION_SAFETY_MARGIN):
    if _utf16_len(build_fn(info, videos)) <= max_len:
        return info

    info = dict(info)
    summary = info.get("summary", "")
    while summary:
        summary = summary[:-40].rstrip() if len(summary) > 40 else ""
        info["summary"] = (summary + "…") if summary else ""
        if _utf16_len(build_fn(info, videos)) <= max_len:
            return info
    return info


def build_caption(mode, info, videos):
    """کپشن کامل را می‌سازد؛ خلاصه را کوتاه نمی‌کند."""
    if mode == "film":
        return make_film_caption(info, videos)
    return make_serial_caption(info, videos)


# ---------------------------------------------------------------------------
# کپشن‌های تیزر / ویدیو / قسمت — بدون پرمیوم، ایموجی معمولی
# ---------------------------------------------------------------------------

def make_video_caption(info, quality):
    return (
        f'📽 دانلود فیلم " {info["name_fa"]} "\n'
        f'📥 کیفیت {quality} - زیرنویس چسبیده فارسی \n'
        f'{PLAYER_NOTE}\n\n'
        f'⌞ {CHANNEL} ⌝'
    )


def make_dub_video_caption(info, quality):
    return (
        f'📽 دانلود فیلم " {info["name_fa"]} "\n'
        f'📥 کیفیت {quality} - دوبله فارسی \n'
        f'{PLAYER_NOTE}\n\n'
        f'⌞ {CHANNEL} ⌝'
    )


def make_serial_episode_caption(info, quality, season, episode):
    return (
        f'🎥 دانلود سریال" {info["name_fa"]} "\n'
        f'📥 کیفیت {quality} - زیرنویس فارسی \n'
        f'🎞 فصل {season} - قسمت {episode}\n'
        f'{PLAYER_NOTE}\n\n'
        f'⌞ {CHANNEL} ⌝'
    )


def make_dub_serial_episode_caption(info, quality, season, episode):
    return (
        f'🎥 دانلود سریال" {info["name_fa"]} "\n'
        f'📥 کیفیت {quality} - دوبله فارسی \n'
        f'🎞 فصل {season} - قسمت {episode}\n'
        f'{PLAYER_NOTE}\n\n'
        f'⌞ {CHANNEL} ⌝'
    )


def make_trailer_caption(info, mode="film"):
    parts = [p for p in [info["name_fa"], info["year"]] if p]
    name = " ".join(parts)
    label = "تریلر سریال" if mode == "serial" else "تریلر فیلم"
    return f'🎥 {label} " {name} "\n\n⌞ {CHANNEL} ⌝'


# ---------------------------------------------------------------------------
# تشخیص زیرنویس/دوبله نهایی — اول از خود کپشن پوستر (برای پیش‌نمایش اولیه)،
# بعد موقع /d بر اساس اکثریت ویدیوهای واقعاً دریافتی بازبینی می‌شه.
# ---------------------------------------------------------------------------

def resolve_is_dub(info, videos):
    if not videos:
        return info.get("is_dub", False)
    dub_count = sum(1 for v in videos if v.get("is_dub"))
    sub_count = len(videos) - dub_count
    return dub_count > sub_count


# ---------------------------------------------------------------------------
# state / handlers
# ---------------------------------------------------------------------------

def mode_keyboard():
    return InlineKeyboardMarkup([
        [
            InlineKeyboardButton("🎬 فیلم", callback_data="mode_film"),
            InlineKeyboardButton("📺 سریال", callback_data="mode_serial"),
        ]
    ])


def cancel_keyboard():
    return InlineKeyboardMarkup([
        [InlineKeyboardButton("❌ لغو", callback_data="cancel_all")],
    ])


def reset_user(uid):
    user_data[uid] = {"mode": None, "videos": [], "trailer": None, "poster": None,
                       "poster_caption": "", "pending_info": None}


def start(bot, update):
    uid = update.message.from_user.id
    reset_user(uid)
    update.message.reply_text("فیلمه یا سریال؟", reply_markup=mode_keyboard())


def film_mode(bot, update):
    uid = update.message.from_user.id
    reset_user(uid)
    user_data[uid]["mode"] = "film"
    update.message.reply_text("حالت فیلم\n\nپوستر آماده رو فوروارد کن")


def serial_mode(bot, update):
    uid = update.message.from_user.id
    reset_user(uid)
    user_data[uid]["mode"] = "serial"
    update.message.reply_text("حالت سریال\n\nپوستر آماده رو فوروارد کن")


def cancel_cmd(bot, update):
    uid = update.message.from_user.id
    reset_user(uid)
    update.message.reply_text("لغو شد. برای شروع دوباره /start بزن.")


def status_cmd(bot, update):
    uid = update.message.from_user.id
    data = user_data.get(uid)
    if not data or not data.get("mode"):
        update.message.reply_text("هنوز چیزی شروع نکردی. /start بزن.")
        return
    mode_fa = "فیلم" if data["mode"] == "film" else "سریال"
    n_videos = len(data.get("videos", []))
    poster_state = "✅ گرفته شده" if data.get("poster") else "❌ نگرفتم"
    trailer_state = "✅ گرفته شده" if data.get("trailer") else "❌ نگرفتم"
    update.message.reply_text(
        f"حالت: {mode_fa}\nپوستر: {poster_state}\nتیزر: {trailer_state}\nویدیو/فایل دریافتی: {n_videos}"
    )


def name_cmd(bot, update):
    """دستور /name — اصلاح دستیِ اسم استخراج‌شده، بدون نیاز به /cancel و فرستادن
    دوباره‌ی پوستر. فرمت: /name اسم فارسی   یا   /name اسم فارسی | English Name"""
    uid = update.message.from_user.id
    data = user_data.get(uid)
    if not data or not data.get("pending_info"):
        update.message.reply_text("هنوز پوستری پردازش نشده که اسمش رو اصلاح کنی. اول پوستر رو بفرست.")
        return

    text = update.message.text.partition(" ")[2].strip()
    if not text:
        update.message.reply_text("فرمت درست:\n/name اسم فارسی درست\nیا\n/name اسم فارسی | English Name")
        return

    parts = [p.strip() for p in text.split("|")]
    fa_name = parts[0]
    en_name = parts[1] if len(parts) > 1 else data["pending_info"].get("name_en", "")

    if not fa_name:
        update.message.reply_text("اسم فارسی نمی‌تونه خالی باشه.")
        return

    data["pending_info"]["name_fa"] = fa_name
    data["pending_info"]["name_en"] = en_name

    update.message.reply_text("اسم اصلاح شد ✅ — از الان روی همه‌ی کپشن‌ها همین اسم میاد.")
    if data.get("poster"):
        send_preview(bot, update.message.chat_id, uid)


def utf16_slice(text, offset, length):
    """آفست/طولی که تلگرام می‌ده بر اساس واحدهای UTF-16 هست، نه ایندکس معمولی
    پایتون؛ برای ایموجی‌هایی که خارج از BMP هستن (اکثر ایموجی‌های امروزی)
    این دوتا فرق دارن. بدون این تبدیل، ممکنه کاراکتر اشتباهی نمایش داده بشه."""
    encoded = text.encode("utf-16-le")
    chunk = encoded[offset * 2: (offset + length) * 2]
    return chunk.decode("utf-16-le", errors="ignore")


def emojiid_cmd(bot, update):
    """دستور /emojiid — ریپلای روی پیامی که ایموجی پرمیوم واقعی توشه، آیدی
    واقعی‌شو برمی‌گردونه. برای گرفتن آیدی‌های باقی‌مونده یا چک کردن فرضیات کد."""
    msg = update.message
    target = msg.reply_to_message
    if not target:
        update.message.reply_text(
            "این دستور رو روی پیامی ریپلای کن که ایموجی پرمیوم (که خودت با اکانت "
            "پرمیوم فرستادی) توش باشه."
        )
        return

    entities = list(target.entities or []) + list(target.caption_entities or [])
    custom = [e for e in entities if getattr(e, "type", None) == "custom_emoji"]
    if not custom:
        update.message.reply_text(
            "توی اون پیام هیچ ایموجی پرمیومی پیدا نشد (باید واقعاً custom emoji پرمیوم باشه)."
        )
        return

    src_text = target.text or target.caption or ""
    lines = []
    for e in custom:
        char = utf16_slice(src_text, e.offset, e.length)
        eid = getattr(e, "custom_emoji_id", None) or "❓ (نگرفت — لطفاً به من بگو تا چک کنم)"
        lines.append(f"{char}  →  {eid}")

    update.message.reply_text("آیدی‌های پیدا شده:\n" + "\n".join(lines))


def build_and_show_result(bot, chat_id, uid):
    data = user_data[uid]
    caption = data.get("poster_caption", "")
    if not caption.strip():
        bot.send_message(chat_id=chat_id, text="⚠️ پوستر دریافت شد ولی کپشن خالیه! نتیجه ممکنه ناقص باشه.")
    info = extract_info(caption)
    data["pending_info"] = info
    send_preview(bot, chat_id, uid)
    bot.send_message(chat_id=chat_id, text="حالا تیزر و ویدیو/فایل‌ها رو بفرست، آخرش دستور /d رو بزن.")


def receive_message(bot, update):
    uid = update.message.from_user.id
    data = user_data.get(uid)
    msg = update.message

    if (not data or not data.get("mode")) and msg.photo:
        if not data:
            reset_user(uid)
            data = user_data[uid]
        data["poster"] = msg.photo[-1].file_id
        data["poster_caption"] = msg.caption or ""
        update.message.reply_text("فیلمه یا سریال؟", reply_markup=mode_keyboard())
        return

    if not data or not data.get("mode"):
        update.message.reply_text("فیلمه یا سریال؟", reply_markup=mode_keyboard())
        return

    if msg.photo:
        data["poster"] = msg.photo[-1].file_id
        data["poster_caption"] = msg.caption or ""
        build_and_show_result(bot, update.message.chat_id, uid)

    elif msg.video:
        caption = msg.caption or ""
        quality = get_quality(caption)
        if any(x in caption for x in ["تیزر", "تریلر", "teaser", "trailer", "Trailer", "Teaser"]):
            data["trailer"] = msg.video.file_id
            update.message.reply_text("تیزر دریافت شد!")
        elif data.get("mode") == "serial":
            season = extract_season(caption)
            episode = extract_episode(caption)
            is_dub = bool(re.search(r"دوبله", caption))
            data["videos"].append({
                "file_id": msg.video.file_id, "quality": quality,
                "is_video": True, "season": season, "episode": episode, "is_dub": is_dub
            })
            update.message.reply_text(
                ("دوبله " if is_dub else "") + f"فصل {season} قسمت {episode} ({quality}) دریافت شد!"
            )
        else:
            is_dub = bool(re.search(r"دوبله", caption))
            data["videos"].append({
                "file_id": msg.video.file_id, "quality": quality, "is_video": True, "is_dub": is_dub
            })
            update.message.reply_text(("دوبله " if is_dub else "") + "ویدیو " + quality + " دریافت شد!")

    elif msg.document:
        caption = msg.caption or ""
        quality = get_quality(caption)
        if data.get("mode") == "serial":
            season = extract_season(caption)
            episode = extract_episode(caption)
            is_dub = bool(re.search(r"دوبله", caption))
            data["videos"].append({
                "file_id": msg.document.file_id, "quality": quality,
                "is_video": False, "season": season, "episode": episode, "is_dub": is_dub
            })
            update.message.reply_text(
                ("دوبله " if is_dub else "") + f"فصل {season} قسمت {episode} ({quality}) دریافت شد!"
            )
        else:
            is_dub = bool(re.search(r"دوبله", caption))
            data["videos"].append({
                "file_id": msg.document.file_id, "quality": quality, "is_video": False, "is_dub": is_dub
            })
            update.message.reply_text(("دوبله " if is_dub else "") + "فایل " + quality + " دریافت شد!")


# ---------------------------------------------------------------------------
# ارسال امن — تلگرام کپشن عکس/ویدیو/فایل رو حداکثر ۱۰۲۴ کاراکتر قبول می‌کنه؛
# کپشن پوستر سریال (خلاصه + خط‌های Download) خیلی وقت‌ها از این رد می‌شه و
# قبلاً باعث می‌شد ارسال کلاً ساکت fail کنه و هیچی نره. الان اگه کپشن پس زده
# بشه، عکس/ویدیو/فایل رو بدون کپشن می‌فرستیم و متن کامل رو جدا پیام می‌کنیم؛
# و اگه هر خطای دیگه‌ای هم بیفته، به‌جای سکوت، به خود کاربر گزارش می‌شه.
# ---------------------------------------------------------------------------

def safe_send_video(bot, chat_id, video, caption):
    try:
        return bot.send_video(chat_id=chat_id, video=video, caption=caption)
    except TelegramError as e:
        logger.exception("send_video fail شد")
        try:
            msg = bot.send_video(chat_id=chat_id, video=video)
        except TelegramError as e2:
            bot.send_message(chat_id=chat_id, text=f"⚠️ ارسال ویدیو با خطا مواجه شد:\n{e2}")
            return None
        bot.send_message(chat_id=chat_id, text=caption)
        return msg


def safe_send_document(bot, chat_id, document, caption):
    try:
        return bot.send_document(chat_id=chat_id, document=document, caption=caption)
    except TelegramError as e:
        logger.exception("send_document fail شد")
        try:
            msg = bot.send_document(chat_id=chat_id, document=document)
        except TelegramError as e2:
            bot.send_message(chat_id=chat_id, text=f"⚠️ ارسال فایل با خطا مواجه شد:\n{e2}")
            return None
        bot.send_message(chat_id=chat_id, text=caption)
        return msg



def _entity_to_dict(entity):
    """MessageEntity را به dict استاندارد Bot API تبدیل می‌کند."""
    data = {
        "type": entity.type,
        "offset": int(entity.offset),
        "length": int(entity.length),
    }
    custom_emoji_id = getattr(entity, "custom_emoji_id", None)
    if custom_emoji_id:
        data["custom_emoji_id"] = str(custom_emoji_id)
    return data


def _shift_entities_for_chunk(text, entities, start_char, end_char):
    """entityهای داخل یک تکه متن را با offset جدید برمی‌گرداند."""
    start_utf16 = _utf16_len(text[:start_char])
    end_utf16 = _utf16_len(text[:end_char])
    shifted = []
    for entity in entities or []:
        e_start = int(entity.offset)
        e_end = e_start + int(entity.length)
        if start_utf16 <= e_start and e_end <= end_utf16:
            shifted.append({
                "type": entity.type,
                "offset": e_start - start_utf16,
                "length": int(entity.length),
                **({"custom_emoji_id": str(getattr(entity, "custom_emoji_id"))}
                   if getattr(entity, "custom_emoji_id", None) else {}),
            })
    return shifted


def _telegram_payload_reply_markup(reply_markup):
    if reply_markup is None:
        return None
    try:
        return reply_markup.to_dict()
    except Exception:
        return None


def _raw_telegram_call(method, payload):
    """فراخوانی مستقیم Bot API با JSON.

    در python-telegram-bot 12.x فیلد custom_emoji_id موقع serialize شدن حذف می‌شود؛
    برای همین پوسترهای دارای ایموجی پرمیوم را مستقیم با Bot API می‌فرستیم.
    JSON هم از form-urlencoded مطمئن‌تر است و خطای واقعی تلگرام را برمی‌گرداند.
    """
    url = f"https://api.telegram.org/bot{TOKEN}/{method}"
    body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
    req = urllib.request.Request(
        url,
        data=body,
        method="POST",
        headers={"Content-Type": "application/json"},
    )
    try:
        with urllib.request.urlopen(req, timeout=45) as response:
            raw = response.read().decode("utf-8")
    except urllib.error.HTTPError as e:
        raw_error = e.read().decode("utf-8", errors="replace")
        try:
            err_json = json.loads(raw_error)
            description = err_json.get("description", raw_error)
        except Exception:
            description = raw_error or str(e)
        raise RuntimeError(f"Telegram API {method} failed: {description}") from e

    data = json.loads(raw)
    if not data.get("ok"):
        raise RuntimeError(data.get("description", raw))
    return data.get("result")


def send_entity_text_raw(bot, chat_id, text, entities):
    """متن کامل را با custom emoji می‌فرستد؛ برای بیشتر از ۴۰۹۶ تکه‌تکه می‌کند."""
    max_len = 4096
    if _utf16_len(text) <= max_len:
        return _raw_telegram_call("sendMessage", {
            "chat_id": chat_id,
            "text": text,
            "entities": [_entity_to_dict(e) for e in (entities or [])],
            "disable_web_page_preview": True,
        })

    sent = None
    start_char = 0
    current = ""
    cursor = 0
    for line in text.splitlines(True):
        if current and _utf16_len(current + line) > max_len:
            end_char = cursor
            sent = _raw_telegram_call("sendMessage", {
                "chat_id": chat_id,
                "text": current,
                "entities": _shift_entities_for_chunk(text, entities, start_char, end_char),
                "disable_web_page_preview": True,
            })
            start_char = cursor
            current = line
        else:
            current += line
        cursor += len(line)

    if current:
        sent = _raw_telegram_call("sendMessage", {
            "chat_id": chat_id,
            "text": current,
            "entities": _shift_entities_for_chunk(text, entities, start_char, len(text)),
            "disable_web_page_preview": True,
        })
    return sent


def send_plain_full_text(bot, chat_id, text):
    """fallback نهایی برای متن‌های بلند بدون entity."""
    max_len = 4096
    if _utf16_len(text) <= max_len:
        return bot.send_message(chat_id=chat_id, text=text)
    sent = None
    current = ""
    for line in text.splitlines(True):
        if current and _utf16_len(current + line) > max_len:
            sent = bot.send_message(chat_id=chat_id, text=current)
            current = line
        else:
            current += line
    if current:
        sent = bot.send_message(chat_id=chat_id, text=current)
    return sent



def _raw_telegram_call_form(method, payload):
    """ارسال فرم برای Bot API؛ برای caption_entities قدیمی PTB مناسب‌تر است."""
    url = f"https://api.telegram.org/bot{TOKEN}/{method}"
    body = urllib.parse.urlencode(payload).encode("utf-8")
    req = urllib.request.Request(
        url, data=body, method="POST",
        headers={"Content-Type": "application/x-www-form-urlencoded"},
    )
    with urllib.request.urlopen(req, timeout=45) as response:
        data = json.loads(response.read().decode("utf-8"))
    if not data.get("ok"):
        raise RuntimeError(data.get("description", data))
    return data.get("result")


def _premium_entities_payload(entities):
    out=[]
    for e in entities:
        item={
            "type":"custom_emoji",
            "offset":int(e.offset),
            "length":int(e.length),
            "custom_emoji_id":str(getattr(e,"custom_emoji_id","")),
        }
        if item["custom_emoji_id"]:
            out.append(item)
    return out


def send_poster_with_premium(bot, chat_id, photo, mode, info, videos, reply_markup=None):
    """ارسال پوستر با ایموجی پرمیوم؛ در صورت خطا کل پست را خراب نمی‌کند.

    فیلم و سریال هر دو از یک مسیر واحد و مطمئن رد می‌شن: تگ‌های <tg-emoji> به
    entity خام Bot API تبدیل می‌شن (چون python-telegram-bot 12.x فیلد
    custom_emoji_id رو موقع سریالایز کردن HTML گم می‌کنه) و مستقیم با JSON
    فرستاده می‌شن. اگه کپشن از سقف ۱۰۲۴ رد بشه، پوستر بدون کپشن می‌ره و متن
    کامل با همون ایموجی‌های پرمیوم جدا فرستاده می‌شه — قبلاً این فقط برای
    سریال بود و فیلم اگه رد می‌شد کلاً کپشن رو از دست می‌داد.
    """
    html_caption = build_caption(mode, info, videos)

    try:
        caption, entities = html_custom_emoji_to_entities(html_caption)
        return send_serial_photo_raw(bot, chat_id, photo, caption, entities, reply_markup)
    except Exception:
        logger.exception("ارسال با entity خام fail شد؛ افتادن روی ایموجی معمولی")

    # فالبک نهایی: اگه حتی مسیر raw هم (مثلاً به‌خاطر قطعی شبکه) کار نکرد،
    # با ایموجی معمولی (بدون پرمیوم) امتحان کن که حداقل پست کامل بره.
    plain = strip_tg_emoji_tags(html_caption)
    try:
        return bot.send_photo(
            chat_id=chat_id,
            photo=photo,
            caption=plain if _utf16_len(plain) <= CAPTION_LIMIT else None,
            reply_markup=reply_markup,
        )
    except Exception as e:
        try:
            bot.send_message(chat_id=chat_id, text=f"خطای ارسال پوستر: {e}")
        except Exception:
            pass
        return None


def send_serial_photo_raw(bot, chat_id, photo, caption, entities, reply_markup=None):
    """سازگاری با نام تابع قدیمی."""
    markup = _telegram_payload_reply_markup(reply_markup)
    if _utf16_len(caption) <= CAPTION_LIMIT:
        payload = {
            "chat_id": chat_id,
            "photo": str(photo),
            "caption": caption,
            "caption_entities": [_entity_to_dict(e) for e in entities],
        }
        if markup:
            payload["reply_markup"] = markup
        return _raw_telegram_call("sendPhoto", payload)
    payload = {"chat_id": chat_id, "photo": str(photo)}
    if markup:
        payload["reply_markup"] = markup
    result = _raw_telegram_call("sendPhoto", payload)
    send_entity_text_raw(bot, chat_id, caption, entities)
    return result

def send_preview(bot, chat_id, uid):
    data = user_data[uid]
    info = dict(data["pending_info"])
    videos = data.get("videos", [])
    info["is_dub"] = resolve_is_dub(info, videos)

    send_poster_with_premium(
        bot, chat_id, data["poster"], data["mode"], info, videos,
        reply_markup=cancel_keyboard(),
    )


def send_final(bot, chat_id, uid):
    data = user_data[uid]
    mode = data["mode"]
    info = dict(data["pending_info"])
    videos = data.get("videos", [])
    info["is_dub"] = resolve_is_dub(info, videos)

    send_poster_with_premium(bot, chat_id, data["poster"], mode, info, videos)

    if data.get("trailer"):
        safe_send_video(bot, chat_id, data["trailer"], make_trailer_caption(info, mode))

    if mode == "serial":
        def sort_key(v):
            q_idx = QUALITY_ORDER.index(v["quality"]) if v["quality"] in QUALITY_ORDER else len(QUALITY_ORDER)
            season_num = int(v["season"]) if v["season"] else 0
            episode_num = int(v["episode"]) if v["episode"] else 0
            return (season_num, q_idx, episode_num)
        videos = sorted(videos, key=sort_key)

    last_group = None
    for v in videos:
        if mode == "serial":
            group = (v.get("season") or "1", v.get("quality") or "")
            if group != last_group:
                bot.send_message(chat_id=chat_id, text=f"📺 فصل {group[0]} - 💎 کیفیت {group[1]}")
                last_group = group
            if v.get("is_dub"):
                video_cap = make_dub_serial_episode_caption(info, v["quality"], v["season"], v["episode"])
            else:
                video_cap = make_serial_episode_caption(info, v["quality"], v["season"], v["episode"])
        elif v.get("is_dub"):
            video_cap = make_dub_video_caption(info, v["quality"])
        else:
            video_cap = make_video_caption(info, v["quality"])

        if v["is_video"]:
            safe_send_video(bot, chat_id, v["file_id"], video_cap)
        else:
            safe_send_document(bot, chat_id, v["file_id"], video_cap)

    reset_user(uid)


def d_cmd(bot, update):
    uid = update.message.from_user.id
    data = user_data.get(uid)
    if not data or not data.get("mode"):
        update.message.reply_text("اول /start بزن")
        return
    if not data.get("poster") or not data.get("pending_info"):
        update.message.reply_text("هنوز پوستر نفرستادی.")
        return
    if not data.get("videos"):
        update.message.reply_text("هنوز هیچ ویدیو/فایلی نفرستادی!")
        return

    send_final(bot, update.message.chat_id, uid)
    update.message.reply_text("همه چیز آماده و ارسال شد! ✅")


def button_handler(bot, update):
    query = update.callback_query
    uid = query.from_user.id
    data = query.data

    if data == "mode_film":
        if not user_data.get(uid):
            reset_user(uid)
        user_data[uid]["mode"] = "film"
        query.answer()
        if user_data[uid].get("poster"):
            query.edit_message_text("حالت فیلم ثبت شد ✅")
            build_and_show_result(bot, query.message.chat_id, uid)
        else:
            query.edit_message_text("حالت فیلم\n\nپوستر آماده رو فوروارد کن")
        return

    if data == "mode_serial":
        if not user_data.get(uid):
            reset_user(uid)
        user_data[uid]["mode"] = "serial"
        query.answer()
        if user_data[uid].get("poster"):
            query.edit_message_text("حالت سریال ثبت شد ✅")
            build_and_show_result(bot, query.message.chat_id, uid)
        else:
            query.edit_message_text("حالت سریال\n\nپوستر آماده رو فوروارد کن")
        return

    if data == "cancel_all":
        reset_user(uid)
        query.answer("لغو شد")
        query.edit_message_reply_markup(reply_markup=None)
        bot.send_message(chat_id=query.message.chat_id, text="لغو شد. برای شروع دوباره /start بزن.")
        return

    query.answer()


def error_handler(bot, update, error):
    logger.error("خطا روی آپدیت %s: %s", update, error)


def main():
    if not TOKEN or TOKEN == "PUT_YOUR_BOT_TOKEN_HERE":
        raise RuntimeError(
            "توکن ربات تنظیم نشده. متغیر محیطی TELEGRAM_BOT_TOKEN را ست کن "
            "یا مقدار TOKEN را در فایل وارد کن."
        )

    updater = Updater(TOKEN)
    dp = updater.dispatcher
    dp.add_error_handler(error_handler)
    dp.add_handler(CommandHandler("start", start))
    dp.add_handler(CommandHandler("f", film_mode))
    dp.add_handler(CommandHandler("s", serial_mode))
    dp.add_handler(CommandHandler("cancel", cancel_cmd))
    dp.add_handler(CommandHandler("status", status_cmd))
    dp.add_handler(CommandHandler("name", name_cmd))
    dp.add_handler(CommandHandler("emojiid", emojiid_cmd))
    dp.add_handler(CommandHandler("d", d_cmd))
    dp.add_handler(CallbackQueryHandler(button_handler))
    dp.add_handler(MessageHandler(Filters.photo | Filters.video | Filters.document, receive_message))

    # منوی دستورات (همون چیزی که با زدن "/" توی چت میاد) — با کد ست می‌شه،
    # نیازی به بات‌فادر نیست. هر بار ربات بالا میاد دوباره ست می‌شه.
    updater.bot.set_my_commands([
        BotCommand("start", "شروع"),
        BotCommand("f", "حالت فیلم"),
        BotCommand("s", "حالت سریال"),
        BotCommand("d", "پردازش و دریافت نتیجه"),
        BotCommand("name", "اصلاح اسم استخراج‌شده"),
        BotCommand("status", "وضعیت فعلی"),
        BotCommand("emojiid", "گرفتن آیدی ایموجی پرمیوم (ریپلای کن)"),
        BotCommand("cancel", "لغو"),
    ])

    print("ربات NetMovis شروع به کار کرد!")
    updater.start_polling()
    updater.idle()


if __name__ == "__main__":
    main()
