import re
import json
import logging
import time
from urllib.parse import urlparse
from pathlib import Path
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, InputMediaPhoto, Update
from telegram.constants import ParseMode
from telegram.error import BadRequest, Forbidden
from telegram.ext import ContextTypes, MessageHandler, filters, CallbackQueryHandler
from bot.core.tasks import run_guarded
from bot.services.media_cache import cache_media, get_cached_media, retry_media_task
from bot.services.media_runtime import media_stats, run_media_task
from bot.services.media_sources import best_source, mark_source_failure, mark_source_success
from bot.handlers.dl_utils import TEMP_DIR, dl_via_ytdlp, download_file, get_http_client, format_size, best_quality_img, cache_get, cache_set, extract_info_ytdlp

logger = logging.getLogger(__name__)

TWITTER_PATTERN = re.compile(r"(?:https?://)?(?:www\.|mobile\.)?(?:twitter\.com|x\.com)/(\w+)/status/(\d{6,25})")
INSTAGRAM_PATTERN = re.compile(r"(?:https?://)?(?:www\.)?(?:instagram\.com)/(?:p|reel|reels)/([a-zA-Z0-9_-]+)")
YOUTUBE_PATTERN = re.compile(r"(?:https?://)?(?:www\.|m\.)?(?:youtube\.com|youtu\.be)/(?!channel/c|user/|@)(?:\S+)?")
YOUTUBE_VIDEO_ID = re.compile(r"(?:youtube\.com/watch\?v=|youtu\.be/|youtube\.com/embed/)([a-zA-Z0-9_-]{11})")

MAX_CAPTION_LEN = 900
MAX_DOCUMENT_MB = 50

TEMP_DIR.mkdir(parents=True, exist_ok=True)

PLATFORM_STYLES = {
    "twitter": {"icon": "🐦", "color": "#1DA1F2", "name": "توییتر", "accent": "🔵"},
    "instagram": {"icon": "📸", "color": "#E1306C", "name": "اینستاگرام", "accent": "🟣"},
    "youtube": {"icon": "🎬", "color": "#FF0000", "name": "یوتیوب", "accent": "🔴"},
}

def _html_escape(text: str) -> str:
    return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")

def _clean_tweet_text(text: str) -> str:
    if not text:
        return ""
    text = re.sub(r"https://t\.co/\S+", "", text).strip()
    return text

def _build_header(platform: str, step: str = "start") -> str:
    s = PLATFORM_STYLES.get(platform, PLATFORM_STYLES["twitter"])
    lines = [
        f"━━━━━━━━━━━━━━━━━━━━━",
        f"{s['icon']}  <b>دانلودر {s['name']}</b>",
        f"━━━━━━━━━━━━━━━━━━━━━",
    ]
    if step == "processing":
        lines.append(f"\n⏳ <i>در حال پردازش...</i>")
    elif step == "complete":
        lines.append(f"\n✅ <i>دانلود کامل شد</i>")
    return "\n".join(lines)

def _build_progress_bar(pct: float, width: int = 12) -> str:
    filled = int(pct * width)
    empty = width - filled
    bar = "▓" * filled + "░" * empty
    return f"<code>|{bar}| {pct*100:.0f}%</code>"

def _build_media_preview(media_count: dict) -> str:
    parts = []
    if media_count.get("photos", 0):
        parts.append(f"📷 {media_count['photos']} عکس")
    if media_count.get("videos", 0):
        parts.append(f"🎥 {media_count['videos']} ویدیو")
    return " • ".join(parts) if parts else ""

def _build_quality_keyboard(formats: list, prefix: str, uid: str = "") -> InlineKeyboardMarkup:
    rows = []
    for fmt in formats:
        label = fmt.get("label", "")
        cb = f"{prefix}:{fmt.get('id', '')}:{uid}" if uid else f"{prefix}:{fmt.get('id', '')}"
        quality_icon = fmt.get("icon", "🎬")
        badge = fmt.get("badge", "")
        btn_text = f"{quality_icon} {label}"
        if badge:
            btn_text += f" {badge}"
        rows.append([InlineKeyboardButton(btn_text, callback_data=cb)])
    rows.append([InlineKeyboardButton("❌ لغو", callback_data=f"{prefix}:cancel")])
    return InlineKeyboardMarkup(rows)

def _format_caption_with_meta(text: str, meta: dict) -> str:
    parts = []
    if text:
        parts.append(text)
    if meta.get("likes"):
        parts.append(f"❤️ {meta['likes']}")
    if meta.get("retweets"):
        parts.append(f"🔄 {meta['retweets']}")
    if meta.get("date"):
        parts.append(f"📅 {meta['date']}")
    if meta.get("duration"):
        parts.append(f"⏱ {meta['duration']}")
    if meta.get("author"):
        parts.append(f"👤 {meta['author']}")
    return " | ".join(parts)

async def _send_or_edit(update: Update, context: ContextTypes.DEFAULT_TYPE, text: str, keyboard=None, edit: bool = False, msg_id: int = None):
    if edit and msg_id:
        try:
            return await context.bot.edit_message_text(text=text, chat_id=update.effective_chat.id, message_id=msg_id, parse_mode=ParseMode.HTML, reply_markup=keyboard, disable_web_page_preview=True)
        except (BadRequest, Forbidden) as e:
            if "not modified" in str(e).lower():
                return None
            logger.warning("Edit failed, sending new: %s", e)
    return await context.bot.send_message(chat_id=update.effective_chat.id, text=text, parse_mode=ParseMode.HTML, reply_markup=keyboard, disable_web_page_preview=True)

async def _delete_msg(context: ContextTypes.DEFAULT_TYPE, chat_id: int, msg_id: int):
    try:
        await context.bot.delete_message(chat_id=chat_id, message_id=msg_id)
    except Exception:
        pass

async def _send_media_group_safe(update: Update, context: ContextTypes.DEFAULT_TYPE, media: list, chat_id: int = None, reply_to: int = None):
    try:
        chat_id = chat_id or update.effective_chat.id
        kwargs = {"chat_id": chat_id, "media": media}
        if reply_to:
            kwargs["reply_to_message_id"] = reply_to
        await context.bot.send_media_group(**kwargs)
    except BadRequest as e:
        logger.warning("Media group failed: %s", e)
        for m in media:
            try:
                await context.bot.send_photo(chat_id=chat_id, photo=m.media, caption=m.caption if hasattr(m, "caption") else None, reply_to_message_id=reply_to)
            except Exception as e2:
                logger.error("Send photo fallback failed: %s", e2)

# ─── Twitter / X ───────────────────────────────────────────────────────────────

async def twitter_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not update.message or not update.message.text:
        return
    urls = TWITTER_PATTERN.findall(update.message.text)
    if not urls:
        return

    header = _build_header("twitter", "processing")
    msg = await update.message.reply_text(header, disable_web_page_preview=True)

    for username, tweet_id in urls:
        tweet_url = f"https://twitter.com/{username}/status/{tweet_id}"
        await _process_tweet(update, context, tweet_url, tweet_id, msg.message_id)

async def _process_tweet(update: Update, context: ContextTypes.DEFAULT_TYPE, url: str, tweet_id: str, msg_id: int):
    cached = get_cached_media(f"tw:{tweet_id}") or cache_get(f"tw:{tweet_id}")

    if cached:
        await _send_or_edit(update, context, f"{_build_header('twitter', 'complete')}\n\n📦 از حافظه بارگذاری شد", edit=True, msg_id=msg_id)
        await _send_tweet_result(update, context, cached, msg_id)
        return

    data = None
    preferred = best_source("twitter")

    sources = ["ytdlp", "xscrape", "nitter"]

    if preferred and preferred in sources:
        sources.remove(preferred)
        sources.insert(0, preferred)

    source_names = {"ytdlp": "yt-dlp", "xscrape": "API", "nitter": "Nitter"}

    for i, source in enumerate(sources):
        status_parts = [_build_header("twitter", "processing")]
        tried = [source_names[s] for s in sources[:i]]
        if tried:
            status_parts.append(f"\n📡 منابع امتحان شده: {', '.join(tried)}")
        status_parts.append(f"\n📡 در حال تلاش با {source_names[source]}...")
        await _send_or_edit(update, context, "\n".join(status_parts), edit=True, msg_id=msg_id)

        if source == "ytdlp":
            data = await retry_media_task(
                lambda: run_media_task("twitter", url, _tw_fetch_ytdlp(url)),
            )

        elif source == "xscrape":
            data = await retry_media_task(
                lambda: run_media_task("twitter", url, _tw_fetch_x_scrape(url)),
            )

        elif source == "nitter":
            data = await retry_media_task(
                lambda: run_media_task("twitter", url, _tw_fetch_nitter(url)),
            )

        if data and data.get("media"):
            mark_source_success("twitter", source)
            break

        mark_source_failure("twitter", source)

    if not data or not data.get("media"):
        await _send_or_edit(update, context, f"{_build_header('twitter')}\n\n❌ <b>خطا در دریافت توییت</b>\n\nهمه منابع امتحان شدن اما نتیجه‌ای نداشت.", edit=True, msg_id=msg_id)
        return

    cache_set(f"tw:{tweet_id}", data)
    cache_media(f"tw:{tweet_id}", data)
    await _send_or_edit(update, context, f"{_build_header('twitter', 'complete')}\n\n✅ دریافت شد: {data.get('media_count_text', '')}", edit=True, msg_id=msg_id)
    await _send_tweet_result(update, context, data, msg_id)

async def _tw_fetch_ytdlp(url: str) -> dict | None:
    try:
        info = await run_guarded(
            "downloader.ytdlp",
            extract_info_ytdlp(url),
            timeout=20,
        )

        if not info:
            return None
        media = []
        if info.get("thumbnails"):
            thumb = info["thumbnails"][-1].get("url", "")
            if thumb:
                media.append({"type": "photo", "url": best_quality_img(thumb)})
        formats = info.get("formats", [])
        video_formats = [f for f in formats if f.get("vcodec") and f.get("vcodec") != "none"]
        if video_formats:
            best = max(video_formats, key=lambda f: f.get("height", 0) or 0)
            media.append({"type": "video", "url": best.get("url", ""), "height": best.get("height", 720)})
        text_parts = [info.get("description") or info.get("title") or ""]
        return _tw_build_data(text_parts, media, info)
    except Exception as e:
        logger.debug("yt-dlp failed for tweet: %s", e)
    return None

async def _tw_fetch_x_scrape(url: str) -> dict | None:
    try:
        api_url = f"https://api.vxtwitter.com/{url.split('/status/')[0].split('/')[-2]}/status/{url.split('/status/')[1].split('?')[0]}"

        async def _fetch():
            async with get_http_client(timeout=15) as client:
                return await client.get(api_url)

        resp = await run_guarded(
            "downloader.xscrape",
            _fetch(),
            timeout=20,
        )

        if not resp:
            return None

            if resp.status_code != 200:
                return None

            j = resp.json()
        media = []
        if j.get("media_extended"):
            for m in j["media_extended"][:10]:
                if m.get("type") == "image" or m.get("url", "").endswith((".jpg", ".png", ".webp")):
                    media.append({"type": "photo", "url": best_quality_img(m.get("url", ""))})
                elif m.get("type") == "video" and m.get("video"):
                    best_vid = max(m["video"].get("urls", []), key=lambda v: v.get("bitrate", 0)) if m["video"].get("urls") else {}
                    media.append({"type": "video", "url": best_vid.get("url", m["video"].get("url", "")), "height": best_vid.get("height", 720)})
        else:
            imgs = j.get("media_urls", [])
            for img in imgs:
                media.append({"type": "photo", "url": best_quality_img(img)})
        text_parts = [j.get("text", "")]
        return _tw_build_data(text_parts, media, j)
    except Exception as e:
        logger.debug("x scrape failed: %s", e)
    return None

async def _tw_fetch_nitter(url: str) -> dict | None:
    nitter_hosts = ["nitter.net", "nitter.1d4.us", "nitter.kavin.rocks", "nitter.poast.org", "nitter.spaceint.fr"]
    username = url.split("/status/")[0].split("/")[-1]
    tweet_id = url.split("/status/")[1].split("?")[0]

    for host in nitter_hosts:
        try:
            nitter_url = f"https://{host}/{username}/status/{tweet_id}"

            async def _fetch():
                async with get_http_client(timeout=15, follow_redirects=True) as client:
                    return await client.get(nitter_url, headers={"User-Agent": "Mozilla/5.0"})

            resp = await run_guarded(
                "downloader.nitter",
                _fetch(),
                timeout=20,
            )

            if not resp:
                continue

                if resp.status_code != 200:
                    continue

                html = resp.text
            media = []

            img_matches = re.findall(r'<img[^>]+src="([^"]+)"[^>]*class="[^"]*static-img[^"]*"', html)
            for img_url in img_matches:
                if img_url.startswith("//"):
                    img_url = "https:" + img_url
                elif img_url.startswith("/"):
                    img_url = f"https://{host}{img_url}"
                if any(ext in img_url for ext in [".jpg", ".png", ".webp", ".jpeg"]):
                    media.append({"type": "photo", "url": best_quality_img(img_url)})

            vid_matches = re.findall(r'<source[^>]+src="([^"]+)"[^>]*type="video/mp4"', html)
            for vid_url in vid_matches:
                if vid_url.startswith("//"):
                    vid_url = "https:" + vid_url
                elif vid_url.startswith("/"):
                    vid_url = f"https://{host}{vid_url}"
                media.append({"type": "video", "url": vid_url, "height": 720})

            text_match = re.search(r'<div class="tweet-content[^"]*"[^>]*>(.*?)</div>', html, re.DOTALL)
            text = ""
            if text_match:
                text = re.sub(r"<[^>]+>", "", text_match.group(1)).strip()

            stats = {}
            for s in ["comments", "retweets", "likes"]:
                m = re.search(rf'class="icon-{s}[^"]*"[^>]*>.*?<span[^>]*>([\d,]+)</span>', html, re.DOTALL)
                if m:
                    stats[s] = m.group(1)
            info = {"text": text, "likes": stats.get("likes"), "retweets": stats.get("retweets"), "replies": stats.get("comments")}

            if media:
                text_parts = [text]
                return _tw_build_data(text_parts, media, info)

        except Exception as e:
            logger.debug("Nitter %s failed: %s", host, e)
            continue
    return None

def _tw_build_data(text_parts: list, media: list, info: dict) -> dict:
    text = "\n".join(p for p in text_parts if p)
    text = _clean_tweet_text(text)
    meta = {
        "likes": info.get("likes"), "retweets": info.get("retweets"),
        "replies": info.get("replies"), "date": info.get("date"),
        "author": info.get("author_name") or info.get("author"),
    }
    caption = _format_caption_with_meta(_html_escape(text), meta)
    photos = len([m for m in media if m["type"] == "photo"])
    vids = len([m for m in media if m["type"] == "video"])
    count_text = []
    if photos: count_text.append(f"📷 {photos} عکس")
    if vids: count_text.append(f"🎥 {vids} ویدیو")
    return {"media": media, "caption": caption, "media_count_text": " • ".join(count_text)}

async def _send_tweet_result(update: Update, context: ContextTypes.DEFAULT_TYPE, data: dict, msg_id: int):
    media = data.get("media", [])
    caption = data.get("caption", "")[:MAX_CAPTION_LEN]

    if not media:
        await _send_or_edit(update, context, f"{_build_header('twitter')}\n\n❌ رسانه‌ای یافت نشد.", edit=True, msg_id=msg_id)
        return

    photos = [m for m in media if m["type"] == "photo"]
    vids = [m for m in media if m["type"] == "video"]

    await _delete_msg(context, update.effective_chat.id, msg_id)

    if photos and not vids:
        if len(photos) == 1:
            await update.message.reply_photo(photo=photos[0]["url"], caption=caption, parse_mode=ParseMode.HTML)
        else:
            group = []
            for i, p in enumerate(photos):
                cap = caption if i == 0 else None
                group.append(InputMediaPhoto(media=p["url"], caption=cap, parse_mode=ParseMode.HTML if cap else None))
            await _send_media_group_safe(update, context, group, reply_to=update.message.message_id)
    elif vids:
        vid = vids[0]
        keyboard = None
        if len(vids) > 1:
            qual_opts = {f"{v.get('height', 720)}p": v["url"] for v in vids}
            rows = [[InlineKeyboardButton(f"🎥 {k}", callback_data=f"twdl:q:{v}")] for k, v in qual_opts.items()]
            keyboard = InlineKeyboardMarkup(rows)
        await update.message.reply_video(video=vid["url"], caption=caption, parse_mode=ParseMode.HTML, reply_markup=keyboard, supports_streaming=True, width=1920, height=vid.get("height", 720))
        if photos:
            group = [InputMediaPhoto(media=p["url"]) for p in photos]
            await _send_media_group_safe(update, context, group)
    else:
        await update.message.reply_text("❌ فرمت رسانه پشتیبانی نمی‌شود.")

async def tw_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await query.answer()
    data = query.data
    parts = data.split(":", 2)
    if len(parts) < 2:
        return
    action = parts[1]
    if action == "q" and len(parts) >= 3:
        url = parts[2]
        try:
            await query.edit_message_reply_markup(reply_markup=None)
            await context.bot.send_video(chat_id=update.effective_chat.id, video=url, caption="🎥 کیفیت انتخاب شده", supports_streaming=True)
        except Exception as e:
            await query.edit_message_text(f"❌ خطا: {e}")
    elif action == "cancel":
        await query.edit_message_text("❌ لغو شد.")

# ─── Instagram ─────────────────────────────────────────────────────────────────

async def instagram_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not update.message or not update.message.text:
        return
    urls = INSTAGRAM_PATTERN.findall(update.message.text)
    if not urls:
        return

    header = _build_header("instagram", "processing")
    msg = await update.message.reply_text(header, disable_web_page_preview=True)

    for shortcode in urls:
        await _process_instagram(update, context, shortcode, msg.message_id)

async def _process_instagram(update: Update, context: ContextTypes.DEFAULT_TYPE, shortcode: str, msg_id: int):
    cached = cache_get(f"ig:{shortcode}")
    if cached:
        await _send_or_edit(update, context, f"{_build_header('instagram', 'complete')}\n\n📦 از حافظه بارگذاری شد", edit=True, msg_id=msg_id)
        await _send_ig_result(update, context, cached, msg_id)
        return

    url = f"https://instagram.com/p/{shortcode}"
    data = await _ig_fetch_ytdlp(url)

    if not data or not data.get("media"):
        await _send_or_edit(update, context, f"{_build_header('instagram')}\n\n❌ <b>خطا در دریافت پست</b>", edit=True, msg_id=msg_id)
        return

    cache_set(f"ig:{shortcode}", data)
    await _send_or_edit(update, context, f"{_build_header('instagram', 'complete')}\n\n✅ دریافت شد: {data.get('media_count_text', '')}", edit=True, msg_id=msg_id)
    await _send_ig_result(update, context, data, msg_id)

async def _ig_fetch_ytdlp(url: str) -> dict | None:
    try:
        info = await extract_info_ytdlp(url)
        if not info:
            return None

        media = []
        entries = info.get("entries") or [info]

        for entry in entries:
            if entry.get("thumbnails"):
                thumb = entry["thumbnails"][-1].get("url", "")
                if thumb:
                    media.append({"type": "photo", "url": best_quality_img(thumb)})
            formats = entry.get("formats", [])
            best_vid = None
            for f in formats:
                if f.get("vcodec") and f.get("vcodec") != "none" and f.get("acodec") and f.get("acodec") != "none":
                    if not best_vid or (f.get("height", 0) or 0) > (best_vid.get("height", 0) or 0):
                        best_vid = f
            if best_vid:
                media.append({"type": "video", "url": best_vid.get("url", ""), "height": best_vid.get("height", 720)})

            if entry.get("url") and not best_vid:
                url_lower = entry["url"].lower()
                if url_lower.endswith((".jpg", ".png", ".webp", ".jpeg")):
                    media.append({"type": "photo", "url": best_quality_img(entry["url"])})
                elif url_lower.endswith(".mp4"):
                    media.append({"type": "video", "url": entry["url"], "height": 720})

        meta = {}
        if info.get("description"):
            meta["text"] = _html_escape(_clean_tweet_text(info["description"]))
        if info.get("like_count"):
            meta["likes"] = info["like_count"]
        if info.get("author"):
            meta["author"] = info["author"]
        caption = _format_caption_with_meta(meta.get("text", ""), meta)
        photos = len([m for m in media if m["type"] == "photo"])
        vids = len([m for m in media if m["type"] == "video"])
        count_text = []
        if photos: count_text.append(f"📷 {photos} عکس")
        if vids: count_text.append(f"🎥 {vids} ویدیو")

        return {"media": media, "caption": caption, "media_count_text": " • ".join(count_text)}
    except Exception as e:
        logger.debug("Instagram yt-dlp failed: %s", e)
    return None

async def _send_ig_result(update: Update, context: ContextTypes.DEFAULT_TYPE, data: dict, msg_id: int):
    media = data.get("media", [])
    caption = data.get("caption", "")[:MAX_CAPTION_LEN]

    if not media:
        await _send_or_edit(update, context, f"{_build_header('instagram')}\n\n❌ رسانه‌ای یافت نشد.", edit=True, msg_id=msg_id)
        return

    await _delete_msg(context, update.effective_chat.id, msg_id)

    photos = [m for m in media if m["type"] == "photo"]
    vids = [m for m in media if m["type"] == "video"]

    if len(photos) > 1 and not vids:
        group = []
        for i, p in enumerate(photos):
            cap = caption if i == 0 else None
            group.append(InputMediaPhoto(media=p["url"], caption=cap, parse_mode=ParseMode.HTML if cap else None))
        await _send_media_group_safe(update, context, group, reply_to=update.message.message_id)
    elif photos and not vids:
        await update.message.reply_photo(photo=photos[0]["url"], caption=caption, parse_mode=ParseMode.HTML)
    elif vids:
        await update.message.reply_video(video=vids[0]["url"], caption=caption, parse_mode=ParseMode.HTML, supports_streaming=True)
    else:
        await update.message.reply_text("❌ فرمت ناشناخته.")

async def ig_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    data = query.data
    if data.startswith("igdl:cancel"):
        await query.answer("لغو شد")
        await query.edit_message_text("❌ لغو شد.")
    else:
        await query.answer("Instagram")

# ─── YouTube ───────────────────────────────────────────────────────────────────

async def youtube_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not update.message or not update.message.text:
        return
    text = update.message.text.strip()
    if not YOUTUBE_PATTERN.search(text):
        return

    vid_match = YOUTUBE_VIDEO_ID.search(text)
    if not vid_match:
        return
    video_id = vid_match.group(1)

    header = _build_header("youtube", "processing")

    msg = await update.message.reply_text(header + "\n\n🔍 در حال获取 اطلاعات ویدیو...", disable_web_page_preview=True)

    info = None
    try:
        info = await extract_info_ytdlp(f"https://youtube.com/watch?v={video_id}", download=False)
    except Exception as e:
        logger.debug("yt-dlp info failed: %s", e)

    if info:
        duration = info.get("duration", 0)
        duration_str = f"{duration//60}:{duration%60:02d}" if duration else "?"
        title = info.get("title", "ویدیو")
        author = info.get("channel", info.get("uploader", "?"))
        views = info.get("view_count", 0)
        rating = info.get("average_rating", 0)

        meta_lines = [
            f"\n🎬 <b>{_html_escape(title[:80])}</b>",
            f"👤 {_html_escape(author)}",
            f"⏱ {duration_str}  |  👁 {views:,} بازدید",
        ]
        if info.get("like_count"):
            meta_lines.append(f"👍 {info['like_count']:,}")

        if info.get("formats"):
            vids = [f for f in info["formats"] if f.get("vcodec") and f.get("vcodec") != "none"]
            audios = [f for f in info["formats"] if f.get("acodec") and f.get("acodec") != "none" and (not f.get("vcodec") or f.get("vcodec") == "none")]

            qual_rows = []
            seen_q = set()
            for f in vids:
                h = f.get("height", 0)
                if h and h not in seen_q:
                    seen_q.add(h)
                    ext = f.get("ext", "mp4").upper()
                    fs = format_size(f.get("filesize", 0)) if f.get("filesize") else ""
                    lbl = f"🎬 {h}p • {ext}"
                    if fs:
                        lbl += f" ({fs})"
                    badge = "★ بهترین" if h >= 1080 else ""
                    qual_rows.append({"label": lbl, "id": f"{h}", "icon": "🎬", "badge": badge})

            qual_rows.sort(key=lambda x: int(x["id"]) if x["id"].isdigit() else 0, reverse=True)

            audio_rows = []
            seen_a = set()
            for f in audios:
                abr = f.get("abr", 0)
                if abr and abr not in seen_a and len(audio_rows) < 4:
                    seen_a.add(abr)
                    fs = format_size(f.get("filesize", 0)) if f.get("filesize") else ""
                    lbl = f"🎵 {abr}kbps"
                    if fs:
                        lbl += f" • {fs}"
                    audio_rows.append({"label": lbl, "id": f"a{abr}", "icon": "🎵"})

            menu_rows = []
            if qual_rows:
                menu_rows.append([InlineKeyboardButton("🎬 ویدیو", callback_data="ytdl:nop")])
                for q in qual_rows[:6]:
                    menu_rows.append([InlineKeyboardButton(f"{q['label']}", callback_data=f"ytdl:video:{video_id}:{q['id']}")])
            if audio_rows:
                menu_rows.append([InlineKeyboardButton("🎵 صوت", callback_data="ytdl:nop")])
                for a in audio_rows[:3]:
                    menu_rows.append([InlineKeyboardButton(f"{a['label']}", callback_data=f"ytdl:audio:{video_id}:{a['id']}")])
            menu_rows.append([InlineKeyboardButton("❌ لغو", callback_data="ytdl:cancel")])

            keyboard = InlineKeyboardMarkup(menu_rows)
            await _send_or_edit(update, context, "".join([header, *meta_lines, "\n\n━━━━━━━━━━━━━━━━━━━━━\n📥 <b>انتخاب کیفیت:</b>"]), keyboard=keyboard, edit=True, msg_id=msg.message_id)
            return

    keyboard = InlineKeyboardMarkup([
        [InlineKeyboardButton("🎬 ویدیو (MP4)", callback_data=f"ytdl:video:{video_id}:best"),
         InlineKeyboardButton("🎵 فقط صوت (MP3)", callback_data=f"ytdl:audio:{video_id}:best")]
    ])
    await _send_or_edit(update, context, f"{header}\n\n📥 <b>انتخاب کنید:</b>", keyboard=keyboard, edit=True, msg_id=msg.message_id)

async def yt_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await query.answer()
    data = query.data
    parts = data.split(":", 3)
    if len(parts) < 3:
        return
    mode = parts[1]
    if mode == "cancel":
        await query.edit_message_text("❌ لغو شد.")
        return
    if mode == "nop":
        return
    video_id = parts[2]
    quality = parts[3] if len(parts) > 3 else "best"

    bar = _build_progress_bar(0.1)
    await query.edit_message_text(f"{_build_header('youtube', 'processing')}\n\n{bar}\n⬇️ در حال دانلود...")

    url = f"https://youtube.com/watch?v={video_id}"
    result = None

    try:
        if mode == "video":
            q = quality if quality != "best" else "best[ext=mp4]/best"
            result = await dl_via_ytdlp(url, f"yt_{video_id}.mp4", q)
        elif mode == "audio":
            result = await dl_via_ytdlp(url, f"yt_{video_id}.mp3", "bestaudio/best", postprocessors=[{
                "key": "FFmpegExtractAudio",
                "preferredcodec": "mp3",
                "preferredquality": quality.replace("a", "") if quality != "best" else "192"
            }])

        if not result or not Path(result).exists():
            await query.edit_message_text(f"{_build_header('youtube')}\n\n❌ دانلود ناموفق بود.")
            return

        bar = _build_progress_bar(1.0)
        file_path = Path(result)
        size = file_path.stat().st_size
        size_mb = size / (1024 * 1024)

        title = file_path.stem[:60]
        caption = f"🎬 {_html_escape(title)} | 📦 {format_size(size)}"
        done_msg = f"{_build_header('youtube', 'complete')}\n\n{bar}\n📦 {format_size(size)}\n\n⬆️ در حال ارسال..."

        if mode == "video" and size_mb <= MAX_DOCUMENT_MB:
            await query.edit_message_text(done_msg)
            await query.delete_message()
            with open(file_path, "rb") as f:
                await context.bot.send_video(chat_id=update.effective_chat.id, video=f, caption=caption, parse_mode=ParseMode.HTML, supports_streaming=True, filename=f"{title}.mp4")
        elif mode == "video":
            await query.edit_message_text(done_msg)
            await query.delete_message()
            with open(file_path, "rb") as f:
                await context.bot.send_document(chat_id=update.effective_chat.id, document=f, caption=caption, parse_mode=ParseMode.HTML, filename=f"{title}.mp4")
        elif mode == "audio":
            await query.edit_message_text(done_msg)
            await query.delete_message()
            with open(file_path, "rb") as f:
                await context.bot.send_audio(chat_id=update.effective_chat.id, audio=f, caption=caption, parse_mode=ParseMode.HTML, filename=f"{title}.mp3", title=title)

        file_path.unlink(missing_ok=True)

    except Exception as e:
        logger.error("YouTube download error: %s", e)
        try:
            await query.edit_message_text(f"{_build_header('youtube')}\n\n❌ خطا: {str(e)[:200]}")
        except Exception:
            pass

# ─── Registration ──────────────────────────────────────────────────────────────

def register(app) -> None:
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, twitter_handler), group=-2)
    app.add_handler(CallbackQueryHandler(tw_callback, pattern=r"^twdl:"), group=-2)
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, instagram_handler), group=-2)
    app.add_handler(CallbackQueryHandler(ig_callback, pattern=r"^igdl:"), group=-2)
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, youtube_handler), group=-2)
    app.add_handler(CallbackQueryHandler(yt_callback, pattern=r"^ytdl:"), group=-2)
    logger.info("Downloaders registered (v3.1)")
