"""
User-facing handlers.

Covers: /start, /help, /cancel, main-menu callbacks, URL processing
(metadata extraction -> quality selection -> download -> upload),
single editable progress message and download-session management.
"""

from __future__ import annotations

import asyncio
import logging
import time
import uuid
from pathlib import Path
from typing import Any, Optional

from telegram import Message, Update
from telegram.constants import ChatAction, ParseMode
from telegram.error import TelegramError
from telegram.ext import ContextTypes
from yt_dlp.utils import DownloadError

import config
import downloader
import force_join
import keyboards
import utils
from downloader import DownloadCancelledError, DownloadState

logger = logging.getLogger(__name__)

# Throttle for progress-message edits (avoids Telegram flood limits).
PROGRESS_EDIT_INTERVAL = 2.0


# --------------------------------------------------------------------------- #
# Small shared helpers
# --------------------------------------------------------------------------- #

async def _effective_max_size_mb(db) -> int:
    return await db.get_setting_int("max_file_size_mb", config.MAX_FILE_SIZE_MB)


async def _ensure_status(
    context: ContextTypes.DEFAULT_TYPE,
    chat_id: int,
    status_message: Optional[Message],
    text: str,
    reply_markup: Any = None,
) -> Message:
    """Edit an existing status message, or send a new one as a fallback."""
    if isinstance(status_message, Message):
        try:
            await status_message.edit_text(
                text, parse_mode=ParseMode.HTML, reply_markup=reply_markup
            )
            return status_message
        except TelegramError:
            pass
    return await context.bot.send_message(
        chat_id, text, parse_mode=ParseMode.HTML, reply_markup=reply_markup
    )


# --------------------------------------------------------------------------- #
# Commands
# --------------------------------------------------------------------------- #

async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Handle /start: register the user and show the welcome + main menu."""
    if update.effective_user is None or update.message is None:
        return
    db = context.bot_data["db"]
    user = update.effective_user
    await db.upsert_user(user.id, user.username, user.first_name)
    logger.info("User started bot: %s (@%s)", user.id, user.username)
    await update.message.reply_html(
        "سلام 👋\n\n"
        "به ربات دانلود از اینستا خوش آمدید.\n\n"
        "🔗لینک پست یا استوری که کپی کردیو بفرست ",
        reply_markup=keyboards.main_menu(),
    )


def _help_text(limit_mb: int) -> str:
    return (
        "❤️ <b>پشتیبانی</b>\n\n"
        "جهت هر گونه انتقاد و پیشنهاد، درخواست همکاری "
        "یا هرگونه مشکل با آیدی زیر در ارتباط باشید.\n\n"
        "📞 آیدی پشتیبانی:\n"
        "@dlarz_support"
    )


async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Handle /help."""
    if update.effective_user is None or update.message is None:
        return
    db = context.bot_data["db"]
    await update.message.reply_html(
        _help_text(await _effective_max_size_mb(db)),
        reply_markup=keyboards.back_to_menu(),
    )


async def cancel_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Handle /cancel: abort the active download or a pending admin action."""
    if update.effective_user is None or update.message is None:
        return
    user_id = update.effective_user.id
    token = utils.USER_ACTIVE.get(user_id)
    if token and token in utils.SESSIONS:
        state = utils.SESSIONS[token].get("state")
        if state is not None:
            state.cancelled = True
        utils.SESSIONS[token]["status"] = "cancelled"
        logger.info("Download cancel requested via /cancel: user=%s", user_id)
        await update.message.reply_html("⏳ درخواست لغو ثبت شد؛ لطفاً چند لحظه صبر کنید...")
        return
    if user_id in utils.ADMIN_PENDING:
        utils.ADMIN_PENDING.pop(user_id, None)
        utils.PENDING_DATA.pop(user_id, None)
        await update.message.reply_html("❌ عملیات فعلی لغو شد.")
        return
    await update.message.reply_html("ℹ️ چیزی برای لغو وجود ندارد.")


# --------------------------------------------------------------------------- #
# Main-menu / session callbacks
# --------------------------------------------------------------------------- #

async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Route menu callbacks, quality selection and download cancellation."""
    query = update.callback_query
    if query is None or update.effective_user is None:
        return
    data = query.data or ""

    if data == keyboards.CB_MENU:
        await query.answer()
        await utils.safe_edit(
            query.message,
            "سلام 👋\n\nبه ربات دانلود از اینستا خوش آمدید.\n\n"
            "🔗 لینک ویدیو را ارسال کنید تا اطلاعات آن را دریافت کنیم.",
            keyboards.main_menu(),
        )
    elif data == keyboards.CB_HELP:
        db = context.bot_data["db"]
        await query.answer()
        await utils.safe_edit(
            query.message, _help_text(await _effective_max_size_mb(db)), keyboards.back_to_menu()
        )
    elif data == keyboards.CB_STATS:
        await query.answer()
        await _show_user_stats(update, context)

    elif data == keyboards.CB_DOWNLOAD:
        await query.answer()
        await query.message.edit_text(
            "🔗 لینک ویدیو را ارسال کنید:"
        )

    elif data.startswith("q:"):
        await _handle_quality_selected(update, context, data)

    elif data.startswith("cancel:"):
        await _handle_download_cancel(update, context, data)
        
async def _show_user_stats(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """📊 آمار من: user id, total downloads, registration date."""
    query = update.callback_query
    db = context.bot_data["db"]
    user = update.effective_user
    stats = await db.user_stats(user.id)
    if stats is None:
        await db.upsert_user(user.id, user.username, user.first_name)
        stats = await db.user_stats(user.id)
    text = (
        "📊 <b>آمار شما</b>\n\n"
        f"👤 شناسه: <code>{user.id}</code>\n"
        f"📥 تعداد دانلودها: {stats['downloads_count'] if stats else 0}\n"
        f"📅 تاریخ عضویت: {stats['joined_at'] if stats else 'نامشخص'}"
    )
    await utils.safe_edit(query.message, text, keyboards.back_to_menu())


async def _handle_quality_selected(
    update: Update, context: ContextTypes.DEFAULT_TYPE, data: str
) -> None:
    """Handle q:<token>:<quality> — start the download for the session owner."""
    query = update.callback_query
    user = update.effective_user
    db = context.bot_data["db"]

    parts = data.split(":")
    if len(parts) != 3:
        return
    _, token, quality = parts

    session = utils.SESSIONS.get(token)
    if session is None:
        await query.answer("⏳ این پیام منقضی شده است.", show_alert=True)
        await utils.safe_edit(
            query.message, "⏳ این پیام منقضی شده است. لطفاً لینک را دوباره ارسال کنید.", None
        )
        return
    if session.get("user_id") != user.id:
        # Callback security: only the session owner may act on it.
        await query.answer("❌ دسترسی غیرمجاز.", show_alert=True)
        return
    if session.get("status") != "ready":
        await query.answer("ℹ️ این درخواست قبلاً پردازش شده است.")
        return
    if quality not in downloader.QUALITY_FORMATS:
        await query.answer("❌ کیفیت نامعتبر است.", show_alert=True)
        return
    if await db.is_user_banned(user.id):
        await query.answer("🚫 شما از استفاده از ربات مسدود شده‌اید.", show_alert=True)
        await utils.safe_edit(query.message, "🚫 شما از استفاده از ربات مسدود شده‌اید.", None)
        return

    if await force_join.is_enabled(db):
        entries = await force_join.get_active_entries(db)
        if entries:
            allowed, missing = await force_join.check_membership(context.bot, user.id, entries)
            if not allowed:
                utils.PENDING_URL[user.id] = {"url": session["url"], "ts": time.time()}
                await query.answer("❌ ابتدا در همه کانال‌ها/گروه‌های اجباری عضو شوید.", show_alert=True)
                await utils.safe_edit(
                    query.message,
                    force_join.build_page_text(missing),
                    keyboards.force_join_user_kb(missing),
                )
                return

    if user.id in utils.USER_ACTIVE:
        await query.answer("⏳ یک دانلود فعال دارید؛ ابتدا آن را لغو یا کامل کنید.", show_alert=True)
        return
    if quality == "mp3" and not downloader.ffmpeg_available():
        await query.answer("❌ تبدیل MP3 نیاز به FFmpeg دارد که روی سرور نصب نیست.", show_alert=True)
        await utils.safe_edit(
            query.message, "❌ تبدیل MP3 نیاز به FFmpeg دارد که روی سرور نصب نیست.", None
        )
        return

    await query.answer("⏳ در حال آماده‌سازی دانلود...")
    session["status"] = "downloading"
    session["quality"] = quality
    state = DownloadState()
    session["state"] = state
    utils.USER_ACTIVE[user.id] = token
    session["download_id"] = await db.add_download(user.id, session["url"], session["title"], quality)
    logger.info("Download started: user=%s quality=%s url=%s", user.id, quality, session["url"])

    initial_text = _progress_text(state)
    progress_message = query.message
    if not isinstance(progress_message, Message):
        progress_message = await context.bot.send_message(
            session["chat_id"],
            initial_text,
            parse_mode=ParseMode.HTML,
            reply_markup=keyboards.cancel_download_kb(token),
        )
    else:
        try:
            await progress_message.edit_text(
                initial_text,
                parse_mode=ParseMode.HTML,
                reply_markup=keyboards.cancel_download_kb(token),
            )
        except TelegramError:
            progress_message = await context.bot.send_message(
                session["chat_id"],
                initial_text,
                parse_mode=ParseMode.HTML,
                reply_markup=keyboards.cancel_download_kb(token),
            )

    context.application.create_task(_download_worker(context, session, progress_message))


async def _handle_download_cancel(
    update: Update, context: ContextTypes.DEFAULT_TYPE, data: str
) -> None:
    """Handle cancel:<token> — flag the session as cancelled; the worker cleans up."""
    query = update.callback_query
    user = update.effective_user
    token = data.split(":", 1)[1]

    session = utils.SESSIONS.get(token)
    if session is None:
        await query.answer("ℹ️ چیزی برای لغو نیست.")
        return
    if session.get("user_id") != user.id:
        await query.answer("❌ دسترسی غیرمجاز.", show_alert=True)
        return
    state = session.get("state")
    if state is not None:
        state.cancelled = True
    session["status"] = "cancelled"
    logger.info("Download cancel requested: user=%s", user.id)
    await query.answer("⏳ درخواست لغو ثبت شد...")


# --------------------------------------------------------------------------- #
# URL processing
# --------------------------------------------------------------------------- #

def _normalize_url(raw: str) -> str:
    if raw.startswith("www."):
        return f"https://{raw}"
    return raw


async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Receive any non-command text and treat it as a video URL."""
    user = update.effective_user
    message = update.message
    if user is None or message is None or update.effective_chat is None:
        return
    db = context.bot_data["db"]
    await db.upsert_user(user.id, user.username, user.first_name)

    url = _normalize_url((message.text or "").strip())
    if not utils.is_valid_url(url):
        await message.reply_html("❌ لینک وارد شده معتبر نیست. لطفاً یک لینک ویدیو ارسال کنید.")
        return
    logger.info("URL received: user=%s url=%s", user.id, url)

    if await db.is_user_banned(user.id):
        await message.reply_html("🚫 شما از استفاده از ربات مسدود شده‌اید.")
        return
    if user.id in utils.USER_ACTIVE:
        await message.reply_html(
            "⏳ یک دانلود برای شما در حال انجام است. لطفاً صبر کنید یا با /cancel آن را لغو کنید."
        )
        return

    if await force_join.is_enabled(db):
        entries = await force_join.get_active_entries(db)
        if entries:
            allowed, missing = await force_join.check_membership(context.bot, user.id, entries)
            if not allowed:
                utils.PENDING_URL[user.id] = {"url": url, "ts": time.time()}
                logger.info("Force join check failed: user=%s", user.id)
                await message.reply_html(
                    force_join.build_page_text(missing),
                    reply_markup=keyboards.force_join_user_kb(missing),
                )
                return

    await process_url(update, context, url)


async def process_url(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
    url: str,
    status_message: Optional[Message] = None,
) -> None:
    """Extract metadata and show the quality-selection UI.

    `status_message` is reused when resuming after force-join verification.
    """
    user = update.effective_user
    chat_id = update.effective_chat.id if update.effective_chat else None
    db = context.bot_data["db"]
    if chat_id is None or user is None:
        return

    if await db.is_user_banned(user.id):
        await _ensure_status(context, chat_id, status_message, "🚫 شما از استفاده از ربات مسدود شده‌اید.")
        return
    if user.id in utils.USER_ACTIVE:
        await _ensure_status(
            context, chat_id, status_message,
            "⏳ یک دانلود فعال دارید؛ لطفاً ابتدا آن را کامل یا لغو کنید.",
        )
        return
    if user.id in utils.USER_EXTRACTING:
        await _ensure_status(
            context, chat_id, status_message,
            "⏳ در حال پردازش لینک قبلی هستید؛ لطفاً کمی صبر کنید.",
        )
        return

    utils.USER_EXTRACTING.add(user.id)
    status_message = await _ensure_status(
        context, chat_id, status_message, "🔎 در حال دریافت اطلاعات ویدیو..."
    )
    try:
        info = await downloader.extract_info(url)
    except DownloadError as exc:
        logger.info("Extraction failed: url=%s error=%s", url, exc)
        await _ensure_status(context, chat_id, status_message, downloader.friendly_error(exc))
        return
    except Exception:  # noqa: BLE001
        logger.exception("Unexpected extraction error: url=%s", url)
        await _ensure_status(context, chat_id, status_message, "❌ دریافت اطلاعات ویدیو موفق نشد.")
        return
    finally:
        utils.USER_EXTRACTING.discard(user.id)

    title = str(info.get("title") or "بدون عنوان")
    duration = info.get("duration")
    uploader = info.get("uploader") or info.get("channel") or info.get("uploader_id")
    size_estimate = downloader.estimate_size(info)
    qualities = downloader.get_available_qualities(info)
    include_mp3 = downloader.ffmpeg_available()
    if not qualities and not include_mp3:
        await _ensure_status(
            context, chat_id, status_message,
            "❌ هیچ کیفیت قابل دانلودی برای این ویدیو پیدا نشد.",
        )
        return

    token = uuid.uuid4().hex[:10]
    utils.SESSIONS[token] = {
        "token": token,
        "user_id": user.id,
        "chat_id": chat_id,
        "url": url,
        "title": title,
        "duration": duration,
        "uploader": uploader,
        "size_estimate": size_estimate,
        "qualities": qualities,
        "status": "ready",
        "created": time.time(),
        "state": None,
        "download_id": None,
    }

    uploader_text = utils.escape_html(str(uploader)) if uploader else "نامشخص"
    text = (
        "✅ <b>اطلاعات ویدیو</b>\n\n"
        f"🎬 عنوان: {utils.escape_html(title)}\n"
        "🎥 کیفیت موردنظر را انتخاب کنید:"
    )
    await _ensure_status(
        context, chat_id, status_message, text,
        keyboards.qualities_kb(token, qualities, include_mp3),
    )


# --------------------------------------------------------------------------- #
# Force-join re-check callback
# --------------------------------------------------------------------------- #

async def handle_force_join_check(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Handle fj:check — re-check membership and resume the pending URL."""
    query = update.callback_query
    user = update.effective_user
    if query is None or user is None:
        return
    db = context.bot_data["db"]
    entries = await force_join.get_active_entries(db)

    allowed, missing = await force_join.check_membership(context.bot, user.id, entries)
    if not allowed:
        await query.answer("❌ ابتدا در همه کانال‌ها/گروه‌های اجباری عضو شوید.", show_alert=True)
        await utils.safe_edit(
            query.message,
            force_join.build_page_text(missing),
            keyboards.force_join_user_kb(missing),
        )
        return

    await query.answer("✅ عضویت شما تأیید شد.")
    logger.info("Force join verified: user=%s", user.id)
    pending = utils.PENDING_URL.pop(user.id, None)
    if pending and pending.get("url"):
        await process_url(update, context, str(pending["url"]), status_message=query.message)
    else:
        await utils.safe_edit(
            query.message,
            "✅ عضویت شما تأیید شد.\n\n🔗 لینک ویدیو را ارسال کنید.",
            keyboards.main_menu(),
        )


# --------------------------------------------------------------------------- #
# Download worker, progress rendering and upload
# --------------------------------------------------------------------------- #

def _progress_text(state: DownloadState) -> str:
    """Build the single editable progress message."""
    if state.phase == "downloading":
        speed = f"{utils.format_size(state.speed)}/s" if state.speed else "—"
        total_part = f" از {utils.format_size(state.total)}" if state.total else ""
        return (
            "⏳ <b>در حال دانلود...</b>\n\n"
            "━━━━━━━━━━━━━━\n"
            f"{utils.progress_bar(state.percent)} {state.percent:.0f}%\n"
            "━━━━━━━━━━━━━━\n\n"
            f"📦 حجم: {utils.format_size(state.downloaded)}{total_part}\n"
            f"⚡ سرعت: {speed}\n"
            f"⏱ باقی‌مانده: {utils.format_duration(state.eta)}"
        )
    if state.phase == "processing":
        return "⚙️ <b>در حال پردازش و ادغام فایل...</b>\n\nلطفاً صبر کنید..."
    return "📤 <b>در حال ارسال فایل به تلگرام...</b>\n\nلطفاً صبر کنید..."


async def _progress_updater(
    context: ContextTypes.DEFAULT_TYPE, session: dict, progress_message: Message
) -> None:
    """Background task: edit the single progress message every few seconds."""
    state: DownloadState = session["state"]
    token = session["token"]
    last_text = ""
    while not state.finished:
        await asyncio.sleep(PROGRESS_EDIT_INTERVAL)
        if state.finished:
            break
        text = _progress_text(state)
        if text != last_text:
            await utils.safe_edit(progress_message, text, keyboards.cancel_download_kb(token))
            last_text = text


async def _upload_media(bot, session: dict, file_path: Path, size: int) -> None:
    """Upload the finished file to Telegram (video with streaming or MP3 audio)."""
    chat_id = session["chat_id"]
    caption = f"🎬 {utils.escape_html(session['title'])}\n📦 {utils.format_size(size)}"
    timeouts = {
        "read_timeout": 300,
        "write_timeout": 1200,
        "connect_timeout": 60,
        "pool_timeout": 60,
    }
    if session["quality"] == "mp3":
        await bot.send_chat_action(chat_id, ChatAction.UPLOAD_VOICE)
        with file_path.open("rb") as handle:
            await bot.send_audio(
                chat_id=chat_id,
                audio=handle,
                caption=caption,
                parse_mode=ParseMode.HTML,
                title=(session["title"] or "audio")[:60],
                **timeouts,
            )
    else:
        await bot.send_chat_action(chat_id, ChatAction.UPLOAD_VIDEO)
        with file_path.open("rb") as handle:
            await bot.send_video(
                chat_id=chat_id,
                video=handle,
                caption=caption,
                parse_mode=ParseMode.HTML,
                supports_streaming=True,
                **timeouts,
            )


async def _download_worker(
    context: ContextTypes.DEFAULT_TYPE, session: dict, progress_message: Message
) -> None:
    """Run the download -> size check -> upload lifecycle for one session."""
    db = context.bot_data["db"]
    token = session["token"]
    user_id = session["user_id"]
    state: DownloadState = session["state"]

    file_path: Optional[Path] = None
    final_size: Optional[int] = None
    outcome = "failed"
    final_text = "⚠️ خطای غیرمنتظره در دانلود رخ داد. لطفاً دوباره تلاش کنید."

    updater_task = asyncio.create_task(_progress_updater(context, session, progress_message))
    try:
        file_path, _info = await downloader.download_media(
            session["url"], session["quality"], state, token
        )
        final_size = file_path.stat().st_size
        limit_mb = await _effective_max_size_mb(db)
        if final_size > limit_mb * 1024 * 1024:
            outcome = "failed"
            final_text = "❌ حجم فایل بیشتر از حد مجاز ربات است."
        else:
            await _upload_media(context.bot, session, file_path, final_size)
            outcome = "completed"
    except DownloadCancelledError:
        outcome = "cancelled"
        final_text = "❌ دانلود لغو شد."
    except DownloadError as exc:
        logger.warning("Download failed: url=%s error=%s", session["url"], exc)
        outcome = "failed"
        final_text = downloader.friendly_error(exc)
    except TelegramError as exc:
        logger.error("Telegram upload failed: %s", exc)
        outcome = "failed"
        final_text = "❌ ارسال فایل به تلگرام ناموفق بود (احتمالاً حجم فایل بیش از حد مجاز ربات است)."
    except Exception:  # noqa: BLE001
        logger.exception("Unexpected error in download worker: url=%s", session["url"])
        outcome = "failed"
        final_text = "⚠️ خطای غیرمنتظره در دانلود رخ داد. لطفاً دوباره تلاش کنید."
    finally:
        state.finished = True
        try:
            await asyncio.wait_for(asyncio.gather(updater_task, return_exceptions=True), timeout=10)
        except asyncio.TimeoutError:
            updater_task.cancel()
        # Always clean temporary files (success or failure).
        if file_path is not None:
            await downloader.delete_file(file_path)
        try:
            await db.finish_download(session.get("download_id") or 0, outcome, final_size)
        except Exception:  # noqa: BLE001
            logger.exception("Failed to update the download record in DB")

    if outcome == "completed":
        try:
            await db.increment_user_downloads(user_id)
        except Exception:  # noqa: BLE001
            logger.exception("Failed to increment user download counter")
        final_text = (
            "✅ <b>دانلود کامل شد و فایل ارسال گردید.</b>\n\n"
            f"🎬 {utils.escape_html(session['title'])}\n"
            f"📦 {utils.format_size(final_size)}"
        )

    utils.USER_ACTIVE.pop(user_id, None)
    utils.SESSIONS.pop(token, None)
    logger.info("Download %s: user=%s url=%s", outcome, user_id, session["url"])
    await _ensure_status(context, session["chat_id"], progress_message, final_text)


# --------------------------------------------------------------------------- #
# Periodic cleanup job
# --------------------------------------------------------------------------- #

async def cleanup_job(context: ContextTypes.DEFAULT_TYPE) -> None:
    """Expire old sessions / pending URLs and delete stale files."""
    now = time.time()

    for token, session in list(utils.SESSIONS.items()):
        if session.get("status") == "downloading":
            continue
        if now - session.get("created", 0) > utils.SESSION_TTL:
            utils.SESSIONS.pop(token, None)

    for user_id, item in list(utils.PENDING_URL.items()):
        if now - item.get("ts", 0) > utils.PENDING_URL_TTL:
            utils.PENDING_URL.pop(user_id, None)

    for user_id, token in list(utils.USER_ACTIVE.items()):
        if token not in utils.SESSIONS:
            utils.USER_ACTIVE.pop(user_id, None)

    removed = await downloader.cleanup_old_files(2 * 3600)
    if removed:
        logger.info("Cleanup removed %d old file(s)", removed)
